diff --git a/.gitignore b/.gitignore index ba0d76014d..da8152446a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ e2e/pacts userserviceclient-userservice.json detectcontentlambda-contentprovider.json pact/bin +pact/lib +pact/data # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index c8f174abe4..0000000000 --- a/MANIFEST +++ /dev/null @@ -1,31 +0,0 @@ -# file GENERATED by distutils, do NOT edit -CHANGELOG.md -CONTRIBUTING.md -LICENSE -README.md -RELEASING.md -requirements_dev.txt -setup.cfg -setup.py -pact/__init__.py -pact/__version__.py -pact/broker.py -pact/constants.py -pact/consumer.py -pact/http_proxy.py -pact/matchers.py -pact/message_consumer.py -pact/message_pact.py -pact/message_provider.py -pact/pact.py -pact/provider.py -pact/verifier.py -pact/verify_wrapper.py -pact/bin/pact-2.0.3-linux-arm64.tar.gz -pact/bin/pact-2.0.3-linux-x86_64.tar.gz -pact/bin/pact-2.0.3-osx-arm64.tar.gz -pact/bin/pact-2.0.3-osx-x86_64.tar.gz -pact/bin/pact-2.0.3-windows-x86.zip -pact/bin/pact-2.0.3-windows-x86_64.zip -pact/cli/__init__.py -pact/cli/verify.py diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 160e1d2d42..0000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include LICENSE -include *.txt -include *.md -include pact/bin/* -prune pact/test -prune e2e diff --git a/Makefile b/Makefile index 10f39ede57..e6b35c175a 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ DOCS_DIR := ./docs PROJECT := pact-python -PYTHON_MAJOR_VERSION := 3.9 +PYTHON_MAJOR_VERSION := 3.11 sgr0 := $(shell tput sgr0) red := $(shell tput setaf 1) @@ -10,7 +10,6 @@ green := $(shell tput setaf 2) help: @echo "" @echo " clean to clear build and distribution directories" - @echo " deps to install the required files for development" @echo " examples to run the example end to end tests (consumer, fastapi, flask, messaging)" @echo " consumer to run the example consumer tests" @echo " fastapi to run the example FastApi provider tests" @@ -19,24 +18,16 @@ help: @echo " package to create a distribution package in /dist/" @echo " release to perform a release build, including deps, test, and package targets" @echo " test to run all tests" - @echo " venv to setup a venv under .venv using pyenv, if available" @echo "" .PHONY: release -release: deps test package +release: test package .PHONY: clean clean: - rm -rf build - rm -rf dist - rm -rf pact/bin - - -.PHONY: deps -deps: - pip install -r requirements_dev.txt -e . + hatch clean define CONSUMER @@ -105,34 +96,11 @@ examples: consumer flask fastapi messaging .PHONY: package package: - python setup.py sdist + hatch build .PHONY: test -test: deps - flake8 - pydocstyle pact - coverage erase - tox - coverage report -m --fail-under=100 - -.PHONY: venv -venv: - @if [ -d "./.venv" ]; then echo "$(red).venv already exists, not continuing!$(sgr0)"; exit 1; fi - @type pyenv >/dev/null 2>&1 || (echo "$(red)pyenv not found$(sgr0)"; exit 1) - - @echo "\n$(green)Try to find the most recent minor version of the major version specified$(sgr0)" - $(eval PYENV_VERSION=$(shell pyenv install -l | grep "\s\s$(PYTHON_MAJOR_VERSION)\.*" | tail -1 | xargs)) - @echo "$(PYTHON_MAJOR_VERSION) -> $(PYENV_VERSION)" - - @echo "\n$(green)Install the Python pyenv version if not already available$(sgr0)" - pyenv install $(PYENV_VERSION) -s - - @echo "\n$(green)Make a .venv dir$(sgr0)" - ~/.pyenv/versions/${PYENV_VERSION}/bin/python3 -m venv ${CURDIR}/.venv - - @echo "\n$(green)Make it 'available' to pyenv$(sgr0)" - ln -sf ${CURDIR}/.venv ~/.pyenv/versions/${PROJECT} - - @echo "\n$(green)Use it! (populate .python-version)$(sgr0)" - pyenv local ${PROJECT} +test: + hatch run all + hatch run test:all + coverage report -m --fail-under=100 \ No newline at end of file diff --git a/hatch_build.py b/hatch_build.py new file mode 100644 index 0000000000..68c3d399e4 --- /dev/null +++ b/hatch_build.py @@ -0,0 +1,147 @@ +"""Hatchling build hook for Pact binary download.""" + +from __future__ import annotations + +import os +import shutil +import typing +from pathlib import Path +from typing import Any + +from hatchling.builders.hooks.plugin.interface import BuildHookInterface +from packaging.tags import sys_tags + +ROOT_DIR = Path(__file__).parent.resolve() +PACT_VERSION = "2.0.3" +PACT_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" +PACT_DISTRIBUTIONS: list[tuple[str, str, str]] = [ + ("linux", "arm64", "tar.gz"), + ("linux", "x86_64", "tar.gz"), + ("osx", "arm64", "tar.gz"), + ("osx", "x86_64", "tar.gz"), + ("windows", "x86", "zip"), + ("windows", "x86_64", "zip"), +] + + +class PactBuildHook(BuildHookInterface): + """Custom hook to download Pact binaries.""" + + PLUGIN_NAME = "custom" + + def clean(self, versions: list[str]) -> None: # noqa: ARG002 + """Clean up any files created by the build hook.""" + for subdir in ["bin", "lib", "data"]: + shutil.rmtree(ROOT_DIR / "pact" / subdir, ignore_errors=True) + + def initialize( + self, + version: str, # noqa: ARG002 + build_data: dict[str, Any], + ) -> None: + """Hook into Hatchling's build process.""" + build_data["infer_tag"] = True + build_data["pure_python"] = False + + pact_version = os.getenv("PACT_VERSION", PACT_VERSION) + self.install_pact_binaries(pact_version) + + def install_pact_binaries(self, version: str) -> None: # noqa: PLR0912 + """ + Install the Pact standalone binaries. + + The binaries are installed in `pact/bin`, and the relevant version for + the current operating system is determined automatically. + + Args: + version: The Pact version to install. Defaults to the value in + `PACT_VERSION`. + """ + platform = typing.cast(str, next(sys_tags()).platform) + + if platform.startswith("macosx"): + os = "osx" + if platform.endswith("arm64"): + machine = "arm64" + elif platform.endswith("x86_64"): + machine = "x86_64" + else: + msg = f"Unknown macOS machine {platform}" + raise ValueError(msg) + url = PACT_URL.format(version=version, os=os, machine=machine, ext="tar.gz") + + elif platform.startswith("win"): + os = "windows" + + if platform.endswith("amd64"): + machine = "x86_64" + elif platform.endswith("x86"): + machine = "x86" + else: + msg = f"Unknown Windows machine {platform}" + raise ValueError(msg) + + url = PACT_URL.format(version=version, os=os, machine=machine, ext="zip") + + elif "linux" in platform: + os = "linux" + if platform.endswith("x86_64"): + machine = "x86_64" + elif platform.endswith("aarch64"): + machine = "arm64" + else: + msg = f"Unknown Linux machine {platform}" + raise ValueError(msg) + + url = PACT_URL.format(version=version, os=os, machine=machine, ext="tar.gz") + + else: + msg = f"Unknown platform {platform}" + raise ValueError(msg) + + self.download_and_extract_pact(url) + + def download_and_extract_pact(self, url: str) -> None: + """ + Download and extract the Pact binaries. + + If the download artifact is already present, it will be used instead of + downloading it again. + + Args: + url: The URL to download the Pact binaries from. + """ + filename = url.split("/")[-1] + artifact = ROOT_DIR / "pact" / "data" / filename + artifact.parent.mkdir(parents=True, exist_ok=True) + + if not filename.endswith((".zip", ".tar.gz")): + msg = f"Unknown artifact type {filename}" + raise ValueError(msg) + + if not artifact.exists(): + import requests + + response = requests.get(url, timeout=30) + response.raise_for_status() + with artifact.open("wb") as f: + f.write(response.content) + + if filename.endswith(".zip"): + import zipfile + + with zipfile.ZipFile(artifact) as f: + f.extractall(ROOT_DIR) + if filename.endswith(".tar.gz"): + import tarfile + + with tarfile.open(artifact) as f: + f.extractall(ROOT_DIR) + + # Move the README that is extracted from the Ruby standalone binaries to + # the `data` subdirectory. + if (ROOT_DIR / "pact" / "README.md").exists(): + shutil.move( + ROOT_DIR / "pact" / "README.md", + ROOT_DIR / "pact" / "data" / "README.md", + ) diff --git a/pact/constants.py b/pact/constants.py index 9c6f7b1171..2fcf2db1aa 100644 --- a/pact/constants.py +++ b/pact/constants.py @@ -1,48 +1,37 @@ """Constant values for the pact-python package.""" import os -from os.path import join, dirname, normpath +from pathlib import Path -def broker_client_exe(): +def broker_client_exe() -> str: """Get the appropriate executable name for this platform.""" - if os.name == 'nt': - return 'pact-broker.bat' - else: - return 'pact-broker' + if os.name == "nt": + return "pact-broker.bat" + return "pact-broker" -def message_exe(): +def message_exe() -> str: """Get the appropriate executable name for this platform.""" - if os.name == 'nt': - return 'pact-message.bat' - else: - return 'pact-message' + if os.name == "nt": + return "pact-message.bat" + return "pact-message" -def mock_service_exe(): +def mock_service_exe() -> str: """Get the appropriate executable name for this platform.""" - if os.name == 'nt': - return 'pact-mock-service.bat' - else: - return 'pact-mock-service' + if os.name == "nt": + return "pact-mock-service.bat" + return "pact-mock-service" -def provider_verifier_exe(): +def provider_verifier_exe() -> str: """Get the appropriate provider executable name for this platform.""" - if os.name == 'nt': - return 'pact-provider-verifier.bat' - else: - return 'pact-provider-verifier' - - -BROKER_CLIENT_PATH = normpath(join( - dirname(__file__), 'bin', 'pact', 'bin', broker_client_exe())) - -MESSAGE_PATH = normpath(join( - dirname(__file__), 'bin', 'pact', 'bin', message_exe())) - -MOCK_SERVICE_PATH = normpath(join( - dirname(__file__), 'bin', 'pact', 'bin', mock_service_exe())) - -VERIFIER_PATH = normpath(join( - dirname(__file__), 'bin', 'pact', 'bin', provider_verifier_exe())) + if os.name == "nt": + return "pact-provider-verifier.bat" + return "pact-provider-verifier" + +ROOT_DIR = Path(__file__).parent.resolve() +BROKER_CLIENT_PATH = ROOT_DIR / "bin" / broker_client_exe() +MESSAGE_PATH = ROOT_DIR / "bin" / message_exe() +MOCK_SERVICE_PATH = ROOT_DIR / "bin" / mock_service_exe() +VERIFIER_PATH = ROOT_DIR / "bin" / provider_verifier_exe() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..30e2533b8b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,157 @@ +[project] +name = "pact-python" +description = "Tool for creating and verifying consumer-driven contracts using the Pact framework." +dynamic = ["version"] + +authors = [{ name = "Matthew Balvanz", email = "matthew.balvanz@workiva.com" }] +maintainers = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] + +readme = "README.md" +license = { file = "LICENSE" } +keywords = ["pact", "contract-testing", "testing"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development :: Testing", +] + +requires-python = ">=3.8,<4.0" +dependencies = [ + "click ~= 8.1", + "fastapi ~= 0.103", + "psutil ~= 5.9", + "requests ~= 2.31", + "six ~= 1.16", + "uvicorn ~= 0.13", +] + +[project.urls] +"Homepage" = "https://pact.io" +"Repository" = "https://github.com/pact-foundation-pact-python" +"Documentation" = "https://docs.pact.io" +"Bug Tracker" = "https://github.com/pact-foundation/pact-python/issues" +"Changelog" = "https://github.com/pact-foundation/pact-python/blob/master/CHANGELOG.md" + +[project.scripts] +pact-verifier = "pact.cli.verify:main" + +[project.optional-dependencies] +types = ["mypy ~= 1.1", "types-requests ~= 2.31"] +test = [ + "coverage[toml] ~= 7.3", + "httpx ~= 0.24", + "mock ~= 5.1", + "pytest ~= 7.4", + "pytest-cov ~= 4.1", + "testcontainers ~= 3.7", +] +dev = [ + "pact-python[types]", + "pact-python[test]", + "black ~= 23.7", + "flask ~= 2.3", + "ruff ~= 0.0", +] + +################################################################################ +## Hatch Build Configuration +################################################################################ + +[build-system] +requires = ["hatchling", "packaging", "requests"] +build-backend = "hatchling.build" + +[tool.hatch.version] +path = "pact/__version__.py" + +[tool.hatch.build] +include = ["pact/**/*.py", "*.md", "LICENSE"] +artifacts = ["pact/bin/*", "pact/data/*"] + +[tool.hatch.build.targets.sdist] +# Ignore binaries in the source distribution, but include the data files +# so that they can be installed from the source distribution. +exclude = ["pact/bin/*"] + +[tool.hatch.build.targets.wheel] +# Ignore the data files in the wheel as their contents are already included +# in the package. +exclude = ["pact/data/*"] + +[tool.hatch.build.targets.wheel.hooks.custom] + +################################################################################ +## Hatch Environment Configuration +################################################################################ + +# Install dev dependencies in the default environment to simplify the developer +# workflow. +[tool.hatch.envs.default] +features = ["dev"] +extra-dependencies = ["hatchling", "packaging", "requests"] + +[tool.hatch.envs.default.scripts] +lint = ["black --check --diff {args:.}", "ruff {args:.}", "mypy {args:.}"] +test = "pytest --cov-config=pyproject.toml --cov=pact --cov=tests tests/" +# TODO: Adapt the examples to work in Hatch +all = ["lint", "tests"] + +# Test environment for running unit tests. This automatically tests against all +# supported Python versions. +[tool.hatch.envs.test] +features = ["test"] + +[[tool.hatch.envs.test.matrix]] +python = ["3.8", "3.9", "3.10", "3.11"] + +[tool.hatch.envs.test.scripts] +test = "pytest --cov-config=pyproject.toml --cov=pact --cov=tests tests/" +# TODO: Adapt the examples to work in Hatch +all = ["tests"] + +################################################################################ +## PyTest Configuration +################################################################################ + +[tool.pytest.ini_options] +addopts = ["--import-mode=importlib"] + +################################################################################ +## Coverage +################################################################################ + +[tool.coverage.report] +exclude_lines = [ + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "pragma: no cover", +] + +################################################################################ +## Ruff Configuration +################################################################################ + +[tool.ruff] +target-version = "py38" +select = ["ALL"] + +ignore = [ + "D203", # Require blank line before class docstring + "D212", # Multi-line docstring summary must start at the first line + "ANN101", # `self` must be typed + "ANN102", # `cls` must be typed +] + +[tool.ruff.pydocstyle] +convention = "google" diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index e179ddbee7..0000000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,26 +0,0 @@ -Click<=8.0.4; python_version < '3.7' -Click>=8.1.3; python_version >= '3.7' -coverage==5.4 -Flask==2.0.3; python_version < '3.7' -Flask==2.2.5; python_version >= '3.7' -configparser==3.5.0 -flake8==5.0.4 -mock==3.0.5 -psutil==5.9.4 -pycodestyle==2.9.0 -pydocstyle==4.0.1 -tox==3.27.1 -pytest==7.0.1; python_version < '3.7' -pytest==7.1.3; python_version >= '3.7' -pytest-cov==2.11.1 -requests==2.27.1; python_version < '3.7' -requests>=2.28.0; python_version >= '3.7' -urllib3>=1.26.12 -uvicorn==0.16.0; python_version < '3.7' -uvicorn>=0.19.0; python_version >= '3.7' -wheel==0.37.1; python_version < '3.7' -wheel==0.40.0; python_version >= '3.7' -markupsafe==2.0.1; python_version < '3.7' -markupsafe==2.1.2; python_version >= '3.7' -httpx==0.22.0; python_version < '3.7' -httpx==0.23.3; python_version >= '3.7' diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 8f66880100..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,24 +0,0 @@ -[coverage:report] -exclude_lines = - if __name__ == .__main__.: - pragma: no cover - -[flake8] -ignore = E226,E302,E41,W503 -max-line-length = 160 -max-complexity = 12 -exclude = .git,build,dist,venv,.venv,.tox,.pytest_cache,.direnv - -[nosetests] -with-coverage=true -cover-package=pact -cover-branches=true -with-xunit=true -xunit-file=nosetests.xml - -[pydocstyle] -match-dir=[^(test|\.)].* - - -[tool:pytest] -norecursedirs=examples diff --git a/setup.py b/setup.py deleted file mode 100644 index 2269af5f59..0000000000 --- a/setup.py +++ /dev/null @@ -1,294 +0,0 @@ -"""pact-python PyPI Package.""" - -import os -import platform -import shutil -import sys -import tarfile - -from zipfile import ZipFile - -from setuptools import setup -from setuptools.command.develop import develop -from setuptools.command.install import install -from distutils.command.sdist import sdist as sdist_orig - - -IS_64 = sys.maxsize > 2 ** 32 -PACT_STANDALONE_VERSION = '2.0.3' -PACT_STANDALONE_SUFFIXES = ['osx-x86_64.tar.gz', - 'osx-arm64.tar.gz', - 'linux-x86_64.tar.gz', - 'linux-arm64.tar.gz', - 'windows-x86_64.zip', - 'windows-x86.zip', - ] - -here = os.path.abspath(os.path.dirname(__file__)) - -about = {} -with open(os.path.join(here, "pact", "__version__.py")) as f: - exec(f.read(), about) - -class sdist(sdist_orig): - """Subclass sdist to download all standalone ruby applications into ./pact/bin.""" - - def run(self): - """Installs the dist.""" - package_bin_path = os.path.join(os.path.dirname(__file__), 'pact', 'bin') - - if os.path.exists(package_bin_path): - shutil.rmtree(package_bin_path, ignore_errors=True) - os.mkdir(package_bin_path) - - for suffix in PACT_STANDALONE_SUFFIXES: - filename = ('pact-{version}-{suffix}').format(version=PACT_STANDALONE_VERSION, suffix=suffix) - download_ruby_app_binary(package_bin_path, filename, suffix) - super().run() - - -class PactPythonDevelopCommand(develop): - """ - Custom develop mode installer for pact-python. - - When the package is installed using `python setup.py develop` or - `pip install -e` it will download and unpack the appropriate Pact - mock service and provider verifier. - """ - - def run(self): - """Install ruby command.""" - develop.run(self) - package_bin_path = os.path.join(os.path.dirname(__file__), 'pact', 'bin') - if not os.path.exists(package_bin_path): - os.mkdir(package_bin_path) - - install_ruby_app(package_bin_path, download_bin_path=None) - - -class PactPythonInstallCommand(install): - """ - Custom installer for pact-python. - - Installs the Python package and unpacks the platform appropriate version - of the Ruby mock service and provider verifier. - - User Options: - --bin-path An absolute folder path containing predownloaded pact binaries - that should be used instead of fetching from the internet. - """ - - user_options = install.user_options + [('bin-path=', None, None)] - - def initialize_options(self): - """Load our preconfigured options.""" - install.initialize_options(self) - self.bin_path = None - - def finalize_options(self): - """Load provided CLI arguments into our options.""" - install.finalize_options(self) - - def run(self): - """Install python binary.""" - install.run(self) - package_bin_path = os.path.join(self.install_lib, 'pact', 'bin') - if not os.path.exists(package_bin_path): - os.mkdir(package_bin_path) - install_ruby_app(package_bin_path, self.bin_path) - - -def install_ruby_app(package_bin_path: str, download_bin_path=None): - """ - Installs the ruby standalone application for this OS. - - :param package_bin_path: The path where we want our pact binaries unarchived. - :param download_bin_path: An optional path containing pre-downloaded pact binaries. - """ - binary = ruby_app_binary() - - # The compressed Pact .tar.gz, zip etc file is expected to be in download_bin_path (if provided). - # Otherwise we will look in package_bin_path. - source_dir = download_bin_path if download_bin_path else package_bin_path - pact_unextracted_path = os.path.join(source_dir, binary['filename']) - - if os.path.isfile(pact_unextracted_path): - # Already downloaded, so just need to extract - extract_ruby_app_binary(source_dir, package_bin_path, binary['filename']) - else: - if download_bin_path: - # An alternative source was provided, but did not contain the .tar.gz - raise RuntimeError('Could not find {} binary.'.format(pact_unextracted_path)) - else: - # Clean start, download an extract - download_ruby_app_binary(package_bin_path, binary['filename'], binary['suffix']) - extract_ruby_app_binary(package_bin_path, package_bin_path, binary['filename']) - - -def ruby_app_binary(): - """ - Determine the ruby app binary required for this OS. - - :return A dictionary of type {'filename': string, 'version': string, 'suffix': string } - """ - target_platform = platform.platform().lower() - - binary = ('pact-{version}-{suffix}') - if ("darwin" in target_platform or "macos" in target_platform) and ("aarch64" in platform.machine() or "arm64" in platform.machine()): - suffix = 'osx-arm64.tar.gz' - elif ("darwin" in target_platform or "macos" in target_platform) and IS_64: - suffix = 'osx-x86_64.tar.gz' - elif 'linux' in target_platform and IS_64 and "aarch64" in platform.machine(): - suffix = 'linux-arm64.tar.gz' - elif 'linux' in target_platform: - suffix = 'linux-x86_64.tar.gz' - elif 'windows' in target_platform and IS_64: - suffix = 'windows-x86_64.zip' - elif 'windows' in target_platform: - suffix = 'windows-x86.zip' - else: - msg = ('Unfortunately, {} is not a supported platform. Only Linux,' - ' Windows, and OSX are currently supported.').format( - platform.platform()) - raise Exception(msg) - - binary = binary.format(version=PACT_STANDALONE_VERSION, suffix=suffix) - return {'filename': binary, 'version': PACT_STANDALONE_VERSION, 'suffix': suffix} - -def download_ruby_app_binary(path_to_download_to, filename, suffix): - """ - Download `binary` into `path_to_download_to`. - - :param path_to_download_to: The path where binaries should be downloaded. - :param filename: The filename that should be installed. - :param suffix: The suffix of the standalone app to install. - """ - uri = ('https://github.com/pact-foundation/pact-ruby-standalone/releases' - '/download/v{version}/pact-{version}-{suffix}') - - if sys.version_info.major == 2: - from urllib import urlopen - else: - from urllib.request import urlopen - - path = os.path.join(path_to_download_to, filename) - resp = urlopen(uri.format(version=PACT_STANDALONE_VERSION, suffix=suffix)) - with open(path, 'wb') as f: - if resp.code == 200: - f.write(resp.read()) - else: - raise RuntimeError( - 'Received HTTP {} when downloading {}'.format( - resp.code, resp.url)) - -def extract_ruby_app_binary(source, destination, binary): - """ - Extract the ruby app binary from `source` into `destination`. - - :param source: The location of the binary to unarchive. - :param destination: The location to unarchive to. - :param binary: The binary that needs to be unarchived. - """ - path = os.path.join(source, binary) - if 'windows' in platform.platform().lower(): - with ZipFile(path) as f: - f.extractall(destination) - else: - with tarfile.open(path) as f: - def is_within_directory(directory, target): - - abs_directory = os.path.abspath(directory) - abs_target = os.path.abspath(target) - - prefix = os.path.commonprefix([abs_directory, abs_target]) - - return prefix == abs_directory - - def safe_extract(tar, path=".", members=None, *, numeric_owner=False): - - for member in tar.getmembers(): - member_path = os.path.join(path, member.name) - if not is_within_directory(path, member_path): - raise Exception("Attempted Path Traversal in Tar File") - - tar.extractall(path, members, numeric_owner=numeric_owner) - - safe_extract(f, destination) - - -def read(filename): - """Read file contents.""" - path = os.path.realpath(os.path.join(os.path.dirname(__file__), filename)) - with open(path, 'rb') as f: - return f.read().decode('utf-8') - - -dependencies = [ - 'psutil>=5.9.4', - 'six>=1.16.0', - 'fastapi>=0.67.0', - 'urllib3>=1.26.12', -] - -if sys.version_info < (3, 7): - dependencies += [ - 'click<=8.0.4', - 'httpx==0.22.0', - 'requests==2.27.1', - 'uvicorn==0.16.0', - ] -else: - dependencies += [ - 'click>=8.1.3', - 'httpx==0.23.3', - 'requests>=2.28.0', - 'uvicorn>=0.19.0', - ] - -# Classifiers: available ones listed at https://pypi.org/classifiers -CLASSIFIERS = [ - 'Development Status :: 5 - Production/Stable', - - 'Operating System :: OS Independent', - - 'Intended Audience :: Developers', - - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - - 'License :: OSI Approved :: MIT License', -] - -if __name__ == '__main__': - setup( - cmdclass={ - 'develop': PactPythonDevelopCommand, - 'install': PactPythonInstallCommand, - 'sdist': sdist}, - name='pact-python', - version=about['__version__'], - description=( - 'Tools for creating and verifying consumer driven ' - 'contracts using the Pact framework.'), - long_description=read('README.md'), - long_description_content_type='text/markdown', - author='Matthew Balvanz', - author_email='matthew.balvanz@workiva.com', - url='https://github.com/pact-foundation/pact-python', - entry_points=''' - [console_scripts] - pact-verifier=pact.cli.verify:main - ''', - classifiers=CLASSIFIERS, - python_requires='>=3.6,<4', - install_requires=dependencies, - packages=['pact', 'pact.cli'], - package_data={'pact': ['bin/*']}, - package_dir={'pact': 'pact'}, - license='MIT License') diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 2cfa5fdeb9..0000000000 --- a/tox.ini +++ /dev/null @@ -1,8 +0,0 @@ -[tox] -envlist=py{36,37,38,39,310,311}-{test,install} -[testenv] -deps= - test: -rrequirements_dev.txt -commands= - test: pytest --cov pact tests - install: python -c "import pact"