From 49197724000b3caf650272537ec987be1cdcec70 Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Sun, 12 Mar 2023 16:03:44 +0100 Subject: [PATCH 0001/1376] feat: add matchers for ISO 8601 date format This introduces `pact.Format.iso_8601_datetime()` method to match a string for a full ISO 8601 Date. This method does not do any sort of date validation, only checks if the string is according to the ISO 8601 spec. It differs from `pact.Format.timestamp`, `pact.Format.date` and `pact.Format.time` implementations in that it is more stringent and tests the string for exact match to the ISO 8601 dates format. Without `with_ms` parameter will match string containing ISO 8601 formatted dates as stated bellow: * 2016-12-15T20:16:01 * 2010-05-01T01:14:31.876 * 2016-05-24T15:54:14.00000Z * 1994-11-05T08:15:30-05:00 * 2002-01-31T23:00:00.1234-02:00 * 1991-02-20T06:35:26.079043+00:00 Otherwise, ONLY dates with milliseconds will match the pattern: * 2010-05-01T01:14:31.876 * 2016-05-24T15:54:14.00000Z * 2002-01-31T23:00:00.1234-02:00 * 1991-02-20T06:35:26.079043+00:00 This change aims to bring the capabilities of the python library into alignment with pact-foundation/docs.pact.io#88, since the existing functionality is a bit liberal and allows tests to pass even in cases where the dates do not conform to the ISO 8601 spec. --- README.md | 28 ++++++++++++----------- pact/matchers.py | 52 ++++++++++++++++++++++++++++++++++++++++++ tests/test_matchers.py | 42 ++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d679649d0..54048deca 100644 --- a/README.md +++ b/README.md @@ -257,23 +257,25 @@ Often times, you find yourself having to re-write regular expressions for common ```python from pact import Format Format().integer # Matches if the value is an integer -Format().ip_address # Matches if the value is a ip address +Format().ip_address # Matches if the value is an ip address ``` We've created a number of them for you to save you the time: -| matcher | description | -|-----------------|-------------------------------------------------------------------------------------------------| -| `identifier` | Match an ID (e.g. 42) | -| `integer` | Match all numbers that are integers (both ints and longs) | -| `decimal` | Match all real numbers (floating point and decimal) | -| `hexadecimal` | Match all hexadecimal encoded strings | -| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) | -| `timestamp` | Match a string containing an RFC3339 formatted timestapm (e.g. Mon, 31 Oct 2016 15:21:41 -0400) | -| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) | -| `ip_address` | Match string containing IP4 formatted address | -| `ipv6_address` | Match string containing IP6 formatted address | -| `uuid` | Match strings containing UUIDs | +| matcher | description | +|-------------------|-------------------------------------------------------------------------------------------------------------------------| +| `identifier` | Match an ID (e.g. 42) | +| `integer` | Match all numbers that are integers (both ints and longs) | +| `decimal` | Match all real numbers (floating point and decimal) | +| `hexadecimal` | Match all hexadecimal encoded strings | +| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) | +| `timestamp` | Match a string containing an RFC3339 formatted timestamp (e.g. Mon, 31 Oct 2016 15:21:41 -0400) | +| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) | +| `iso_datetime` | Match string containing ISO 8601 formatted dates (e.g. 2015-08-06T16:53:10+01:00) | +| `iso_datetime_ms` | Match string containing ISO 8601 formatted dates, enforcing millisecond precision (e.g. 2015-08-06T16:53:10.123+01:00) | +| `ip_address` | Match string containing IP4 formatted address | +| `ipv6_address` | Match string containing IP6 formatted address | +| `uuid` | Match strings containing UUIDs | These can be used to replace other matchers diff --git a/pact/matchers.py b/pact/matchers.py index 52a414804..fd929f6b6 100644 --- a/pact/matchers.py +++ b/pact/matchers.py @@ -264,6 +264,8 @@ def __init__(self): self.timestamp = self.timestamp() self.date = self.date() self.time = self.time() + self.iso_datetime = self.iso_8601_datetime() + self.iso_datetime_ms = self.iso_8601_datetime(with_ms=True) def integer_or_identifier(self): """ @@ -360,6 +362,52 @@ def time(self): ).time().isoformat() ) + def iso_8601_datetime(self, with_ms=False): + """ + Match a string for a full ISO 8601 Date. + + Does not do any sort of date validation, only checks if the string is + according to the ISO 8601 spec. + + This method differs from :func:`~pact.Format.timestamp`, + :func:`~pact.Format.date` and :func:`~pact.Format.time` implementations + in that it is more stringent and tests the string for exact match to + the ISO 8601 dates format. + + Without `with_ms` will match string containing ISO 8601 formatted dates + as stated bellow: + + * 2016-12-15T20:16:01 + * 2010-05-01T01:14:31.876 + * 2016-05-24T15:54:14.00000Z + * 1994-11-05T08:15:30-05:00 + * 2002-01-31T23:00:00.1234-02:00 + * 1991-02-20T06:35:26.079043+00:00 + + Otherwise, ONLY dates with milliseconds will match the pattern: + + * 2010-05-01T01:14:31.876 + * 2016-05-24T15:54:14.00000Z + * 2002-01-31T23:00:00.1234-02:00 + * 1991-02-20T06:35:26.079043+00:00 + + :param with_ms: Enforcing millisecond precision. + :type with_ms: bool + :return: a Term object with a date regex. + :rtype: Term + """ + date = [1991, 2, 20, 6, 35, 26] + if with_ms: + matcher = self.Regexes.iso_8601_datetime_ms.value + date.append(79043) + else: + matcher = self.Regexes.iso_8601_datetime.value + + return Term( + matcher, + datetime.datetime(*date, tzinfo=datetime.timezone.utc).isoformat() + ) + class Regexes(Enum): """Regex Enum for common formats.""" @@ -398,3 +446,7 @@ class Regexes(Enum): r'0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|' \ r'[12]\d{2}|3([0-5]\d|6[1-6])))?)' time_regex = r'^(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?$' + iso_8601_datetime = r'^\d{4}-[01]\d-[0-3]\d\x54[0-2]\d:[0-6]\d:' \ + r'[0-6]\d(?:\.\d+)?(?:(?:[+-]\d\d:\d\d)|\x5A)?$' + iso_8601_datetime_ms = r'^\d{4}-[01]\d-[0-3]\d\x54[0-2]\d:[0-6]\d:' \ + r'[0-6]\d\.\d+(?:(?:[+-]\d\d:\d\d)|\x5A)?$' diff --git a/tests/test_matchers.py b/tests/test_matchers.py index 1cc446f3b..241c965be 100644 --- a/tests/test_matchers.py +++ b/tests/test_matchers.py @@ -407,3 +407,45 @@ def test_time(self): }, }, ) + + def test_iso_8601_datetime(self): + date = self.formatter.iso_datetime.generate() + self.assertEqual( + date, + { + "json_class": "Pact::Term", + "json_class": "Pact::Term", + "data": { + "matcher": { + "json_class": "Regexp", + "s": self.formatter.Regexes.iso_8601_datetime.value, + "o": 0, + }, + "generate": datetime.datetime( + 1991, 2, 20, 6, 35, 26, + tzinfo=datetime.timezone.utc + ).isoformat(), + }, + }, + ) + + def test_iso_8601_datetime_mills(self): + date = self.formatter.iso_datetime_ms.generate() + self.assertEqual( + date, + { + "json_class": "Pact::Term", + "json_class": "Pact::Term", + "data": { + "matcher": { + "json_class": "Regexp", + "s": self.formatter.Regexes.iso_8601_datetime_ms.value, + "o": 0, + }, + "generate": datetime.datetime( + 1991, 2, 20, 6, 35, 26, 79043, + tzinfo=datetime.timezone.utc + ).isoformat(), + }, + }, + ) From 9ce2d6949eff8f67c7842a8b05d20e9e7d0c4553 Mon Sep 17 00:00:00 2001 From: Elliott Murray Date: Sun, 19 Feb 2023 11:28:01 +0000 Subject: [PATCH 0002/1376] chore: Releasing version 1.7.0 --- RELEASING.md | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index da65fa8e2..094ce22d7 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,29 +1,43 @@ # Releasing -1. Increment the version according to semantic versioning rules in `pact/__version__.py` +## Preparing the release +The easiest way is to just run the following command from the root folder with the HEAD commit on trunk and the appropriate version. We follow .. versioning. + + `$ script/release_prep.sh X.Y.Z` -2. To upgrade the versions of `pact-mock_service` and `pact-provider-verifier`, change the - `PACT_STANDALONE_VERSION` in `setup.py` to match the latest version available from the - [pact-ruby-standalone](https://github.com/pact-foundation/pact-ruby-standalone/releases) repository. +This script effectively runs the following: + +1. Increment the version according to semantic versioning rules in `pact/__version__.py` -3. Update the `CHANGELOG.md` using: +2. Update the `CHANGELOG.md` using: `$ git log --pretty=format:' * %h - %s (%an, %ad)' vX.Y.Z..HEAD` -4. Add files to git +3. Add files to git `$ git add CHANGELOG.md pact/__version__.py` -5. Commit +4. Commit `$ git commit -m "Releasing version X.Y.Z"` -6. Tag +5. Tag `$ git tag -a vX.Y.Z -m "Releasing version X.Y.Z" && git push origin master --tags` -7. Wait until travis has run and the new tag is available at https://github.com/pact-foundation/pact-python/releases/tag/vX.Y.Z +## Updating Pact Ruby +To upgrade the versions of `pact-mock_service` and `pact-provider-verifier`, change the +`PACT_STANDALONE_VERSION` in `setup.py` to match the latest version available from the +[pact-ruby-standalone](https://github.com/pact-foundation/pact-ruby-standalone/releases) repository. Do this before preparing the release. + +## Publishing to Pypi + +1. Wait until Github Actions have run and the new tag is available at https://github.com/pact-foundation/pact-python/releases/tag/vX.Y.Z + +2. Set the title to `pact-python-X.Y.Z` + +3. Save -8. Set the title to `pact-python-X.Y.Z` +4. Go to Github Actions for Pact Python and you should see an 'Upload Python Package action blocked for your version. -9. Save +5. Click this and then 'Review deployments'. Select 'Upload Python Package' and Approve deploy. If you can't do this you may need an administrator to give you permissions or do it for you. You should see in Slack #pact-python that the release has happened. Verify in [pypi](https://pypi.org/project/pact-python/) From 71f15298b2b0021f322ad1b6c43939d9e1268bac Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Wed, 5 Apr 2023 12:27:49 +0200 Subject: [PATCH 0003/1376] chore: do not add merge commits to the change log --- CHANGELOG.md | 1 - script/release_prep.sh | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cf8b8e0a..dbefd72a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,4 @@ ### 1.7.0 - * 9c1132e - Merge pull request #325 from pact-foundation/pactflow_camelcase (Yousaf Nabi, Fri Jan 27 12:29:59 2023 +0000) * 44cda33 - chore: /s/Pactflow/PactFlow (Yousaf Nabi, Thu Jan 26 16:11:54 2023 +0000) * 1bbdd37 - feat: Enhance provider states for pact-message (#322) (nsfrias, Tue Jan 24 17:04:29 2023 +0000) * 53ca129 - chore: add workflow to create a jira issue for pactflow team when smartbear-supported label added to github issue (Beth Skurrie, Wed Jan 18 10:51:05 2023 +1100) diff --git a/script/release_prep.sh b/script/release_prep.sh index 336231cfd..b6bf29ca2 100755 --- a/script/release_prep.sh +++ b/script/release_prep.sh @@ -18,7 +18,8 @@ mv tmp-version pact/__version__.py echo "Releasing $TAG_NAME" -echo -e "`git log --pretty=format:' * %h - %s (%an, %ad)' $LAST_TAG..HEAD`\n$(cat CHANGELOG.md)" > CHANGELOG.md +LOG_ENTRIES="$(git log --pretty=format:' * %h - %s (%an, %ad)' $LAST_TAG..HEAD | grep -v 'Merge pull request')" +echo -e "${LOG_ENTRIES}\n$(cat CHANGELOG.md)" > CHANGELOG.md echo -e "### $VERSION\n$(cat CHANGELOG.md)" > CHANGELOG.md echo "Appended Changelog to $VERSION" From e721d8156558cce117e67ff7527e4f624d6304c5 Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Wed, 5 Apr 2023 12:39:35 +0200 Subject: [PATCH 0004/1376] docs: reformat releasing documentation --- RELEASING.md | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 094ce22d7..f51cb722e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,43 +1,60 @@ # Releasing ## Preparing the release -The easiest way is to just run the following command from the root folder with the HEAD commit on trunk and the appropriate version. We follow .. versioning. - `$ script/release_prep.sh X.Y.Z` +The easiest way is to just run the following command from the root folder with +the HEAD commit on trunk and the appropriate version. We follow +`..` versioning. + + ```shell + $ script/release_prep.sh X.Y.Z + ``` This script effectively runs the following: 1. Increment the version according to semantic versioning rules in `pact/__version__.py` 2. Update the `CHANGELOG.md` using: - - `$ git log --pretty=format:' * %h - %s (%an, %ad)' vX.Y.Z..HEAD` + ```shell + $ git log --pretty=format:' * %h - %s (%an, %ad)' vX.Y.Z..HEAD + ``` 3. Add files to git - - `$ git add CHANGELOG.md pact/__version__.py` + ```shell + $ git add CHANGELOG.md pact/__version__.py + ``` 4. Commit - - `$ git commit -m "Releasing version X.Y.Z"` + ```shell + $ git commit -m "Releasing version X.Y.Z" + ``` 5. Tag - - `$ git tag -a vX.Y.Z -m "Releasing version X.Y.Z" && git push origin master --tags` + ```shell + $ git tag -a vX.Y.Z -m "Releasing version X.Y.Z" + $ git push origin master --tags + ``` ## Updating Pact Ruby + To upgrade the versions of `pact-mock_service` and `pact-provider-verifier`, change the `PACT_STANDALONE_VERSION` in `setup.py` to match the latest version available from the -[pact-ruby-standalone](https://github.com/pact-foundation/pact-ruby-standalone/releases) repository. Do this before preparing the release. +[pact-ruby-standalone](https://github.com/pact-foundation/pact-ruby-standalone/releases) +repository. Do this before preparing the release. -## Publishing to Pypi +## Publishing to pypi -1. Wait until Github Actions have run and the new tag is available at https://github.com/pact-foundation/pact-python/releases/tag/vX.Y.Z +1. Wait until GitHub Actions have run and the new tag is available at + https://github.com/pact-foundation/pact-python/releases/tag/vX.Y.Z 2. Set the title to `pact-python-X.Y.Z` 3. Save -4. Go to Github Actions for Pact Python and you should see an 'Upload Python Package action blocked for your version. +4. Go to GitHub Actions for Pact Python and you should see an 'Upload Python + Package' action blocked for your version. -5. Click this and then 'Review deployments'. Select 'Upload Python Package' and Approve deploy. If you can't do this you may need an administrator to give you permissions or do it for you. You should see in Slack #pact-python that the release has happened. Verify in [pypi](https://pypi.org/project/pact-python/) +5. Click this and then 'Review deployments'. Select 'Upload Python Package' + and Approve deploy. If you can't do this you may need an administrator to + give you permissions or do it for you. You should see in Slack #pact-python + that the release has happened. Verify in [pypi](https://pypi.org/project/pact-python/) From 19be499b910a143a67b74a253b3ec5b271390c51 Mon Sep 17 00:00:00 2001 From: Lukas Riedersberger Date: Fri, 14 Apr 2023 12:22:21 +0200 Subject: [PATCH 0005/1376] fix: fix cors parameter not doing anything If the cors parameter is set to True '--cors=*' is appended to the list of commands for the external mock service. Previously the parameter was ignored. --- pact/pact.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pact/pact.py b/pact/pact.py index 638d9de78..28126bfc6 100644 --- a/pact/pact.py +++ b/pact/pact.py @@ -214,6 +214,8 @@ def start_service(self): command.extend(['--sslcert', self.sslcert]) if self.sslkey: command.extend(['--sslkey', self.sslkey]) + if self.cors: + command.extend(['--cors']) self._process = Popen(command) self._wait_for_server_start() From 93db8ae71657d5967f146b3d37294d76ff69bc0e Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 21 Apr 2023 12:35:23 +0100 Subject: [PATCH 0006/1376] feat: support arm64 osx/linux --- MANIFEST | 9 +++++---- setup.py | 24 +++++++++++++----------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/MANIFEST b/MANIFEST index 610dd70df..5793cf027 100644 --- a/MANIFEST +++ b/MANIFEST @@ -21,9 +21,10 @@ pact/pact.py pact/provider.py pact/verifier.py pact/verify_wrapper.py -pact/bin/pact-1.88.83-linux-x86.tar.gz -pact/bin/pact-1.88.83-linux-x86_64.tar.gz -pact/bin/pact-1.88.83-osx.tar.gz -pact/bin/pact-1.88.83-win32.zip +pact/bin/pact-3.1.2.2-alpha-linux-arm64.tar.gz +pact/bin/pact-3.1.2.2-alpha-linux-x86_64.tar.gz +pact/bin/pact-3.1.2.2-alpha-osx-arm64.tar.gz +pact/bin/pact-3.1.2.2-alpha-osx-x86_64.tar.gz +pact/bin/pact-3.1.2.2-alpha-windows-x86_64.zip pact/cli/__init__.py pact/cli/verify.py diff --git a/setup.py b/setup.py index 98d67657d..ae547f876 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,12 @@ IS_64 = sys.maxsize > 2 ** 32 -PACT_STANDALONE_VERSION = '1.88.83' -PACT_STANDALONE_SUFFIXES = ['osx.tar.gz', +PACT_STANDALONE_VERSION = '3.1.2.2-alpha' +PACT_STANDALONE_SUFFIXES = ['osx-x86_64.tar.gz', + 'osx-arm64.tar.gz', 'linux-x86_64.tar.gz', - 'linux-x86.tar.gz', - 'win32.zip'] + 'linux-arm64.tar.gz', + 'windows-x86_64.zip'] here = os.path.abspath(os.path.dirname(__file__)) @@ -131,13 +132,14 @@ def ruby_app_binary(): target_platform = platform.platform().lower() binary = ('pact-{version}-{suffix}') - - if 'darwin' in target_platform or 'macos' in target_platform: - suffix = 'osx.tar.gz' - elif 'linux' in target_platform and IS_64: - suffix = 'linux-x86_64.tar.gz' + 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.tar.gz' + suffix = 'linux-x86_64.tar.gz' elif 'windows' in target_platform: suffix = 'win32.zip' else: @@ -157,7 +159,7 @@ def download_ruby_app_binary(path_to_download_to, filename, suffix): :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' + uri = ('https://github.com/you54f/pact-ruby-standalone/releases' '/download/v{version}/pact-{version}-{suffix}') if sys.version_info.major == 2: From 28440da2e1f6598b3d61a08c41f8382cf4bf4bf9 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 21 Apr 2023 15:37:30 +0100 Subject: [PATCH 0007/1376] ci: test arm64 on cirrus-ci / test win/osx on gh --- .cirrus.yml | 55 +++++++++++++++++++ .github/workflows/build_and_test.yml | 5 +- .../workflows/package_and_push_to_pypi.yml | 4 +- Dockerfile | 17 ++++++ Dockerfile.ubuntu | 30 ++++++++++ run-docker.sh | 9 +++ 6 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 .cirrus.yml create mode 100644 Dockerfile create mode 100644 Dockerfile.ubuntu create mode 100755 run-docker.sh diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 000000000..8f4a1b8e2 --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,55 @@ +BUILD_TEST_TASK_TEMPLATE: &BUILD_TEST_TASK_TEMPLATE + arch_check_script: + - uname -am + test_script: + - python --version + - python -m pip install --upgrade pip + - python -m pip install -r requirements_dev.txt + - python -m flake8 + - python -m pydocstyle pact + - python -m tox -e test + # - make examples + +linux_arm64_task: + env: + matrix: + - IMAGE: python:3.6-slim + - IMAGE: python:3.7-slim + - IMAGE: python:3.8-slim + - IMAGE: python:3.9-slim + - IMAGE: python:3.10-slim + arm_container: + image: $IMAGE + install_script: + - apt update --yes && apt install --yes gcc make + << : *BUILD_TEST_TASK_TEMPLATE + + +macosx_arm64_task: + macos_instance: + image: ghcr.io/cirruslabs/macos-ventura-base:latest + env: + PATH: ${HOME}/.pyenv/shims:${PATH} + matrix: + - PYTHON: 3.6 + - PYTHON: 3.7 + - PYTHON: 3.8 + - PYTHON: 3.9 + - PYTHON: 3.10 + install_script: + # Per the pyenv homebrew recommendations. + # https://github.com/pyenv/pyenv/wiki#suggested-build-environment + # - xcode-select --install # Unnecessary on Cirrus + - brew update + # - brew install openssl readline sqlite3 xz zlib + - brew install pyenv + - pyenv install ${PYTHON} + - pyenv global ${PYTHON} + - pyenv rehash + ## To install rosetta + # - softwareupdate --install-rosetta --agree-to-license + << : *BUILD_TEST_TASK_TEMPLATE + + + + diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 6624e484f..26e8651ed 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -19,7 +19,7 @@ jobs: - '3.9' - '3.10' - '3.11' - os: [ ubuntu-latest ] + os: [ ubuntu-latest, windows-latest, macos-latest ] # These versions are no longer supported by Python team, and may # eventually be dropped from GitHub Actions. @@ -29,7 +29,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -50,4 +50,5 @@ jobs: run: tox -e test - name: Test examples + if: runner.os == 'Linux' run: make examples diff --git a/.github/workflows/package_and_push_to_pypi.yml b/.github/workflows/package_and_push_to_pypi.yml index 485fdc16a..4336ce061 100644 --- a/.github/workflows/package_and_push_to_pypi.yml +++ b/.github/workflows/package_and_push_to_pypi.yml @@ -9,9 +9,9 @@ jobs: environment: "Upload Python Package" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.x' - name: Install dependencies diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..a048a2b3a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +ARG PYTHON_VERSION=3.6 +FROM python:$PYTHON_VERSION-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt update --yes && apt install --yes gcc make + +WORKDIR /app +COPY . /app + +RUN python -m pip install --upgrade pip +RUN python -m pip install -r requirements_dev.txt +RUN python -m flake8 +RUN python -m pydocstyle pact +RUN python -m tox -e test + +CMD ["sh","-c","python -m tox -e test"] \ No newline at end of file diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu new file mode 100644 index 000000000..aaf6224b9 --- /dev/null +++ b/Dockerfile.ubuntu @@ -0,0 +1,30 @@ +FROM ubuntu:22.04 +ENV DEBIAN_FRONTEND=noninteractive +ARG PYTHON_VERSION 3.9 + +#Set of all dependencies needed for pyenv to work on Ubuntu +RUN apt-get update \ + && apt-get install -y --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget ca-certificates curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev mecab-ipadic-utf8 git + +# Set-up necessary Env vars for PyEnv +ENV PYENV_ROOT /root/.pyenv +ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH + +# Install pyenv +RUN set -ex \ + && curl https://pyenv.run | bash \ + && pyenv update \ + && pyenv install $PYTHON_VERSION \ + && pyenv global $PYTHON_VERSION \ + && pyenv rehash + +WORKDIR /app +COPY . /app + +RUN python -m pip install --upgrade pip +RUN python -m pip install -r requirements_dev.txt +RUN python -m flake8 +RUN python -m pydocstyle pact +RUN python -m tox -e test + +CMD ["sh","-c","python -m tox -e test"] \ No newline at end of file diff --git a/run-docker.sh b/run-docker.sh new file mode 100755 index 000000000..ef98a520b --- /dev/null +++ b/run-docker.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +for arch in arm64 amd64; do + # for version in 3.6; do + for version in 3.7 3.8 3.9 3.10 3.11; do + docker build -t python-$arch-$version --build-arg PYTHON_VERSION=$version --platform=linux/$arch . + docker run -it --rm python-$arch-$version + done +done \ No newline at end of file From 7aff538177d028e685e5b4d48a650ae1dad3fb3a Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 21 Apr 2023 15:44:48 +0100 Subject: [PATCH 0008/1376] feat: support x86 and x86_64 windows --- .cirrus.yml | 2 +- setup.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 8f4a1b8e2..6a476c228 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -31,7 +31,7 @@ macosx_arm64_task: env: PATH: ${HOME}/.pyenv/shims:${PATH} matrix: - - PYTHON: 3.6 + # - PYTHON: 3.6 # This works locally, with cirrus run, but fails in CI - PYTHON: 3.7 - PYTHON: 3.8 - PYTHON: 3.9 diff --git a/setup.py b/setup.py index ae547f876..c1a25af70 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,9 @@ 'osx-arm64.tar.gz', 'linux-x86_64.tar.gz', 'linux-arm64.tar.gz', - 'windows-x86_64.zip'] + 'windows-x86_64.zip', + 'windows-x86.zip', + ] here = os.path.abspath(os.path.dirname(__file__)) @@ -140,8 +142,10 @@ def ruby_app_binary(): 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 = 'win32.zip' + suffix = 'windows-x86.zip' else: msg = ('Unfortunately, {} is not a supported platform. Only Linux,' ' Windows, and OSX are currently supported.').format( From 2c673ea8e8d8a5ff67f38c4790fefce9dbdb28ac Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 21 Apr 2023 15:46:22 +0100 Subject: [PATCH 0009/1376] ci: skip 3.6 python arm64 failing in cirrus, passing locally with cirrus run --- .cirrus.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 6a476c228..27098bdaa 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -13,7 +13,7 @@ BUILD_TEST_TASK_TEMPLATE: &BUILD_TEST_TASK_TEMPLATE linux_arm64_task: env: matrix: - - IMAGE: python:3.6-slim + # - IMAGE: python:3.6-slim # This works locally, with cirrus run, but fails in CI - IMAGE: python:3.7-slim - IMAGE: python:3.8-slim - IMAGE: python:3.9-slim @@ -31,7 +31,7 @@ macosx_arm64_task: env: PATH: ${HOME}/.pyenv/shims:${PATH} matrix: - # - PYTHON: 3.6 # This works locally, with cirrus run, but fails in CI + - PYTHON: 3.6 - PYTHON: 3.7 - PYTHON: 3.8 - PYTHON: 3.9 From 4e3ca3858b8343a5057de506f19a4f3284bb8bd4 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Sat, 29 Apr 2023 00:43:31 +0100 Subject: [PATCH 0010/1376] feat: use pact-ruby-standalone 2.0.0 release --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c1a25af70..4d502eb6d 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ IS_64 = sys.maxsize > 2 ** 32 -PACT_STANDALONE_VERSION = '3.1.2.2-alpha' +PACT_STANDALONE_VERSION = '2.0.0' PACT_STANDALONE_SUFFIXES = ['osx-x86_64.tar.gz', 'osx-arm64.tar.gz', 'linux-x86_64.tar.gz', @@ -163,7 +163,7 @@ def download_ruby_app_binary(path_to_download_to, filename, suffix): :param filename: The filename that should be installed. :param suffix: The suffix of the standalone app to install. """ - uri = ('https://github.com/you54f/pact-ruby-standalone/releases' + uri = ('https://github.com/pact-foundation/pact-ruby-standalone/releases' '/download/v{version}/pact-{version}-{suffix}') if sys.version_info.major == 2: From 1267d7d53193be3ddbbdbe5d5b3978f8bd508dcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 May 2023 19:51:09 +0000 Subject: [PATCH 0011/1376] build(deps): bump flask from 2.2.2 to 2.2.5 in /examples/message Bumps [flask](https://github.com/pallets/flask) from 2.2.2 to 2.2.5. - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/2.2.2...2.2.5) --- updated-dependencies: - dependency-name: flask dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- examples/message/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/message/requirements.txt b/examples/message/requirements.txt index 279c15d14..021958ce1 100644 --- a/examples/message/requirements.txt +++ b/examples/message/requirements.txt @@ -1,5 +1,5 @@ Flask==2.0.3; python_version < '3.7' -Flask==2.2.2; python_version >= '3.7' +Flask==2.2.5; python_version >= '3.7' pytest==7.0.1; python_version < '3.7' pytest==7.1.3; python_version >= '3.7' requests==2.27.1; python_version < '3.7' From a0efd69362ada72695ed3104933eb2cb62c27c08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 May 2023 20:30:24 +0000 Subject: [PATCH 0012/1376] build(deps): bump flask from 2.2.2 to 2.2.5 in /examples/flask_provider Bumps [flask](https://github.com/pallets/flask) from 2.2.2 to 2.2.5. - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/2.2.2...2.2.5) --- updated-dependencies: - dependency-name: flask dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- examples/flask_provider/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flask_provider/requirements.txt b/examples/flask_provider/requirements.txt index 6764a512b..b3f83b690 100644 --- a/examples/flask_provider/requirements.txt +++ b/examples/flask_provider/requirements.txt @@ -1,5 +1,5 @@ Flask==2.0.3; python_version < '3.7' -Flask==2.2.2; python_version >= '3.7' +Flask==2.2.5; python_version >= '3.7' pytest==7.0.1; python_version < '3.7' pytest==7.1.3; python_version >= '3.7' requests==2.27.1; python_version < '3.7' From 7b14aa3b613adedbbb8ce4ac5378aa355791baeb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 May 2023 20:30:31 +0000 Subject: [PATCH 0013/1376] build(deps-dev): bump flask from 2.2.2 to 2.2.5 Bumps [flask](https://github.com/pallets/flask) from 2.2.2 to 2.2.5. - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/2.2.2...2.2.5) --- updated-dependencies: - dependency-name: flask dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index c7d62261e..e179ddbee 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -2,7 +2,7 @@ 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.2; python_version >= '3.7' +Flask==2.2.5; python_version >= '3.7' configparser==3.5.0 flake8==5.0.4 mock==3.0.5 From fc6ced8b08aed733ed2406a98bef9554a9da399a Mon Sep 17 00:00:00 2001 From: Serghei Iakovlev Date: Thu, 4 May 2023 09:51:19 +0200 Subject: [PATCH 0014/1376] style: add missing newline/linefeed Adding newline characters at the end of files to: - Comply with POSIX standards - Improve cross-platform compatibility - Optimize version control interactions --- .cirrus.yml | 4 ---- Dockerfile | 2 +- Dockerfile.ubuntu | 2 +- Makefile | 2 +- examples/README.md | 2 +- examples/consumer/run_pytest.sh | 2 +- examples/fastapi_provider/run_pytest.sh | 2 +- examples/fastapi_provider/verify_pact.sh | 2 +- examples/flask_provider/run_pytest.sh | 2 +- examples/pacts/userserviceclient-userservice.json | 2 +- run-docker.sh | 2 +- 11 files changed, 10 insertions(+), 14 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 27098bdaa..0ad7b2988 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -49,7 +49,3 @@ macosx_arm64_task: ## To install rosetta # - softwareupdate --install-rosetta --agree-to-license << : *BUILD_TEST_TASK_TEMPLATE - - - - diff --git a/Dockerfile b/Dockerfile index a048a2b3a..6761bd859 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,4 +14,4 @@ RUN python -m flake8 RUN python -m pydocstyle pact RUN python -m tox -e test -CMD ["sh","-c","python -m tox -e test"] \ No newline at end of file +CMD ["sh","-c","python -m tox -e test"] diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index aaf6224b9..74efa1c69 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -27,4 +27,4 @@ RUN python -m flake8 RUN python -m pydocstyle pact RUN python -m tox -e test -CMD ["sh","-c","python -m tox -e test"] \ No newline at end of file +CMD ["sh","-c","python -m tox -e test"] diff --git a/Makefile b/Makefile index c12e80dc6..10f39ede5 100644 --- a/Makefile +++ b/Makefile @@ -135,4 +135,4 @@ venv: ln -sf ${CURDIR}/.venv ~/.pyenv/versions/${PROJECT} @echo "\n$(green)Use it! (populate .python-version)$(sgr0)" - pyenv local ${PROJECT} \ No newline at end of file + pyenv local ${PROJECT} diff --git a/examples/README.md b/examples/README.md index 7c1bce7cf..885cdc6dc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -217,4 +217,4 @@ without a [Pact Broker]. [Virtual Environment]: https://docs.python.org/3/tutorial/venv.html [Sharing Pacts]: https://docs.pact.io/getting_started/sharing_pacts/] [How to Run a Flask Application]: https://www.twilio.com/blog/how-run-flask-application -[Requiring/Loading plugins in a test module or conftest file]: https://docs.pytest.org/en/6.2.x/writing_plugins.html#requiring-loading-plugins-in-a-test-module-or-conftest-file \ No newline at end of file +[Requiring/Loading plugins in a test module or conftest file]: https://docs.pytest.org/en/6.2.x/writing_plugins.html#requiring-loading-plugins-in-a-test-module-or-conftest-file diff --git a/examples/consumer/run_pytest.sh b/examples/consumer/run_pytest.sh index 7494398bb..f997e1583 100755 --- a/examples/consumer/run_pytest.sh +++ b/examples/consumer/run_pytest.sh @@ -1,4 +1,4 @@ #!/bin/bash set -o pipefail -pytest tests --run-broker True --publish-pact 1 \ No newline at end of file +pytest tests --run-broker True --publish-pact 1 diff --git a/examples/fastapi_provider/run_pytest.sh b/examples/fastapi_provider/run_pytest.sh index ff579b469..447e0c4b3 100755 --- a/examples/fastapi_provider/run_pytest.sh +++ b/examples/fastapi_provider/run_pytest.sh @@ -3,4 +3,4 @@ set -o pipefail # Unlike in the Flask example, here the FastAPI service is started up as a pytest fixture. This is then including the # main and pact routes via fastapi_provider.py to run the tests against -pytest --run-broker True --publish-pact 1 \ No newline at end of file +pytest --run-broker True --publish-pact 1\ diff --git a/examples/fastapi_provider/verify_pact.sh b/examples/fastapi_provider/verify_pact.sh index df0ec34a6..356b9f739 100755 --- a/examples/fastapi_provider/verify_pact.sh +++ b/examples/fastapi_provider/verify_pact.sh @@ -34,4 +34,4 @@ else --pact-broker-password pactbroker \ --publish-verification-results \ --provider-states-setup-url=http://localhost:8000/_pact/provider_states -fi \ No newline at end of file +fi diff --git a/examples/flask_provider/run_pytest.sh b/examples/flask_provider/run_pytest.sh index ca022085f..37aa783a0 100755 --- a/examples/flask_provider/run_pytest.sh +++ b/examples/flask_provider/run_pytest.sh @@ -17,4 +17,4 @@ trap teardown EXIT sleep 1 # Now run the tests -pytest tests --run-broker True --publish-pact 1 \ No newline at end of file +pytest tests --run-broker True --publish-pact 1 diff --git a/examples/pacts/userserviceclient-userservice.json b/examples/pacts/userserviceclient-userservice.json index 5453494e2..d3260f688 100644 --- a/examples/pacts/userserviceclient-userservice.json +++ b/examples/pacts/userserviceclient-userservice.json @@ -62,4 +62,4 @@ "version": "2.0.0" } } -} \ No newline at end of file +} diff --git a/run-docker.sh b/run-docker.sh index ef98a520b..9bbf49a05 100755 --- a/run-docker.sh +++ b/run-docker.sh @@ -6,4 +6,4 @@ for arch in arm64 amd64; do docker build -t python-$arch-$version --build-arg PYTHON_VERSION=$version --platform=linux/$arch . docker run -it --rm python-$arch-$version done -done \ No newline at end of file +done From c70573c5aefa7bdd341a6d8e6fe6dd4be5ab490b Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Wed, 3 May 2023 19:17:28 +0100 Subject: [PATCH 0015/1376] chore(docs): update provider verifier options table - Table was mismatched, cli args were the python args and some missing - updated links to examples --- README.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 98c814d5e..1ce4e5b9b 100644 --- a/README.md +++ b/README.md @@ -485,13 +485,26 @@ assert success == 0 ``` The parameters for this differ slightly in naming from their CLI equivalents: -| CLI | native Python | +| CLI | native Python | notes | |-----------------|-------------------------------------------------------------------------------------------------| -| `consumer_tags` | `consumer-version-tag` | -| `provider_tags` | `provider-version-tag` | -| `custom-provider-header` | `headers` | - -You can see more details in the [e2e examples](https://github.com/pact-foundation/pact-python/tree/master/examples/e2e/tests/provider/test_provider.py). +| `--log-dir` | `log_dir` || +| `--log-level` | `log_level` || +| `--provider-app-version` | `provider_app_version` || +| `--headers` | `custom_provider_headers` || +| `--consumer-version-tag` | `consumer_tags` || +| `--provider-version-tag` | `provider_tags` || +| `--provider-states-setup-url` | `provider_states_setup_url` || +| `--verbose` | `verbose` || +| `--consumer-version-selector` | `consumer_selectors` | takes an untyped dict of consumer version selectors and converts to json | +| `--publish-verification-results` | `publish_verification_results` | recommended only to set in CI | +| `--provider-version-branch` | `provider_version_branch` | recommended to set | + + +You can see more details in the examples + +- [`examples/message/tests/provider/test_message_provider.py`](`examples/message/tests/provider/test_message_provider.py`) +- [`examples/flask_provider/tests/provider/test_provider.py`](`examples/flask_provider/tests/provider/test_provider.py`) +- [`examples/fastapi_provider/tests/provider/test_provider.py`](`examples/fastapi_provider/tests/provider/test_provider.py`) ### Provider States In many cases, your contracts will need very specific data to exist on the provider From 80f06cfe4a7c7e382dc85b218ac613c8c608d080 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Wed, 3 May 2023 19:20:37 +0100 Subject: [PATCH 0016/1376] chore(docs): correct table --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1ce4e5b9b..359b50343 100644 --- a/README.md +++ b/README.md @@ -486,7 +486,7 @@ assert success == 0 The parameters for this differ slightly in naming from their CLI equivalents: | CLI | native Python | notes | -|-----------------|-------------------------------------------------------------------------------------------------| +|-----------------|-------------------------------------------------------------------------------------------------|----| | `--log-dir` | `log_dir` || | `--log-level` | `log_level` || | `--provider-app-version` | `provider_app_version` || @@ -502,9 +502,9 @@ The parameters for this differ slightly in naming from their CLI equivalents: You can see more details in the examples -- [`examples/message/tests/provider/test_message_provider.py`](`examples/message/tests/provider/test_message_provider.py`) -- [`examples/flask_provider/tests/provider/test_provider.py`](`examples/flask_provider/tests/provider/test_provider.py`) -- [`examples/fastapi_provider/tests/provider/test_provider.py`](`examples/fastapi_provider/tests/provider/test_provider.py`) +- [`examples/message/tests/provider/test_message_provider.py`](examples/message/tests/provider/test_message_provider.py) +- [`examples/flask_provider/tests/provider/test_provider.py`](examples/flask_provider/tests/provider/test_provider.py) +- [`examples/fastapi_provider/tests/provider/test_provider.py`](examples/fastapi_provider/tests/provider/test_provider.py) ### Provider States In many cases, your contracts will need very specific data to exist on the provider From 9bc3e21ae217804f270e98307ba13957183d68c0 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Thu, 4 May 2023 12:10:39 +0100 Subject: [PATCH 0017/1376] chore(docs): improve table alignment and abs links --- README.md | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 359b50343..230d6fd9d 100644 --- a/README.md +++ b/README.md @@ -329,16 +329,16 @@ output, logs = verifier.verify_pacts('./userserviceclient-userservice.json') ``` The parameters for this differ slightly in naming from their CLI equivalents: -| CLI | native Python | -|-----------------|-------------------------------------------------------------------------------------------------| -| `--branch` | `branch` | -| `--build-url` | `build_url` | -| `--auto-detect-version-properties` | `auto_detect_version_properties` | -| `--tag=TAG` | `consumer_tags` | -| `--tag-with-git-branch` | `tag_with_git_branch` | -| `PACT_DIRS_OR_FILES` | `pact_dir` | -| `--consumer-app-version` | `version` | -| `n/a` | `consumer_name` | +| CLI | native Python | +|-----------------------------------|-----------------------------------| +| `--branch` | `branch` | +| `--build-url` | `build_url` | +| `--auto-detect-version-properties`| `auto_detect_version_properties` | +| `--tag=TAG` | `consumer_tags` | +| `--tag-with-git-branch` | `tag_with_git_branch` | +| `PACT_DIRS_OR_FILES` | `pact_dir` | +| `--consumer-app-version` | `version` | +| `n/a` | `consumer_name` | ## Verifying Pacts Against a Service @@ -485,26 +485,26 @@ assert success == 0 ``` The parameters for this differ slightly in naming from their CLI equivalents: -| CLI | native Python | notes | -|-----------------|-------------------------------------------------------------------------------------------------|----| -| `--log-dir` | `log_dir` || -| `--log-level` | `log_level` || -| `--provider-app-version` | `provider_app_version` || -| `--headers` | `custom_provider_headers` || -| `--consumer-version-tag` | `consumer_tags` || -| `--provider-version-tag` | `provider_tags` || -| `--provider-states-setup-url` | `provider_states_setup_url` || -| `--verbose` | `verbose` || -| `--consumer-version-selector` | `consumer_selectors` | takes an untyped dict of consumer version selectors and converts to json | -| `--publish-verification-results` | `publish_verification_results` | recommended only to set in CI | -| `--provider-version-branch` | `provider_version_branch` | recommended to set | +| CLI | native Python | +|-----------------------------------|------------------------------- | +| `--log-dir` | `log_dir` | +| `--log-level` | `log_level` | +| `--provider-app-version` | `provider_app_version` | +| `--headers` | `custom_provider_headers` | +| `--consumer-version-tag` | `consumer_tags` | +| `--provider-version-tag` | `provider_tags` | +| `--provider-states-setup-url` | `provider_states_setup_url` | +| `--verbose` | `verbose` | +| `--consumer-version-selector` | `consumer_selectors` | +| `--publish-verification-results` | `publish_verification_results` | +| `--provider-version-branch` | `provider_version_branch` | You can see more details in the examples -- [`examples/message/tests/provider/test_message_provider.py`](examples/message/tests/provider/test_message_provider.py) -- [`examples/flask_provider/tests/provider/test_provider.py`](examples/flask_provider/tests/provider/test_provider.py) -- [`examples/fastapi_provider/tests/provider/test_provider.py`](examples/fastapi_provider/tests/provider/test_provider.py) +- [Message Provider Verifier Test](https://github.com/pact-foundation/pact-python/tree/master/examples/message/tests/provider/test_message_provider.py) +- [Flask Provider Verifier Test](https://github.com/pact-foundation/pact-python/tree/master/examples/flask_provider/tests/provider/test_provider.py) +- [FastAPI Provider Verifier Test](https://github.com/pact-foundation/pact-python/tree/master/examples/fastapi_provider/tests/provider/test_provider.py) ### Provider States In many cases, your contracts will need very specific data to exist on the provider From 819f0a77d03d3c038c34fbd37e0a62165cb3bbe7 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Thu, 18 May 2023 23:30:30 +0100 Subject: [PATCH 0018/1376] test: v2.0.1 (pact-2.0.1) - pact-ruby-standalone --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4d502eb6d..7d225083a 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ IS_64 = sys.maxsize > 2 ** 32 -PACT_STANDALONE_VERSION = '2.0.0' +PACT_STANDALONE_VERSION = '2.0.1' PACT_STANDALONE_SUFFIXES = ['osx-x86_64.tar.gz', 'osx-arm64.tar.gz', 'linux-x86_64.tar.gz', From 2a244ea550b0dcf75721c74a228060d0fa802ddd Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Sat, 8 Jul 2023 14:12:18 +0100 Subject: [PATCH 0019/1376] chore: update to 2.0.2 pact-ruby-standalone --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7d225083a..4af647300 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ IS_64 = sys.maxsize > 2 ** 32 -PACT_STANDALONE_VERSION = '2.0.1' +PACT_STANDALONE_VERSION = '2.0.2' PACT_STANDALONE_SUFFIXES = ['osx-x86_64.tar.gz', 'osx-arm64.tar.gz', 'linux-x86_64.tar.gz', From 00dcacd8ea49e8e90df3005d8883356286d061ae Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Mon, 10 Jul 2023 13:54:35 +0100 Subject: [PATCH 0020/1376] chore: Releasing version 2.0.0 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ pact/__version__.py | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbefd72a4..e1ecd74b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,37 @@ +### 2.0.0 + * 2a244ea - chore: update to 2.0.2 pact-ruby-standalone (Yousaf Nabi, Sat Jul 8 14:12:18 2023 +0100) + * 819f0a7 - test: v2.0.1 (pact-2.0.1) - pact-ruby-standalone (Yousaf Nabi, Thu May 18 23:30:30 2023 +0100) + * 9bc3e21 - chore(docs): improve table alignment and abs links (Yousaf Nabi, Thu May 4 12:10:39 2023 +0100) + * 80f06cf - chore(docs): correct table (Yousaf Nabi, Wed May 3 19:20:37 2023 +0100) + * c70573c - chore(docs): update provider verifier options table (Yousaf Nabi, Wed May 3 19:17:28 2023 +0100) + * fc6ced8 - style: add missing newline/linefeed (Serghei Iakovlev, Thu May 4 09:51:19 2023 +0200) + * 7b14aa3 - build(deps-dev): bump flask from 2.2.2 to 2.2.5 (dependabot[bot], Wed May 3 20:30:31 2023 +0000) + * a0efd69 - build(deps): bump flask from 2.2.2 to 2.2.5 in /examples/flask_provider (dependabot[bot], Wed May 3 20:30:24 2023 +0000) + * 1267d7d - build(deps): bump flask from 2.2.2 to 2.2.5 in /examples/message (dependabot[bot], Wed May 3 19:51:09 2023 +0000) + * 4e3ca38 - feat: use pact-ruby-standalone 2.0.0 release (Yousaf Nabi, Sat Apr 29 00:43:31 2023 +0100) + * 2c673ea - ci: skip 3.6 python arm64 failing in cirrus, passing locally with cirrus run (Yousaf Nabi, Fri Apr 21 15:46:22 2023 +0100) + * 7aff538 - feat: support x86 and x86_64 windows (Yousaf Nabi, Fri Apr 21 15:44:48 2023 +0100) + * 28440da - ci: test arm64 on cirrus-ci / test win/osx on gh (Yousaf Nabi, Fri Apr 21 15:37:30 2023 +0100) + * 93db8ae - feat: support arm64 osx/linux (Yousaf Nabi, Fri Apr 21 12:35:23 2023 +0100) + * 19be499 - fix: fix cors parameter not doing anything (Lukas Riedersberger, Fri Apr 14 12:22:21 2023 +0200) + * e721d81 - docs: reformat releasing documentation (Serghei Iakovlev, Wed Apr 5 12:39:35 2023 +0200) + * 71f1529 - chore: do not add merge commits to the change log (Serghei Iakovlev, Wed Apr 5 12:27:49 2023 +0200) + * 9ce2d69 - chore: Releasing version 1.7.0 (Elliott Murray, Sun Feb 19 11:28:01 2023 +0000) + * 429e171 - build: use a single Dockerfile, providing args for the Python version instead of multiple files (Mike Geeves, Mon Apr 3 09:01:37 2023 +0100) + * e99e7fb - docs: rephrase the instructions for running the tests (Serghei Iakovlev, Sun Apr 2 22:20:07 2023 +0200) + * a5d3a2e - docs: paraphrase the instructions for running the tests (Serghei Iakovlev, Sun Apr 2 22:19:37 2023 +0200) + * 24c2dbf - docs: fix instruction to build python 3.11 image (Serghei Iakovlev, Sun Apr 2 22:18:10 2023 +0200) + * 55dcaf2 - feat(test): add docker images for Python 3.9-3.11 for testing purposes (Serghei Iakovlev, Fri Mar 17 11:24:42 2023 +0100) + * 28fc7d3 - docs: fix link for GitHub badge (Serghei Iakovlev, Fri Mar 31 22:50:23 2023 +0200) + * 26eaaac - fix: remove dead code (Serghei Iakovlev, Sun Mar 5 02:05:14 2023 +0100) + * f7c5006 - docs: add Python 3.11 to CONTRIBUTING.md (Serghei Iakovlev, Thu Mar 30 23:22:22 2023 +0200) + * 348bf5e - build: use compatible dependency versions for Python 3.6 (Serghei Iakovlev, Thu Mar 30 23:18:57 2023 +0200) + * 4d9f4cd - feat: describe classifiers and python version for pypi package (Serghei Iakovlev, Sun Mar 5 09:16:29 2023 +0100) + * 7603815 - ci: add python 3.11 to test matrix (Serghei Iakovlev, Sun Mar 5 09:15:23 2023 +0100) + * bea1563 - doc: improve commit messages guide (Serghei Iakovlev, Sat Mar 4 00:30:56 2023 +0100) + * 60f2aac - doc: correct links in contributing manual (Serghei Iakovlev, Fri Mar 3 21:38:58 2023 +0100) + * a219f49 - fix: actualize doc on how to make contributions (Serghei Iakovlev, Thu Mar 2 08:56:48 2023 +0100) + * 4919772 - feat: add matchers for ISO 8601 date format (Serghei Iakovlev, Sun Mar 12 16:03:44 2023 +0100) ### 1.7.0 * 44cda33 - chore: /s/Pactflow/PactFlow (Yousaf Nabi, Thu Jan 26 16:11:54 2023 +0000) * 1bbdd37 - feat: Enhance provider states for pact-message (#322) (nsfrias, Tue Jan 24 17:04:29 2023 +0000) diff --git a/pact/__version__.py b/pact/__version__.py index 86d36b05e..348505fb4 100644 --- a/pact/__version__.py +++ b/pact/__version__.py @@ -1,3 +1,3 @@ """Pact version info.""" -__version__ = '1.7.0' +__version__ = '2.0.0' From 1429d2fadeb90dd0eb348d19fbeccb18f98504af Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Tue, 25 Jul 2023 13:56:08 +0100 Subject: [PATCH 0021/1376] chore: update MANIFEST file to note 2.0.2 standalone --- MANIFEST | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/MANIFEST b/MANIFEST index 5793cf027..39fb6aa9f 100644 --- a/MANIFEST +++ b/MANIFEST @@ -21,10 +21,11 @@ pact/pact.py pact/provider.py pact/verifier.py pact/verify_wrapper.py -pact/bin/pact-3.1.2.2-alpha-linux-arm64.tar.gz -pact/bin/pact-3.1.2.2-alpha-linux-x86_64.tar.gz -pact/bin/pact-3.1.2.2-alpha-osx-arm64.tar.gz -pact/bin/pact-3.1.2.2-alpha-osx-x86_64.tar.gz -pact/bin/pact-3.1.2.2-alpha-windows-x86_64.zip +pact/bin/pact-2.0.2-linux-arm64.tar.gz +pact/bin/pact-2.0.2-linux-x86_64.tar.gz +pact/bin/pact-2.0.2-osx-arm64.tar.gz +pact/bin/pact-2.0.2-osx-x86_64.tar.gz +pact/bin/pact-2.0.2-windows-x86.zip +pact/bin/pact-2.0.2-windows-x86_64.zip pact/cli/__init__.py pact/cli/verify.py From ef12e5608ead4d9fd710f911ca5f0818ee928c96 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Tue, 25 Jul 2023 14:00:38 +0100 Subject: [PATCH 0022/1376] feat: update standalone to 2.0.3 --- MANIFEST | 12 ++++++------ setup.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/MANIFEST b/MANIFEST index 39fb6aa9f..c8f174abe 100644 --- a/MANIFEST +++ b/MANIFEST @@ -21,11 +21,11 @@ pact/pact.py pact/provider.py pact/verifier.py pact/verify_wrapper.py -pact/bin/pact-2.0.2-linux-arm64.tar.gz -pact/bin/pact-2.0.2-linux-x86_64.tar.gz -pact/bin/pact-2.0.2-osx-arm64.tar.gz -pact/bin/pact-2.0.2-osx-x86_64.tar.gz -pact/bin/pact-2.0.2-windows-x86.zip -pact/bin/pact-2.0.2-windows-x86_64.zip +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/setup.py b/setup.py index 4af647300..2269af5f5 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ IS_64 = sys.maxsize > 2 ** 32 -PACT_STANDALONE_VERSION = '2.0.2' +PACT_STANDALONE_VERSION = '2.0.3' PACT_STANDALONE_SUFFIXES = ['osx-x86_64.tar.gz', 'osx-arm64.tar.gz', 'linux-x86_64.tar.gz', From d3397b777ea467b71649da6407ec7988e542eaee Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Tue, 25 Jul 2023 14:55:42 +0100 Subject: [PATCH 0023/1376] chore(examples): update docker setup for non linux os --- examples/broker/docker-compose.yml | 6 +++++- examples/common/sharedfixtures.py | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/broker/docker-compose.yml b/examples/broker/docker-compose.yml index b5d5316cd..11585e298 100644 --- a/examples/broker/docker-compose.yml +++ b/examples/broker/docker-compose.yml @@ -20,9 +20,11 @@ services: # # As well as changing the image, the destination port will need to be changed # from 9292 below, and in the nginx.conf proxy_pass section - image: pactfoundation/pact-broker + image: pactfoundation/pact-broker:latest-multi ports: - "80:9292" + depends_on: + - postgres links: - postgres environment: @@ -32,6 +34,7 @@ services: PACT_BROKER_DATABASE_NAME: postgres PACT_BROKER_BASIC_AUTH_USERNAME: pactbroker PACT_BROKER_BASIC_AUTH_PASSWORD: pactbroker + PACT_BROKER_DATABASE_CONNECT_MAX_RETRIES: "5" # The Pact Broker provides a healthcheck endpoint which we will use to wait # for it to become available before starting up healthcheck: @@ -55,3 +58,4 @@ services: depends_on: broker_app: condition: service_healthy + \ No newline at end of file diff --git a/examples/common/sharedfixtures.py b/examples/common/sharedfixtures.py index b430342f6..4cdece923 100644 --- a/examples/common/sharedfixtures.py +++ b/examples/common/sharedfixtures.py @@ -1,3 +1,4 @@ +import platform import pathlib import docker @@ -65,6 +66,11 @@ def publish_existing_pact(broker): "PACT_BROKER_PASSWORD": "pactbroker", } + target_platform = platform.platform().lower() + + if 'macos' in target_platform or 'windows' in target_platform: + envs["PACT_BROKER_BASE_URL"] = "http://host.docker.internal:80" + client = docker.from_env() print("Publishing existing Pact") @@ -72,7 +78,7 @@ def publish_existing_pact(broker): remove=True, network="broker_default", volumes=pacts, - image="pactfoundation/pact-cli:latest", + image="pactfoundation/pact-cli:latest-multi", environment=envs, command="publish /pacts --consumer-app-version 1", ) From 03df5c50ca7c26fe4afe013583b9b1036f5ed796 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Wed, 26 Jul 2023 15:04:16 +0100 Subject: [PATCH 0024/1376] chore: Releasing version 2.0.1 --- CHANGELOG.md | 4 ++++ pact/__version__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1ecd74b1..62aa65a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 2.0.1 + * d3397b7 - chore(examples): update docker setup for non linux os (Yousaf Nabi, Tue Jul 25 14:55:42 2023 +0100) + * ef12e56 - feat: update standalone to 2.0.3 (Yousaf Nabi, Tue Jul 25 14:00:38 2023 +0100) + * 1429d2f - chore: update MANIFEST file to note 2.0.2 standalone (Yousaf Nabi, Tue Jul 25 13:56:08 2023 +0100) ### 2.0.0 * 2a244ea - chore: update to 2.0.2 pact-ruby-standalone (Yousaf Nabi, Sat Jul 8 14:12:18 2023 +0100) * 819f0a7 - test: v2.0.1 (pact-2.0.1) - pact-ruby-standalone (Yousaf Nabi, Thu May 18 23:30:30 2023 +0100) diff --git a/pact/__version__.py b/pact/__version__.py index 348505fb4..a9c04f731 100644 --- a/pact/__version__.py +++ b/pact/__version__.py @@ -1,3 +1,3 @@ """Pact version info.""" -__version__ = '2.0.0' +__version__ = '2.0.1' From ed5f86c95ae0856338f0fd244c06399ce395f353 Mon Sep 17 00:00:00 2001 From: Matt Fellows Date: Fri, 4 Aug 2023 16:37:05 +1000 Subject: [PATCH 0025/1376] chore: add pact-foundation triage automation --- .github/workflows/triage.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/triage.yml diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml new file mode 100644 index 000000000..eb5ec3054 --- /dev/null +++ b/.github/workflows/triage.yml @@ -0,0 +1,15 @@ +name: Triage Issue + +on: + issues: + types: + - opened + - labeled + pull_request: + types: + - labeled + +jobs: + call-workflow: + uses: pact-foundation/.github/.github/workflows/triage.yml@master + secrets: inherit From d5017f879ab5e8db7ca7104909d7cf1bdd0954f4 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 14 Sep 2023 11:22:58 +1000 Subject: [PATCH 0026/1376] style: add pre-commit hooks and editorconfig Without wanting to overload the pre-commit hooks, this adds a few checks on the commits to ensure validity of files. More time-consuming checks for linting and formatting are done pre-push to avoid impacting the developer experience during local development. These are covered by `prettier` (for markdown, yaml, json, ...), `black` for Python and `ruff` for Python linting. To help standardise formatting in the future, an `.editorconfig` file has also been added to the repository. By virtue of using `pre-commit`, only modified files are checked which ensures older files are incrementally updated. Signed-off-by: JP-Ellis --- .editorconfig | 22 ++++++++++++++++++ .pre-commit-config.yaml | 51 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..acd41cef1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.py] +indent_size = 4 + +[Makefile] +indent_size = 4 +indent_style = tab + +[*.md] +indent_size = 4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ff4f8937..3ddd02d1c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,55 @@ +default_install_hook_types: + - commit-msg + - pre-commit + - pre-push + repos: + # Generic hooks that apply to a lot of files + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: destroyed-symlinks + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + + # The following only check that the files are parseable and does _not_ + # modify the formatting. + - id: check-toml + - id: check-xml + - id: check-yaml + + - repo: https://gitlab.com/bmares/check-json5 + rev: v1.0.0 + hooks: + # As above, this only checks for valid JSON files. This implementation + # allows for comments within JSON files. + - id: check-json5 + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.0.3 + hooks: + - id: prettier + stages: [pre-push] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.289 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + stages: [pre-push] + + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + stages: [pre-push] + - repo: https://github.com/commitizen-tools/commitizen rev: master hooks: From 04deeec1eb056b29c9626606f40fedcccaafe37d Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 13 Sep 2023 10:57:51 +1000 Subject: [PATCH 0027/1376] chore: update pre-commit config Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ddd02d1c..68f0845b0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,7 +51,7 @@ repos: stages: [pre-push] - repo: https://github.com/commitizen-tools/commitizen - rev: master + rev: 3.8.2 hooks: - id: commitizen stages: [commit-msg] From 5b1966552a918f6a513855714b177159b9d8a198 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 12 Sep 2023 16:13:24 +1000 Subject: [PATCH 0028/1376] chore!: migrate to pyproject.toml and hatch The previous build process relied on `distutils` which is due to be deprecated in 3.12. Furthermore, the use of `setup.py` is now discouraged. The choice to migrate to `hatch` was made for the following reasons: - It offers a very simple management of the venv. No more need to `python -m venv .venv` and `pip install`, `hatch` handles all of that automatically when creating the virtual environment. - `hatch` supercedes `tox`, allowing for multiple python versions to be tested in a single command. - `hatch` manages the build process, and offers a nicer way to hook in a custom build process to download the `pact` standalone binaries. A minor change to the packaging of the library now places the binaries in `pact/bin` instead of `pact/bin/pact/bin`. The `constants.py` file has been accordingly updated to reflect this change in case anyone was making direct use of the binaries. While this change is rather significant, it should not affect the end user experience. Users will still be able to `pip install pact-python` from PyPI. Other than for the aforementioned, there has been no changes to the library code. Official support to Python 3.6 and 3.7 is dropped as part of this change as security fixes for these versions are no longer provided (ended 21 months ago for 3.6, and 3 months ago for 3.7). Furthermore, a number of dependencies have dropped support for these versions, and pinning historical versions of these dependencies is introducing known security vulnerabilities. BREAKING CHANGE: Drop support for Python 3.6 and 3.7 Resolves: #369 Refs: https://docs.python.org/3/whatsnew/3.10.html#distutils-deprecated Refs: https://setuptools.pypa.io/en/latest/userguide/quickstart.html#setuppy-discouraged Signed-off-by: JP-Ellis --- .gitignore | 3 +- MANIFEST | 31 ----- MANIFEST.in | 6 - Makefile | 46 ++----- hatch_build.py | 147 ++++++++++++++++++++++ pact/constants.py | 54 ++++---- pyproject.toml | 157 +++++++++++++++++++++++ requirements_dev.txt | 26 ---- setup.cfg | 24 ---- setup.py | 294 ------------------------------------------- tox.ini | 8 -- 11 files changed, 335 insertions(+), 461 deletions(-) delete mode 100644 MANIFEST delete mode 100644 MANIFEST.in create mode 100644 hatch_build.py create mode 100644 pyproject.toml delete mode 100644 requirements_dev.txt delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index ba0d76014..1dfef317e 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__/ @@ -104,4 +106,3 @@ ENV/ .noseids - diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index c8f174abe..000000000 --- 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 160e1d2d4..000000000 --- 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 10f39ede5..cdf2c9100 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 +test: + hatch run all + hatch run test:all 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} diff --git a/hatch_build.py b/hatch_build.py new file mode 100644 index 000000000..940dba7c1 --- /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", "win32")): + 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 9c6f7b117..225d0a528 100644 --- a/pact/constants.py +++ b/pact/constants.py @@ -1,48 +1,38 @@ """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' + if os.name == "nt": + return "pact-provider-verifier.bat" + 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())) +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 000000000..30e2533b8 --- /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 e179ddbee..000000000 --- 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 8f6688010..000000000 --- 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 2269af5f5..000000000 --- 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 2cfa5fdeb..000000000 --- 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" From 093d9b85c0c6bfc81be60e990e6c17ca103977fc Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 13 Sep 2023 13:21:36 +1000 Subject: [PATCH 0029/1376] chore(ci): migrate cicd to hatch With Hatch as the new build system, the previous GitHub actions no longer work to lint, test, and publish the package. This commit makes use of pypa/cibuildwheel to build the package for multiple platforms, and then publishes the package to PyPI using the existing secrets. Signed-off-by: JP-Ellis --- .cirrus.yml | 58 +++--- .github/workflows/build.yml | 171 ++++++++++++++++++ .github/workflows/build_and_test.yml | 54 ------ .../workflows/package_and_push_to_pypi.yml | 27 --- .github/workflows/test.yml | 61 +++++++ 5 files changed, 260 insertions(+), 111 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/build_and_test.yml delete mode 100644 .github/workflows/package_and_push_to_pypi.yml create mode 100644 .github/workflows/test.yml diff --git a/.cirrus.yml b/.cirrus.yml index 0ad7b2988..b8c4d9331 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,51 +1,49 @@ -BUILD_TEST_TASK_TEMPLATE: &BUILD_TEST_TASK_TEMPLATE +TEST_TEMPLATE: &TEST_TEMPLATE arch_check_script: - uname -am test_script: - python --version - - python -m pip install --upgrade pip - - python -m pip install -r requirements_dev.txt - - python -m flake8 - - python -m pydocstyle pact - - python -m tox -e test - # - make examples + # TODO: Fix lints before enabling + - echo hatch run lint + # TODO: Implement the examples to work in hatch + - echo hatch run example + - hatch run test -linux_arm64_task: +linux_arm64_task: env: + PATH: ${HOME}/.local/bin:${PATH} matrix: - # - IMAGE: python:3.6-slim # This works locally, with cirrus run, but fails in CI - - IMAGE: python:3.7-slim - - IMAGE: python:3.8-slim - - IMAGE: python:3.9-slim - - IMAGE: python:3.10-slim + - IMAGE: "python:3.8-slim" + - IMAGE: "python:3.9-slim" + - IMAGE: "python:3.10-slim" + - IMAGE: "python:3.11-slim" arm_container: image: $IMAGE install_script: - - apt update --yes && apt install --yes gcc make - << : *BUILD_TEST_TASK_TEMPLATE - + - apt update --yes + - apt install --yes gcc make + - python -m pip install --upgrade pip pipx + - pipx install hatch + <<: *TEST_TEMPLATE macosx_arm64_task: macos_instance: image: ghcr.io/cirruslabs/macos-ventura-base:latest env: - PATH: ${HOME}/.pyenv/shims:${PATH} + PATH: ${HOME}/.local/bin:${HOME}/.pyenv/shims:${PATH} matrix: - - PYTHON: 3.6 - - PYTHON: 3.7 - - PYTHON: 3.8 - - PYTHON: 3.9 - - PYTHON: 3.10 + - PYTHON: "3.8" + - PYTHON: "3.9" + - PYTHON: "3.10" + - PYTHON: "3.11" install_script: - # Per the pyenv homebrew recommendations. - # https://github.com/pyenv/pyenv/wiki#suggested-build-environment - # - xcode-select --install # Unnecessary on Cirrus - - brew update - # - brew install openssl readline sqlite3 xz zlib + - brew update - brew install pyenv - pyenv install ${PYTHON} - pyenv global ${PYTHON} - pyenv rehash - ## To install rosetta - # - softwareupdate --install-rosetta --agree-to-license - << : *BUILD_TEST_TASK_TEMPLATE + - python -m pip install --upgrade pip pipx + - pyenv rehash + - pipx install hatch + - pyenv rehash + <<: *TEST_TEMPLATE diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..054dfb29e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,171 @@ +name: build + +on: + push: + tags: + - v* + branches: + - master + pull_request: + branches: + - master + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true + +env: + STABLE_PYTHON_VERSION: "3.11" + CIBW_BUILD_FRONTEND: build + CIBW_TEST_COMMAND: > + python -c + "from pact import EachLike; + assert EachLike(1).generate() == {'json_class': 'Pact::ArrayLike', 'contents': 1, 'min': 1} + " + +jobs: + build-x86_64: + name: Build wheels on ${{ matrix.os }} (x86, 64-bit) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + archs: x86_64 + - os: macos-latest + archs: x86_64 + - os: windows-latest + archs: AMD64 + + steps: + - uses: actions/checkout@v4 + with: + # Fetch all tags + fetch-depth: 0 + + - name: Create wheels + uses: pypa/cibuildwheel@v2.15.0 + env: + CIBW_ARCHS: ${{ matrix.archs }} + + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: ./wheelhouse/*.whl + if-no-files-found: error + + build-x86: + name: Build wheels on ${{ matrix.os }} (x86, 32-bit) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + archs: x86 + + steps: + - uses: actions/checkout@v4 + with: + # Fetch all tags + fetch-depth: 0 + + - name: Create wheels + uses: pypa/cibuildwheel@v2.15.0 + env: + CIBW_ARCHS: ${{ matrix.archs }} + + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: ./wheelhouse/*.whl + if-no-files-found: error + + build-arm64: + name: Build wheels on ${{ matrix.os }} (arm64) + runs-on: ${{ matrix.os }} + # As this requires emulation, it's not worth running on PRs + if: >- + github.event_name == 'push' && + startsWith(github.event.ref, 'refs/tags') + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + archs: aarch64 + build: "*manylinux*" + - os: macos-latest + archs: arm64 + build: "*" + + steps: + - uses: actions/checkout@v4 + with: + # Fetch all tags + fetch-depth: 0 + + - name: Set up QEMU + if: matrix.os == 'ubuntu-latest' + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Create wheels + uses: pypa/cibuildwheel@v2.15.0 + env: + CIBW_ARCHS: ${{ matrix.archs }} + CIBW_BUILD: ${{ matrix.build }} + + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: ./wheelhouse/*.whl + if-no-files-found: error + + check: + name: Check wheels + needs: + - build-x86_64 + - build-x86 + - build-arm64 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.STABLE_PYTHON_VERSION }} + + - uses: actions/download-artifact@v3 + with: + name: wheels + path: wheelhouse + + - run: | + pipx run twine check --strict wheelhouse/* + + publish: + name: Publish wheels + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + needs: [check] + runs-on: ubuntu-latest + + steps: + - uses: actions/download-artifact@v3 + with: + name: artifacts + path: wheelhouse + + - name: Push build artifacts to PyPI + uses: pypa/gh-action-pypi-publish@v1.8.10 + with: + skip_existing: true + user: ${{ secrets.PYPI_USERNAME }} + password: ${{ secrets.PYPI_PASSWORD }} + packages-dir: wheelhouse diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml deleted file mode 100644 index 26e8651ed..000000000 --- a/.github/workflows/build_and_test.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Build and Test - -on: [push, pull_request] - -jobs: - build: - name: Python ${{ matrix.python-version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - - strategy: - # When set to true, GitHub cancels - # all in-progress jobs if any matrix job fails. - fail-fast: false - - matrix: - python-version: - - '3.7' - - '3.8' - - '3.9' - - '3.10' - - '3.11' - os: [ ubuntu-latest, windows-latest, macos-latest ] - - # These versions are no longer supported by Python team, and may - # eventually be dropped from GitHub Actions. - include: - - python-version: '3.6' - os: ubuntu-20.04 - - steps: - - name: Check out code - uses: actions/checkout@v3 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -r requirements_dev.txt - - - name: Lint with flake8, pydocstyle - run: | - flake8 - pydocstyle pact - - - name: Test with pytest - run: tox -e test - - - name: Test examples - if: runner.os == 'Linux' - run: make examples diff --git a/.github/workflows/package_and_push_to_pypi.yml b/.github/workflows/package_and_push_to_pypi.yml deleted file mode 100644 index 4336ce061..000000000 --- a/.github/workflows/package_and_push_to_pypi.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Upload Python Package - -on: - release: - types: [created] - -jobs: - deploy: - environment: "Upload Python Package" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist - twine upload dist/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..6808db77a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,61 @@ +name: test + +on: + push: + branches: + - master + pull_request: + branches: + - master + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + STABLE_PYTHON_VERSION: "3.11" + +jobs: + run: + name: >- + Python ${{ matrix.python-version }} + on ${{ matrix.os }} + + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.8", "3.9", "3.10", "3.11"] + experimental: [false] + include: + - # Run tests against the next Python version, but no need for the full list of OSes. + os: ubuntu-latest + python-version: "3.12-dev" + experimental: true + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Hatch + run: pip install --upgrade hatch + + - # TODO: Fix lints before enabling this + name: Lint + if: matrix.python-version == env.STABLE_PYTHON_VERSION && runner.os == 'Linux' + run: echo hatch run lint + + - # TODO: Implement the examples to work in hatch + name: Examples + if: matrix.python-version == env.STABLE_PYTHON_VERSION && runner.os == 'Linux' + run: echo hatch run example + + - name: Run tests and track code coverage + run: hatch run test From 3634a5ca4c6817f03d2a07c7ba1d41e75a7e832d Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Sep 2023 18:00:19 +1000 Subject: [PATCH 0030/1376] docs: rewrite contributing.md This commit rewrites the contributing.md file to be up to date with the current state of the project. It draws heavily from the `CONTRIBUTING.md` file in the [Docusaurus](https://github.com/facebook/docusaurus) project. Signed-off-by: JP-Ellis --- CONTRIBUTING.md | 215 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 158 insertions(+), 57 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 78d6b3c2a..ff77b77bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,86 +1,187 @@ -# Raising issues +# Contributing to Pact Python -_Before raising an issue, please ensure that you are using the latest version of pact-python._ +Pact Python is the Python implementation of the [Pact](https://pact.io) integration testing framework. If you're interested in contributing to Pact Python, hopefully, this document makes the process for contributing clear. -Please provide the following information with your issue to enable us to respond as quickly as possible. +The [Open Source Guides](https://opensource.guide/) website has a collection of resources for individuals, communities, and companies who want to learn how to run and contribute to an open source project. Contributors and people new to open source alike will find the following guides especially useful: -- The relevant versions of the packages you are using. -- The steps to recreate your issue. -- The full stacktrace if there is an exception. -- An executable code example where possible. You can fork this repository and - use the [examples] directory to quickly recreate your issue. +- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) +- [Building Welcoming Communities](https://opensource.guide/building-community/) -# Contributing +## Get Involved -1. Fork it -2. Create your feature branch (`git checkout -b my-new-feature`) -3. Commit your changes (`git commit -am 'Add some feature'`) -4. Push to the branch (`git push origin my-new-feature`) -5. Create new Pull Request +There are many ways to contribute to Pact Python and the broader Pact ecosystem. Here's a few ideas to get started: -If you are intending to implement a fairly large feature we'd appreciate if you open -an issue with GitHub detailing your use case and intended solution to discuss how it -might impact other work that is in flight. +- Look through the [open issues](https://github.com/pact-foundation/pact-python/issues). Provide workarounds, ask for clarification, or suggest labels. Help [triage issues](#triaging-issues-and-pull-requests). +- If you find an issue you would like to fix, [open a pull request](#pull-requests). Issues tagged as [_Good first issue_](https://github.com/pact-foundation/pact-python/labels/Good%20first%20issue) are a good place to get started. +- Read through the [docs](https://docs.pact.io). If you find anything that is confusing or can be improved, you can click "Edit this page" at the bottom of most docs, which takes you to the GitHub interface to make and propose changes. +- Take a look at the [features requested](https://github.com/pact-foundation/pact-python/labels/feature) by others in the community and consider opening a pull request if you see something you want to work on. -We also appreciate it if you take the time to update and write tests for any changes -you submit. +Contributions are very welcome. If you think you need help planning your contribution, please ping us on [Slack](https://slack.pact.io) and let us know you are looking for a bit of help. -[examples]: https://github.com/pact-foundation/pact-python/tree/master/examples +### Join our Slack -## Commit messages +We have a [Slack](https://slack.pact.io) to discuss all things about Pact and its development. Feel free to ask questions about Pact Python specifically in the [`#pact-python`](https://pact-foundation.slack.com/archives/C9VECUP6E) channel, or broader questions about the Pact ecosystem over in the [`#general`](https://pact-foundation.slack.com/archives/C5F4KFKR8) channel. -pact-python is adopting the [Conventional Commits](https://www.conventionalcommits.org) -convention. Please ensure you follow the guidelines, we don't want to be that person, -but the commit messages are very important to the automation of our release process. +### Triaging Issues and Pull Requests -Take a look at the git history (`git log`) to get the gist of it. +One great way you can contribute to the project without writing any code is to help triage issues and pull requests as they come in. -If you'd like to get some CLI assistance there is a node npm package. Example usage is: +- Ask for more information if you believe the issue does not provide all the details required to solve it. +- Suggest [labels](https://github.com/pact-foundation/pact-python/labels) that can help categorize issues. +- Flag issues that are stale or that should be closed. +- Ask for test plans and review code. -```shell -npm install -g commitizen -npm install -g cz-conventional-changelog -echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc -``` +## Our Development Process -When you commit with Commitizen, you'll be prompted to fill out any required -commit fields at commit time. Simply use `git cz` or just `cz` instead of -`git commit` when committing. You can also use `git-cz`, which is an alias -for `cz`. +Pact Python uses [GitHub](https://github.com/pact-foundation/pact-python) as its source of truth. The team will be working directly there. All changes will be public from the beginning. -See https://www.npmjs.com/package/commitizen for more info. +All pull requests will be checked by the continuous integration system, GitHub actions. There are unit tests, end-to-end tests, performance tests, style tests, and much more. -There is a pypi package that does similar [commitizen](https://pypi.org/project/commitizen/). +### Branch Organization -## Running the tests +Pact Python has one primary branch `master` and we use feature branches to deliver new features with pull requests. We use the following naming convention for branches: -You can run the tests locally with `make test`, this will run the tests with `tox` +- `feature/` or `feat/` for new features +- `fix/` for bug fixes +- `chore/` for chores +- `docs/` for documentation changes -You will need `pyenv` to test on different versions `3.6`, `3.7`, `3.8`, `3.9`, -`3.10`, `3.11`. +These generally follow the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) categories. -Download and install python versions: -``` -pyenv install 3.6.15 3.7.16 3.8.16 3.9.16 3.10.10 3.11.2 -``` +## Issues -Set these versions locally for the project: -``` -pyenv local 3.6.15 3.7.16 3.8.16 3.9.16 3.10.10 3.11.2 -``` +When [opening a new issue](https://github.com/pact-foundation/pact-python/issues/new/choose), always make sure to fill out the issue template. **This step is very important!** Not doing so may slow down the response. Don't take this personally if this happens, and feel free to open a new issue once you've gathered all the information required by the template. + +**Please don't use the GitHub issue tracker for questions.** If you have questions about using Pact Python, prefer the [Discussion pages](https://github.com/pact-foundation/pact-python/discussions) or [Slack](https://slack.pact.io), and we will do our best to answer your questions. + +### Bugs + +We use [GitHub Issues](https://github.com/pact-foundation/pact-python/issues) for our public bugs. If you would like to report a problem, take a look around and see if someone already opened an issue about it. If you are certain this is a new, unreported bug, you can submit a [bug report](https://github.com/pact-foundation/pact-python/issues/new?assignees=&labels=bug%2Cstatus%3A+needs+triage&template=bug.yml). + +- **One issue, one bug:** Please report a single bug per issue. +- **Provide reproduction steps:** List all the steps necessary to reproduce the issue. The person reading your bug report should be able to follow these steps to reproduce your issue with minimal effort. + +If you're only fixing a bug, it's fine to submit a pull request right away but we still recommend filing an issue detailing what you're fixing. This is helpful in case we don't accept that specific fix but want to keep track of the issue. + +### Feature requests + +If you would like to request a new feature or enhancement but are not yet thinking about opening a pull request, you can file an issue with the [feature template](https://github.com/pact-foundation/pact-python/issues/new?assignees=&labels=feature%2Cstatus%3A+needs+triage&template=feature.yml) for more thought out ideas. Alternatively, you can use the [Canny board](https://pact.canny.io/) for more casual feature requests and gain enough traction before proposing an RFC. + +### Claiming issues + +We have a list of [beginner-friendly issues](https://github.com/pact-foundation/pact-python/labels/good%20first%20issue) to help you get your feet wet in the Pact Python codebase and familiar with our contribution process. This is a great place to get started. + +Apart from the `good first issue`, it is also worth looking at the [`help wanted`](https://github.com/pact-foundation/pact-python/labels/help%20wanted) issues. If you have specific knowledge in one domain, working on these issues can make your expertise shine. + +If you want to work on any of these issues, just drop a message saying "I'd like to work on this", and we will assign the issue to you and update the issue's status as "claimed". + +Alternatively, when opening an issue, you can also click the "self service" checkbox to indicate that you'd like to work on the issue yourself, which will also make us see the issue as "claimed". + +## Development + +### Online one-click setup for contributing + +You can also try using the new [github.dev](https://github.dev/pact-foundation/pact-python) feature. While you are browsing any file, changing the domain name from `github.com` to `github.dev` will turn your browser into an online editor. You can start making changes and send pull requests right away. + +### Installation + +1. Ensure you have [Python](https://www.python.org/) installed. + +2. Ensure you have [Hatch](https://hatch.pypa.io/) installed. This is used to manage the development environment can be installed as follows: + + ```sh + python -m pip install --user pipx # If you don't have pipx + pipx install hatch + ``` + +3. After cloning the repository, run `hatch shell` in the root of the repository. This will install all dependencies in a Python virtual environment and then ensure that the virtual environment is activated. + +4. To run tests, run `hatch run test` to make sure the test suite is working. You should also make sure the example works by running `hatch run example`. For the examples, you will have to make sure that you have Docker (or a suitable alternative) installed and running. + +5. If you want to run the tests against all supported Python versions, you can run `hatch run test:all`. + +### Code Conventions -Run the tests: +- **Most important: Look around.** Match the style you see used in the rest of the project. This includes formatting, naming files, naming things in code, naming things in documentation, etc. +- "Attractive" +- We do have Black (a formatter) and Ruff (a syntax linter) to catch most stylistic problems. If you are working locally, they should automatically fix some issues during git commits and push. + +Don't worry too much about styles in general—the maintainers will help you fix them as they review your code. + +To help catch a lot of simple formatting or linting issues, you can run `hatch run lint` to run Black and Ruff. This process can also be automated by installing [`pre-commit`](https://pre-commit.com/) hooks: + +```sh +pre-commit install ``` -make test + +## Pull Requests + +So you have decided to contribute code back to upstream by opening a pull request. You've invested a good chunk of time, and we appreciate it. We will do our best to work with you and get the PR looked at. + +Working on your first Pull Request? You can learn how from this free video series: + +[**How to Contribute to an Open Source Project on GitHub**](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) + +Please make sure the following is done when submitting a pull request: + +1. **Keep your PR small.** Small pull requests (~300 lines of diff) are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. +2. **Use descriptive titles.** It is recommended to follow this [commit message style](#semantic-commit-messages). +3. **Test your changes.** Describe your [**test plan**](#test-plan) in your pull request description. + +All pull requests should be opened against the `master` branch. + +We have a lot of integration systems that run automated tests to guard against mistakes. The maintainers will also review your code and fix obvious issues for you. These systems' duty is to make you worry as little about the chores as possible. Your code contributions are more important than sticking to any procedures, although completing the checklist will surely save everyone's time. + +### Conventional Commit Messages + +Pact Python has adopted the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) convention and we use it to generate our changelog and in the automation of our release process. + +The format is: + +```text +(): ``` -### macOS Setup Guide +`` is optional. If your change is specific to one/two packages, consider adding the scope. Scopes should be brief but recognizable, e.g. `docs`, `ci`, etc. You can take a quick look at the Git history (`git log`) to get the gist. + +The various types of commits: + +- `feat`: a new API or behavior **for the end user**. +- `fix`: a bug fix **for the end user**. +- `docs`: a change to the website or other Markdown documents in our repo. +- `style`: a change to production code that leads to no behavior difference, e.g. splitting files, renaming internal variables, improving code style... +- `test`: adding missing tests, refactoring tests; no production code change. +- `chore`: upgrading dependencies, releasing new versions... Chores that are **regularly done** for maintenance purposes. +- `misc`: anything else that doesn't change production code and doesn't fit in the above. + +If you'd like to get some CLI assistance, you can install [commitizen](https://www.npmjs.com/package/commitizen). The `cz` command line tool will help you write conventional commit messages. + +### Test Plan + +A good test plan has the exact commands you ran and their output. + +Tests are integrated into our continuous integration system, so you don't always need to run local tests. However, for significant code changes, it saves both your and the maintainers' time if you can do exhaustive tests locally first to make sure your PR is in good shape. There are many types of tests: -See the following guides to setup Python and configure `pyenv` on your Mac. +- **Build and typecheck.** We use [mypy](https://mypy.readthedocs.io/en/stable/) in our codebase, which can make sure your code is consistent and catches some obvious mistakes early. +- **Unit tests.** We use [pytest](https://pytest.org) for unit tests. You can run `hatch run test` in the root directory to run all tests, and `hatch run test:all` to test against all supported Python versions. -- [How to Install Python on macOS](https://realpython.com/installing-python/#how-to-install-python-on-macos) -- [Managing Multiple Python Versions With pyenv](https://realpython.com/intro-to-pyenv/) +### Licensing + +By contributing to Pact Python, you agree that your contributions will be licensed under its MIT license. + +### Breaking Changes + +When adding a new breaking change, follow this template in your pull request: + +```md +### New breaking change here + +- **Who does this affect**: +- **How to migrate**: +- **Why make this breaking change**: +- **Severity (number of people affected x effort)**: +``` -## Running the examples +### What Happens Next? -Make sure you have docker running! +The team will be monitoring pull requests. Do help us by keeping pull requests consistent by following the guidelines above. From 6d223fcef1909c9962f86752f00456b4af90a27e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Sep 2023 18:04:43 +1000 Subject: [PATCH 0031/1376] docs: add issue and pr templates Along with the overhaul of the `CONTRIBUTING.md` file, this commit adds issue and PR templates to the repository. These templates have been heavily inspired by the [Docusaurus](https://github.com/facebook/docusaurus) project. Signed-off-by: JP-Ellis --- .github/ISSUE_TEMPLATE/bug.yml | 114 +++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 14 ++++ .github/ISSUE_TEMPLATE/feature.yml | 54 ++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 24 ++++++ 4 files changed, 206 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 000000000..e56f7cdec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,114 @@ +name: 🐛 Bug Report +description: Submit a bug report to help us improve +labels: [bug, triage] +body: + - type: markdown + attributes: + value: | + ## Please help us help you! + + Before filing your issue, ask yourself: + + - Is this clearly a Pact Python bug? + - Do I have basic ideas about where it goes wrong? + - Could it be because of my own mistakes? + + **The GitHub issue tracker is not a support forum**. If you are not sure whether it could be your mistakes, ask on [Slack](https://slack.pact.io). + + Make the bug obvious. Ideally, we should be able to understand it without running any code. + + Bugs are fixed faster if you include: + - A reproduction repository to replicate the issue + - Pact files + - Logs + + - type: checkboxes + attributes: + label: Have you read the Contributing Guidelines on issues? + options: + - label: I have read the [Contributing Guidelines on issues](https://github.com/pact-foundation/pact-python/main/CONTRIBUTING.md#issues). + required: true + + - type: checkboxes + attributes: + label: Prerequisites + description: Please check the following items before creating a issue. This way we know you've done these steps first. + options: + - label: I'm using the latest version of `pact-python`. + required: true + - label: I have read the console error message carefully (if applicable). + + - type: textarea + attributes: + label: Description + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: input + attributes: + label: Reproducible demo + description: | + Paste the link to an example repo if possible, and exact instructions to reproduce the issue. + + > **What happens if you skip this step?** Someone will read your bug report, and maybe will be able to help you, but it's unlikely that it will get much attention from the team. Eventually, the issue will likely get closed in favor of issues that have reproducible demos. + + Please remember that: + + - Issues without reproducible demos have a very low priority. + - The person fixing the bug would have to do that anyway. Please be respectful of their time. + - You might figure out the issues yourself as you work on extracting it. + + Thanks for helping us help you! + + - type: textarea + attributes: + label: Steps to reproduce + description: Write down the steps to reproduce the bug. You should start with a fresh installation, or your git repository linked above. + placeholder: | + 1. Step 1... + 2. Step 2... + 3. Step 3... + validations: + required: true + + - type: textarea + attributes: + label: Expected behavior + description: How did you expect your project to behave? It's fine if you're not sure your understanding is correct. Write down what you thought would happen. + placeholder: Write what you thought would happen. + validations: + required: true + + - type: textarea + attributes: + label: Actual behavior + description: | + Did something go wrong? Is something broken, or not behaving as you expected? Describe this section in detail. Don't only say "it doesn't work"! Please submit exhaustive and complete log messages, not just the final error message. + + > Please read error messages carefully: it often tells you exactly what you are doing wrong. + + If the logs are too long, you can paste them in a [gist](https://gist.github.com/) and link it here. + placeholder: | + Write what happened. Add full console log messages. + validations: + required: true + + - type: textarea + attributes: + label: Your environment + description: Include as many relevant details about the environment you experienced the bug in. + value: | + - Public source code: + - Pact Python version used: `pip list | grep pact` + - Operating system and version (e.g. Ubuntu 20.04.2 LTS, macOS Ventura): + + - type: checkboxes + attributes: + label: Self-service + description: | + If you feel like you could contribute to this issue, please check the box below. This would tell us and other people looking for contributions that someone's working on it. + + If you do check this box, please send a pull request. If circumstances change and you can't work on it anymore, let us know and we can re-assign it. + options: + - label: I'd be willing to fix this bug myself. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..7c09cc37b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: 🚀 Feature request + url: https://pact.canny.io + about: The Canny board to send us feature requests, vote and measure the interest of users. Useful to submit a feature request when you have an idea but no concrete API design proposal. + - name: ❓ Simple question - Slack chat + url: https://slack.pact.io + about: If you have a simple question, or want to discuss a feature request, join our Slack chat. + - name: ❓ Simple question - Stack Overflow + url: https://stackoverflow.com/questions/tagged/pact + about: The GitHub issue tracker is not for technical support. Please use Stack Overflow, and ask the community for help. + - name: ❓ Advanced question - GitHub Discussions + url: https://github.com/pact-foundation/pact-python/discussions + about: Use GitHub Discussions for advanced and unanswered questions only, requiring a maintainer's answer. Make sure the question wasn't already asked. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 000000000..a27030432 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,54 @@ +name: 💅 Feature design / RFC +description: Submit a detailed feature request with a concrete proposal +labels: [feature, triage] +body: + - type: markdown + attributes: + value: | + Important things: + + - We expect the feature request to be detailed. + - The request does not have to be perfect, we'll discuss it and fix it if needed. + - For a more "casual" feature request, consider using Canny instead: https://pact.canny.io. + + - type: checkboxes + attributes: + label: Have you read the Contributing Guidelines on issues? + options: + - label: I have read the [Contributing Guidelines on issues](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md#issues). + required: true + + - type: textarea + attributes: + label: Description + description: A clear and concise description of what the feature is. + validations: + required: true + + - type: input + attributes: + label: Has this been requested on Canny? + description: Please post the [Canny](https://pact.canny.io) link, it is helpful to see how much interest there is for this feature. + + - type: textarea + attributes: + label: Motivation + description: Please outline the motivation for the proposal and why it should be implemented. Has this been requested by a lot of users? + validations: + required: true + + - type: textarea + attributes: + label: Have you tried building it? + description: | + Please explain how you tried to build the feature by yourself, and how successful it was. If you haven't tried, that's alright. + + - type: checkboxes + attributes: + label: Self-service + description: | + If you answered the question above as "no" because you feel like you could contribute directly to our repo, please check the box below. This would tell us and other people looking for contributions that someone's working on it. + + If you do check this box, please send a pull request. If circumstances change and you can't work on it anymore, let us know and we can re-assign it. + options: + - label: I'd be willing to contribute this feature to Docusaurus myself. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..cbc0068bf --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,24 @@ + + +## Pre-flight checklist + +- [ ] I have read the [Contributing Guidelines on pull requests](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md#pull-requests). +- [ ] **If this is a code change**: I have written unit tests and/or added dogfooding pages to fully verify the new behavior. +- [ ] **If this is a new API or substantial change**: the PR has an accompanying issue (closes #0000) and the maintainers have approved on my working plan. + +## Motivation + + + +## Test Plan + + + +## Related issues/PRs + + From 7c60dd5e0979907828b27ff7b9bb5069eec397c8 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 22 Sep 2023 11:37:22 +1000 Subject: [PATCH 0032/1376] docs: incorporate suggestions from @YOU54F Signed-off-by: JP-Ellis --- .github/ISSUE_TEMPLATE/bug.yml | 7 +++++-- .github/PULL_REQUEST_TEMPLATE.md | 21 +++++++++++++++++---- CONTRIBUTING.md | 23 +++++++++++++++-------- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index e56f7cdec..be7292c2a 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -9,11 +9,12 @@ body: Before filing your issue, ask yourself: + - Has this bug already been reported? - Is this clearly a Pact Python bug? - Do I have basic ideas about where it goes wrong? - - Could it be because of my own mistakes? + - Could it be because of something on my end? - **The GitHub issue tracker is not a support forum**. If you are not sure whether it could be your mistakes, ask on [Slack](https://slack.pact.io). + **The GitHub issue tracker is not a support forum**. If you are not sure whether it could be on your end or within Pact Python, ask on [Slack](https://slack.pact.io). Make the bug obvious. Ideally, we should be able to understand it without running any code. @@ -100,7 +101,9 @@ body: description: Include as many relevant details about the environment you experienced the bug in. value: | - Public source code: + - Is this a consumer or provider issue? Do you have information about the other side? - Pact Python version used: `pip list | grep pact` + - Information about your Pact broker (version, hosted where, pactflow, ...) - Operating system and version (e.g. Ubuntu 20.04.2 LTS, macOS Ventura): - type: checkboxes diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cbc0068bf..f43b17a19 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,20 +5,33 @@ You can learn more about contributing to Pact-python here: https://github.com/pa Happy contributing! --> -## Pre-flight checklist +## :airplane: Pre-flight checklist - [ ] I have read the [Contributing Guidelines on pull requests](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md#pull-requests). - [ ] **If this is a code change**: I have written unit tests and/or added dogfooding pages to fully verify the new behavior. - [ ] **If this is a new API or substantial change**: the PR has an accompanying issue (closes #0000) and the maintainers have approved on my working plan. -## Motivation +## :memo: Summary + + + +## :rotating_light: Breaking Changes + + + +## :fire: Motivation -## Test Plan +## :hammer: Test Plan -## Related issues/PRs +## :link: Related issues/PRs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff77b77bb..4b6ca493c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,7 @@ The [Open Source Guides](https://opensource.guide/) website has a collection of - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [Building Welcoming Communities](https://opensource.guide/building-community/) +- [Contributing to Pact](https://docs.pact.io/contributing) ## Get Involved @@ -18,9 +19,11 @@ There are many ways to contribute to Pact Python and the broader Pact ecosystem. Contributions are very welcome. If you think you need help planning your contribution, please ping us on [Slack](https://slack.pact.io) and let us know you are looking for a bit of help. -### Join our Slack +### Join our Community -We have a [Slack](https://slack.pact.io) to discuss all things about Pact and its development. Feel free to ask questions about Pact Python specifically in the [`#pact-python`](https://pact-foundation.slack.com/archives/C9VECUP6E) channel, or broader questions about the Pact ecosystem over in the [`#general`](https://pact-foundation.slack.com/archives/C5F4KFKR8) channel. +We have a [Slack](https://slack.pact.io) to discuss all things about Pact and its development. Feel free to ask questions about Pact Python specifically in the [`#pact-python`](https://pact-foundation.slack.com/archives/C9VECUP6E) channel, or broader questions about the Pact ecosystem over in the [`#general`](https://pact-foundation.slack.com/archives/C5F4KFKR8) channel. We store a searchable archive of our Slack channels on [linen.dev](https://linen.dev/s/pact-foundation). + +Questions have also been asked over on StackOverflow, under the [`pact`](https://stackoverflow.com/questions/tagged/pact) tag. This is a great place to ask more general usage questions for pact, and discover existing answers. ### Triaging Issues and Pull Requests @@ -39,15 +42,13 @@ All pull requests will be checked by the continuous integration system, GitHub a ### Branch Organization -Pact Python has one primary branch `master` and we use feature branches to deliver new features with pull requests. We use the following naming convention for branches: +Pact Python has one primary branch `master` and we use feature branches to deliver new features with pull requests. Typically, we scope the branch according to the [conventional commit](#conventional-commit-messages) categories. The more common ones are: - `feature/` or `feat/` for new features - `fix/` for bug fixes - `chore/` for chores - `docs/` for documentation changes -These generally follow the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) categories. - ## Issues When [opening a new issue](https://github.com/pact-foundation/pact-python/issues/new/choose), always make sure to fill out the issue template. **This step is very important!** Not doing so may slow down the response. Don't take this personally if this happens, and feel free to open a new issue once you've gathered all the information required by the template. @@ -77,11 +78,15 @@ If you want to work on any of these issues, just drop a message saying "I'd like Alternatively, when opening an issue, you can also click the "self service" checkbox to indicate that you'd like to work on the issue yourself, which will also make us see the issue as "claimed". +Once an issue is claimed, we hope to see a pull request; however we understand that life happens and you may not be able to complete the issue. If you are unable to complete the issue, please let us know so we can unassign the issue and make it available for others to work on. + +The claiming process is there to help ensure effort is wasted. Even if you are not sure whether you can complete the issue, claiming it will help us know that someone is working on it. If you are not sure how to proceed, feel free to ask for help. + ## Development ### Online one-click setup for contributing -You can also try using the new [github.dev](https://github.dev/pact-foundation/pact-python) feature. While you are browsing any file, changing the domain name from `github.com` to `github.dev` will turn your browser into an online editor. You can start making changes and send pull requests right away. +You can also try using the new [github.dev](https://github.dev/pact-foundation/pact-python) feature. While you are browsing any file, changing the domain name from `github.com` to `github.dev` will turn your browser into an online editor. You can start making changes and send pull requests right away. This is a great way to get started quickly, but it does not offer the full development environment and you won't be able to run tests. ### Installation @@ -116,7 +121,9 @@ pre-commit install ## Pull Requests -So you have decided to contribute code back to upstream by opening a pull request. You've invested a good chunk of time, and we appreciate it. We will do our best to work with you and get the PR looked at. +So you are considering contributing to Pact Python's code? Great! We'd love to have you. First off, please make sure it is related to an existing issue. If not, please open a new issue to discuss the problem you are trying to solve before investing a lot of time into a pull request. While we do accept PRs that are not related to an issue (especially if the PR is very simple), it is best to discuss it first to avoid wasting your time. + +Once you have opened a PR, we will do our best to work with you and get the PR looked at. Working on your first Pull Request? You can learn how from this free video series: @@ -130,7 +137,7 @@ Please make sure the following is done when submitting a pull request: All pull requests should be opened against the `master` branch. -We have a lot of integration systems that run automated tests to guard against mistakes. The maintainers will also review your code and fix obvious issues for you. These systems' duty is to make you worry as little about the chores as possible. Your code contributions are more important than sticking to any procedures, although completing the checklist will surely save everyone's time. +We have a lot of integration systems that run automated tests to guard against mistakes. The maintainers will also review your code and may fix obvious issues for you. These systems' duty is to make you worry as little about the chores as possible. Your code contributions are more important than sticking to any procedures, although completing the checklist will surely save everyone's time. ### Conventional Commit Messages From 8ee2eb09ddd7cb11ace6bc50c00e803c421b9930 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 14 Sep 2023 17:00:14 +1000 Subject: [PATCH 0033/1376] feat(example): simplify docker-compose For the purposes of showcasing an example, the previous `docker-compose` was rather excessive running the Pact Broker behind a Nginx proxy with a PostgreSQL backend. This simplifies the containers to just use the core Pact broker image and uses `sqlite` for the database. This commit also drops SSL/TLS from the example as it does not meaningfully contribute to the example. Signed-off-by: JP-Ellis --- examples/broker/docker-compose.yml | 61 ------------------------ examples/broker/ssl/nginx-selfsigned.crt | 21 -------- examples/broker/ssl/nginx-selfsigned.key | 27 ----------- examples/broker/ssl/nginx.conf | 23 --------- examples/docker-compose.yml | 42 ++++++++++++++++ 5 files changed, 42 insertions(+), 132 deletions(-) delete mode 100644 examples/broker/docker-compose.yml delete mode 100644 examples/broker/ssl/nginx-selfsigned.crt delete mode 100644 examples/broker/ssl/nginx-selfsigned.key delete mode 100644 examples/broker/ssl/nginx.conf create mode 100644 examples/docker-compose.yml diff --git a/examples/broker/docker-compose.yml b/examples/broker/docker-compose.yml deleted file mode 100644 index 11585e298..000000000 --- a/examples/broker/docker-compose.yml +++ /dev/null @@ -1,61 +0,0 @@ -version: '3.9' - -services: - # A PostgreSQL database for the Broker to store Pacts and verification results - postgres: - image: postgres - healthcheck: - test: psql postgres --command "select 1" -U postgres - ports: - - "5432:5432" - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: password - POSTGRES_DB: postgres - - # The Pact Broker - broker_app: - # Alternatively the DiUS Pact Broker can be used: - # image: dius/pact-broker - # - # As well as changing the image, the destination port will need to be changed - # from 9292 below, and in the nginx.conf proxy_pass section - image: pactfoundation/pact-broker:latest-multi - ports: - - "80:9292" - depends_on: - - postgres - links: - - postgres - environment: - PACT_BROKER_DATABASE_USERNAME: postgres - PACT_BROKER_DATABASE_PASSWORD: password - PACT_BROKER_DATABASE_HOST: postgres - PACT_BROKER_DATABASE_NAME: postgres - PACT_BROKER_BASIC_AUTH_USERNAME: pactbroker - PACT_BROKER_BASIC_AUTH_PASSWORD: pactbroker - PACT_BROKER_DATABASE_CONNECT_MAX_RETRIES: "5" - # The Pact Broker provides a healthcheck endpoint which we will use to wait - # for it to become available before starting up - healthcheck: - test: [ "CMD", "wget", "-q", "--tries=1", "--spider", "http://pactbroker:pactbroker@localhost:9292/diagnostic/status/heartbeat" ] - interval: 1s - timeout: 2s - retries: 5 - - # An NGINX reverse proxy in front of the Broker on port 8443, to be able to - # terminate with SSL - nginx: - image: nginx:alpine - links: - - broker_app:broker - volumes: - - ./ssl/nginx.conf:/etc/nginx/conf.d/default.conf:ro - - ./ssl:/etc/nginx/ssl - ports: - - "8443:443" - restart: always - depends_on: - broker_app: - condition: service_healthy - \ No newline at end of file diff --git a/examples/broker/ssl/nginx-selfsigned.crt b/examples/broker/ssl/nginx-selfsigned.crt deleted file mode 100644 index 144287f9c..000000000 --- a/examples/broker/ssl/nginx-selfsigned.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDiDCCAnACCQCWW6LywpPSwjANBgkqhkiG9w0BAQsFADCBhTELMAkGA1UEBhMC -QVUxEzARBgNVBAgMClNvbWUgU3RhdGUxDzANBgNVBAcMBlN5ZG5leTENMAsGA1UE -CgwEUGFjdDEPMA0GA1UECwwGUHl0aG9uMRIwEAYDVQQDDAlsb2NhbGhvc3QxHDAa -BgkqhkiG9w0BCQEWDXNvbWVAbWFpbC5jb20wHhcNMjAwNjEwMTUzOTM2WhcNMjMw -MzMxMTUzOTM2WjCBhTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUgU3RhdGUx -DzANBgNVBAcMBlN5ZG5leTENMAsGA1UECgwEUGFjdDEPMA0GA1UECwwGUHl0aG9u -MRIwEAYDVQQDDAlsb2NhbGhvc3QxHDAaBgkqhkiG9w0BCQEWDXNvbWVAbWFpbC5j -b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDm/BMUkuVaYwLjnoq/ -u4fFKoBGSPl3CxvSUWhzlsaM5i+UlS7ZLwXxAxw+Vba9cztSyYHNs2BCxCHUWBFe -B818cXzQXbV0gunMz9oDxr8aQmwpRkIdxxBvmaqLbk6sjj5cTqRK39/BNtZEkZmA -QAOggnfB7Bx/OQmh4aidT6DytjA8ur3FofAVUVXHfQohm/kJOhqcdXL5pBQqD2bh -Ua6KPbZTsfOmFLggZmhqPZSjS+leqFagpissW/aHSyk/3c+vhXOhEbCUeCXaz7up -/DNF/0OHF4+r2UaeonxMxC/X6NEhNYHyNPypbdC3/59Zoa2Spu2BLy8ZoChe1dRk -hZqtAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHpq3JmhAm5t/orY4ONFxPq1iF89 -3nKsKckfcOpDF/zjS2+6I30LVByuU88BKdTt7tsRojoWXEI01YGqYWTEwerfESr9 -M16xek5h5e7XJqp9jzyX6kswel/rWB8rF93biW0v00/KKRwwIr5IDvKb4XvugzW4 -FEG+1nhXCyjrkmKV/bbCfdkBHgavaj5TPv1LoXOX7VDRjwqoM7RP/z6JJsZkxDx3 -TkXtC8Lw4LF+tpWY8nQu3/HCqwxL7Vgy4M/IvoXRePdSI6goH8ri0zFuK9pvAREK -IjY271t+lapu8sDqUEf9tW/98YhxpBInQYBL2bEEtMYTRXRm06fSn7o3IlM= ------END CERTIFICATE----- diff --git a/examples/broker/ssl/nginx-selfsigned.key b/examples/broker/ssl/nginx-selfsigned.key deleted file mode 100644 index e8a6b5423..000000000 --- a/examples/broker/ssl/nginx-selfsigned.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA5vwTFJLlWmMC456Kv7uHxSqARkj5dwsb0lFoc5bGjOYvlJUu -2S8F8QMcPlW2vXM7UsmBzbNgQsQh1FgRXgfNfHF80F21dILpzM/aA8a/GkJsKUZC -HccQb5mqi25OrI4+XE6kSt/fwTbWRJGZgEADoIJ3wewcfzkJoeGonU+g8rYwPLq9 -xaHwFVFVx30KIZv5CToanHVy+aQUKg9m4VGuij22U7HzphS4IGZoaj2Uo0vpXqhW -oKYrLFv2h0spP93Pr4VzoRGwlHgl2s+7qfwzRf9DhxePq9lGnqJ8TMQv1+jRITWB -8jT8qW3Qt/+fWaGtkqbtgS8vGaAoXtXUZIWarQIDAQABAoIBAQC3r5woz0yO3ZAN -nSWvpZ0pwUuzGRMxhOcCEPUkfrG0mNUbrqtL0WZDLHsIYzdoXzu88TxFbbFORxSz -/bkJ8uCJZuKf/PVxCy6MTnqMaD/OzSWgiRvI/GXoqeYC7ZypApE67NsgI/qXd1lb -vAG7CK0ZtscvsulSjvRHBOIG/6z5dUAKnLJjr7uKydMHSIKNafKAEA6HGDCvIu4d -J9EQzLfmpjLTkeB1DNZrv1mtNjf/kG/M/UX5a1RtOJTGvHQn/oZSUKng3DVUNBtq -dEO6Pi5n88xWuxH6YAWqqDjCfqyey1Jc1rQxfnx6vRPL7+IaXRugAKFMFm8Xbp9/ -/9eEDCyNAoGBAPZEjYH9u2856KYUTyky8gD1TOE9gf4x4zFjK6SzBT8v1y1RdSwQ -tf7ozj94OV/b9bAE3k/z2a09xYty5VBXs6MCluQTS67KgRaO9sSFtRmnupyBNk2z -r3QEYuVDmJ6Dk/3ovItXqFaW8IbOZMf6Acu5aEDx4UKmb2tzGGJ7DxF/AoGBAPAc -57p1yRWIG+hJMdkudXhBz+L3t2NbESWom33hi1mDMIKp3dwJmhA4kq+Uyqfl32uF -Iy3z+3xr2V1BdGg1RnicfcyjHaQ4/89YB+nkOHB8muV2R57tYahOgWn6rXXxTOBs -X2Vjd7ByAEFimrVfDH33inrYuIiI/cku4Xyj71HTAoGBAJeyrsBuPfFL6KW1SPYF -7dDtSchNjS+6J0sa3Z18sTS1EYVW8iiMuq8lVTb/pcgIxJUCyrbRbTssG+3EfsE4 -5Oz7AVvJDwvCrjXpJtTz0BTXnzoc1giTMPb0ZL75HqA2SQlVPh9PheCg5dUEekw9 -ErIdqbynwqy9vVCg+1pel2+dAoGAR1C+fsIHFG8VottCg/fpies6HHZosIjWwfGf -JTc9FTwCx3w+WeE8Mf8rihzOSCndPukPNtHVavH5YFpVgbH5GU+ZiZMU9ba8O9Aw -oYZYQQixVN/Zi9mDfOK8S0baCELAC5QEjW+KmAx0CPeJbb8qTaudJLmDrYHKpttW -u5dROGMCgYEAlgTZNiEeBAPQZD30CSvFUlZVCOOyu5crP9hCPA9um5FsvD9minSz -yJqeMj7zapZsatAzYwHrGG6nHnTKWEBNaimR7kjTpKdKzXQaA9XeVLmeFAZ3Exad -JDKTPI+asF+097sHUcVuloMOZXbD1uAZnvLWIwfsaHxs41AkF+0lmM4= ------END RSA PRIVATE KEY----- diff --git a/examples/broker/ssl/nginx.conf b/examples/broker/ssl/nginx.conf deleted file mode 100644 index e3c6bb36e..000000000 --- a/examples/broker/ssl/nginx.conf +++ /dev/null @@ -1,23 +0,0 @@ -server { - listen 443 ssl default_server; - server_name localhost; - ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt; - ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_prefer_server_ciphers on; - ssl_ecdh_curve secp384r1; - ssl_session_cache shared:SSL:10m; - ssl_stapling on; - ssl_stapling_verify on; - - location / { - # To use with the Dius Pact Broker: - # proxy_pass http://broker:80; - proxy_pass http://broker:9292; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Scheme "https"; - proxy_set_header X-Forwarded-Port "443"; - proxy_set_header X-Forwarded-Ssl "on"; - proxy_set_header X-Real-IP $remote_addr; - } -} diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml new file mode 100644 index 000000000..c845b1ee1 --- /dev/null +++ b/examples/docker-compose.yml @@ -0,0 +1,42 @@ +version: "3.9" + +services: + postgres: + image: postgres + ports: + - "5432:5432" + healthcheck: + test: psql postgres -U postgres --command 'SELECT 1' + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + + broker: + image: pactfoundation/pact-broker:latest-multi + depends_on: + - postgres + ports: + - "9292:9292" + environment: + # Basic auth credentials for the Broker + PACT_BROKER_ALLOW_PUBLIC_READ: "true" + PACT_BROKER_BASIC_AUTH_USERNAME: pactbroker + PACT_BROKER_BASIC_AUTH_PASSWORD: pactbroker + # Database + PACT_BROKER_DATABASE_URL: "postgres://postgres:postgres@postgres/postgres" + # PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite # Pending pact-foundation/pact-broker-docker#148 + + healthcheck: + test: + [ + "CMD", + "curl", + "--silent", + "--show-error", + "--fail", + "http://pactbroker:pactbroker@localhost:9292/diagnostic/status/heartbeat", + ] + interval: 1s + timeout: 2s + retries: 5 From 4c47843896785296c47716ac36aa4d6f3e3f2721 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 15 Sep 2023 16:30:20 +1000 Subject: [PATCH 0034/1376] chore(example): migrate consumer example Migrate the consumer example to the new example structure. This should help reduce the redundancy between the examples and make it easier for users to test them out and see how they work. Ultimately, the idea will be that the consumer tests run first, and then the provider tests run against the generated pact file. Signed-off-by: JP-Ellis --- .cirrus.yml | 2 - .github/workflows/test.yml | 5 +- examples/.ruff.toml | 11 ++ examples/common/sharedfixtures.py | 94 ------------- examples/conftest.py | 68 +++++++++ examples/consumer/conftest.py | 8 -- examples/consumer/requirements.txt | 6 - examples/consumer/run_pytest.sh | 4 - examples/consumer/src/consumer.py | 40 ------ .../tests/consumer/test_user_consumer.py | 129 ----------------- examples/pacts/.gitignore | 2 + .../pacts/userserviceclient-userservice.json | 65 --------- examples/src/__init__.py | 12 ++ examples/src/consumer.py | 103 ++++++++++++++ examples/tests/test_00_consumer.py | 130 ++++++++++++++++++ pyproject.toml | 3 +- 16 files changed, 330 insertions(+), 352 deletions(-) create mode 100644 examples/.ruff.toml delete mode 100644 examples/common/sharedfixtures.py create mode 100644 examples/conftest.py delete mode 100644 examples/consumer/conftest.py delete mode 100644 examples/consumer/requirements.txt delete mode 100755 examples/consumer/run_pytest.sh delete mode 100644 examples/consumer/src/consumer.py delete mode 100644 examples/consumer/tests/consumer/test_user_consumer.py create mode 100644 examples/pacts/.gitignore delete mode 100644 examples/pacts/userserviceclient-userservice.json create mode 100644 examples/src/__init__.py create mode 100644 examples/src/consumer.py create mode 100644 examples/tests/test_00_consumer.py diff --git a/.cirrus.yml b/.cirrus.yml index b8c4d9331..9c08833dc 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -5,8 +5,6 @@ TEST_TEMPLATE: &TEST_TEMPLATE - python --version # TODO: Fix lints before enabling - echo hatch run lint - # TODO: Implement the examples to work in hatch - - echo hatch run example - hatch run test linux_arm64_task: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6808db77a..ea1ae8d5c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,10 +52,9 @@ jobs: if: matrix.python-version == env.STABLE_PYTHON_VERSION && runner.os == 'Linux' run: echo hatch run lint - - # TODO: Implement the examples to work in hatch - name: Examples + - name: Examples if: matrix.python-version == env.STABLE_PYTHON_VERSION && runner.os == 'Linux' - run: echo hatch run example + run: hatch run example --color=yes --capture=no - name: Run tests and track code coverage run: hatch run test diff --git a/examples/.ruff.toml b/examples/.ruff.toml new file mode 100644 index 000000000..03292c3c2 --- /dev/null +++ b/examples/.ruff.toml @@ -0,0 +1,11 @@ +extend = "../pyproject.toml" + +ignore = [ + "S101", # Forbid assert statements + "D103", # Require docstring in public function +] + +[per-file-ignores] +"tests/**.py" = [ + "INP001", # Forbid implicit namespaces +] diff --git a/examples/common/sharedfixtures.py b/examples/common/sharedfixtures.py deleted file mode 100644 index 4cdece923..000000000 --- a/examples/common/sharedfixtures.py +++ /dev/null @@ -1,94 +0,0 @@ -import platform -import pathlib - -import docker -import pytest -from testcontainers.compose import DockerCompose - - -# This fixture is to simulate a managed Pact Broker or PactFlow account. -# For almost all purposes outside this example, you will want to use a real -# broker. See https://github.com/pact-foundation/pact_broker for further details. -@pytest.fixture(scope="session", autouse=True) -def broker(request): - version = request.config.getoption("--publish-pact") - publish = True if version else False - - # If the results are not going to be published to the broker, there is - # nothing further to do anyway - if not publish: - yield - return - - run_broker = request.config.getoption("--run-broker") - - if run_broker: - # Start up the broker using docker-compose - print("Starting broker") - with DockerCompose("../broker", compose_file_name=["docker-compose.yml"], pull=True) as compose: - stdout, stderr = compose.get_logs() - if stderr: - print("Errors\\n:{}".format(stderr)) - print("{}".format(stdout)) - print("Started broker") - - yield - print("Stopping broker") - print("Broker stopped") - else: - # Assuming there is a broker available already, docker-compose has been - # used manually as the --run-broker option has not been provided - yield - return - - -@pytest.fixture(scope="session", autouse=True) -def publish_existing_pact(broker): - """Publish the contents of the pacts folder to the Pact Broker. - - In normal usage, a Consumer would publish Pacts to the Pact Broker after - running tests - this fixture would NOT be needed. - . - Because the broker is being used standalone here, it will not contain the - required Pacts, so we must first spin up the pact-cli and publish them. - - In the Pact Broker logs, this corresponds to the following entry: - PactBroker::Pacts::Service -- Creating new pact publication with params \ - {:consumer_name=>"UserServiceClient", :provider_name=>"UserService", \ - :revision_number=>nil, :consumer_version_number=>"1", :pact_version_sha=>nil, \ - :consumer_name_in_pact=>"UserServiceClient", :provider_name_in_pact=>"UserService"} - """ - source = str(pathlib.Path.cwd().joinpath("..", "pacts").resolve()) - pacts = [f"{source}:/pacts"] - envs = { - "PACT_BROKER_BASE_URL": "http://broker_app:9292", - "PACT_BROKER_USERNAME": "pactbroker", - "PACT_BROKER_PASSWORD": "pactbroker", - } - - target_platform = platform.platform().lower() - - if 'macos' in target_platform or 'windows' in target_platform: - envs["PACT_BROKER_BASE_URL"] = "http://host.docker.internal:80" - - client = docker.from_env() - - print("Publishing existing Pact") - client.containers.run( - remove=True, - network="broker_default", - volumes=pacts, - image="pactfoundation/pact-cli:latest-multi", - environment=envs, - command="publish /pacts --consumer-app-version 1", - ) - print("Finished publishing") - - -def pytest_addoption(parser): - parser.addoption( - "--publish-pact", type=str, action="store", help="Upload generated pact file to pact broker with version" - ) - - parser.addoption("--run-broker", type=bool, action="store", help="Whether to run broker in this test or not.") - parser.addoption("--provider-url", type=str, action="store", help="The url to our provider.") diff --git a/examples/conftest.py b/examples/conftest.py new file mode 100644 index 000000000..3c0a5994a --- /dev/null +++ b/examples/conftest.py @@ -0,0 +1,68 @@ +""" +Shared PyTest configuration. + +In order to run the examples, we need to run the Pact broker. In order to avoid +having to run the Pact broker manually, or repeating the same code in each +example, we define a PyTest fixture to run the Pact broker. + +We also define a `pact_dir` fixture to define the directory where the generated +Pact files will be stored. You are encouraged to have a look at these files +after the examples have been run. +""" +from __future__ import annotations + +from pathlib import Path +from typing import Any, Generator + +import pytest +from testcontainers.compose import DockerCompose +from yarl import URL + +EXAMPLE_DIR = Path(__file__).parent.resolve() + + +def pytest_addoption(parser: pytest.Parser) -> None: + """Define additional command lines to customise the examples.""" + parser.addoption( + "--broker-url", + help=( + "The URL of the broker to use. If this option has been given, the container" + " will _not_ be started." + ), + type=str, + ) + + +@pytest.fixture(scope="session") +def broker(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: + """ + Fixture to run the Pact broker. + + This inspects whether the `--broker-url` option has been given. If it has, + it is assumed that the broker is already running and simply returns the + given URL. + + Otherwise, the Pact broker is started in a container. The URL of the + containerised broker is then returned. + """ + broker_url: str | None = request.config.getoption("--broker-url") + + # If we have been given a broker URL, there's nothing more to do here and we + # can return early. + if broker_url: + yield URL(broker_url) + return + + with DockerCompose( + EXAMPLE_DIR, + compose_file_name=["docker-compose.yml"], + pull=True, + ) as _: + yield URL("http://pactbroker:pactbroker@localhost:9292") + return + + +@pytest.fixture(scope="session") +def pact_dir() -> Path: + """Fixture for the Pact directory.""" + return EXAMPLE_DIR / "pacts" diff --git a/examples/consumer/conftest.py b/examples/consumer/conftest.py deleted file mode 100644 index 90ac63b9a..000000000 --- a/examples/consumer/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys - -# Load in the fixtures from common/sharedfixtures.py -sys.path.append("../common") - -pytest_plugins = [ - "sharedfixtures", -] diff --git a/examples/consumer/requirements.txt b/examples/consumer/requirements.txt deleted file mode 100644 index c93ec0f76..000000000 --- a/examples/consumer/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -pytest==7.0.1; python_version < '3.7' -pytest==7.1.3; python_version >= '3.7' -requests==2.27.1; python_version < '3.7' -requests>=2.28.0; python_version >= '3.7' -testcontainers==3.7.0; python_version < '3.7' -testcontainers==3.7.1; python_version >= '3.7' diff --git a/examples/consumer/run_pytest.sh b/examples/consumer/run_pytest.sh deleted file mode 100755 index f997e1583..000000000 --- a/examples/consumer/run_pytest.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -set -o pipefail - -pytest tests --run-broker True --publish-pact 1 diff --git a/examples/consumer/src/consumer.py b/examples/consumer/src/consumer.py deleted file mode 100644 index 6d6ed3268..000000000 --- a/examples/consumer/src/consumer.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Optional - -import requests -from datetime import datetime - - -class User(object): - """Define the basic User data we expect to receive from the User Provider.""" - - def __init__(self, name: str, created_on: str): - self.name = name - self.created_on = created_on - - -class UserConsumer(object): - """Demonstrate some basic functionality of how the User Consumer will interact - with the User Provider, in this case a simple get_user.""" - - def __init__(self, base_uri: str): - """Initialise the Consumer, in this case we only need to know the URI. - - :param base_uri: The full URI, including port of the Provider to connect to - """ - self.base_uri = base_uri - - def get_user(self, user_name: str) -> Optional[User]: - """Fetch a user object by user_name from the server. - - :param user_name: User name to search for - :return: User details if found, None if not found - """ - uri = self.base_uri + "/users/" + user_name - response = requests.get(uri) - if response.status_code == 404: - return None - - name = response.json()["name"] - created_on = datetime.strptime(response.json()["created_on"], "%Y-%m-%dT%H:%M:%S") - - return User(name, created_on) diff --git a/examples/consumer/tests/consumer/test_user_consumer.py b/examples/consumer/tests/consumer/test_user_consumer.py deleted file mode 100644 index 72ebaf589..000000000 --- a/examples/consumer/tests/consumer/test_user_consumer.py +++ /dev/null @@ -1,129 +0,0 @@ -"""pact test for user service client""" - -import atexit -import logging -import os - -import pytest - -from pact import Consumer, Like, Provider, Term, Format -from src.consumer import UserConsumer - -log = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -# If publishing the Pact(s), they will be submitted to the Pact Broker here. -# For the purposes of this example, the broker is started up as a fixture defined -# in conftest.py. For normal usage this would be self-hosted or using PactFlow. -PACT_BROKER_URL = "http://localhost" -PACT_BROKER_USERNAME = "pactbroker" -PACT_BROKER_PASSWORD = "pactbroker" - -# Define where to run the mock server, for the consumer to connect to. These -# are the defaults so may be omitted -PACT_MOCK_HOST = "localhost" -PACT_MOCK_PORT = 1234 - -# Where to output the JSON Pact files created by any tests -PACT_DIR = os.path.dirname(os.path.realpath(__file__)) - - -@pytest.fixture -def consumer() -> UserConsumer: - return UserConsumer("http://{host}:{port}".format(host=PACT_MOCK_HOST, port=PACT_MOCK_PORT)) - - -@pytest.fixture(scope="session") -def pact(request): - """Setup a Pact Consumer, which provides the Provider mock service. This - will generate and optionally publish Pacts to the Pact Broker""" - - # When publishing a Pact to the Pact Broker, a version number of the Consumer - # is required, to be able to construct the compatability matrix between the - # Consumer versions and Provider versions - version = request.config.getoption("--publish-pact") - publish = True if version else False - - pact = Consumer("UserServiceClient", version=version).has_pact_with( - Provider("UserService"), - host_name=PACT_MOCK_HOST, - port=PACT_MOCK_PORT, - pact_dir=PACT_DIR, - publish_to_broker=publish, - broker_base_url=PACT_BROKER_URL, - broker_username=PACT_BROKER_USERNAME, - broker_password=PACT_BROKER_PASSWORD, - ) - - pact.start_service() - - # Make sure the Pact mocked provider is stopped when we finish, otherwise - # port 1234 may become blocked - atexit.register(pact.stop_service) - - yield pact - - # This will stop the Pact mock server, and if publish is True, submit Pacts - # to the Pact Broker - pact.stop_service() - - # Given we have cleanly stopped the service, we do not want to re-submit the - # Pacts to the Pact Broker again atexit, since the Broker may no longer be - # available if it has been started using the --run-broker option, as it will - # have been torn down at that point - pact.publish_to_broker = False - - -def test_get_user_non_admin(pact, consumer): - # Define the Matcher; the expected structure and content of the response - expected = { - "name": "UserA", - "id": Format().uuid, - "created_on": Term(r"\d+-\d+-\d+T\d+:\d+:\d+", "2016-12-15T20:16:01"), - "ip_address": Format().ip_address, - "admin": False, - } - - # Define the expected behaviour of the Provider. This determines how the - # Pact mock provider will behave. In this case, we expect a body which is - # "Like" the structure defined above. This means the mock provider will - # return the EXACT content where defined, e.g. UserA for name, and SOME - # appropriate content e.g. for ip_address. - ( - pact.given("UserA exists and is not an administrator") - .upon_receiving("a request for UserA") - .with_request("get", "/users/UserA") - .will_respond_with(200, body=Like(expected)) - ) - - with pact: - # Perform the actual request - user = consumer.get_user("UserA") - - # In this case the mock Provider will have returned a valid response - assert user.name == "UserA" - - # Make sure that all interactions defined occurred - pact.verify() - - -def test_get_non_existing_user(pact, consumer): - # Define the expected behaviour of the Provider. This determines how the - # Pact mock provider will behave. In this case, we expect a 404 - ( - pact.given("UserA does not exist") - .upon_receiving("a request for UserA") - .with_request("get", "/users/UserA") - .will_respond_with(404) - ) - - with pact: - # Perform the actual request - user = consumer.get_user("UserA") - - # In this case, the mock Provider will have returned a 404 so the - # consumer will have returned None - assert user is None - - # Make sure that all interactions defined occurred - pact.verify() diff --git a/examples/pacts/.gitignore b/examples/pacts/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/examples/pacts/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/examples/pacts/userserviceclient-userservice.json b/examples/pacts/userserviceclient-userservice.json deleted file mode 100644 index d3260f688..000000000 --- a/examples/pacts/userserviceclient-userservice.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "consumer": { - "name": "UserServiceClient" - }, - "provider": { - "name": "UserService" - }, - "interactions": [ - { - "description": "a request for UserA", - "providerState": "UserA exists and is not an administrator", - "request": { - "method": "get", - "path": "/users/UserA" - }, - "response": { - "status": 200, - "headers": { - }, - "body": { - "name": "UserA", - "id": "fc763eba-0905-41c5-a27f-3934ab26786c", - "created_on": "2016-12-15T20:16:01", - "ip_address": "127.0.0.1", - "admin": false - }, - "matchingRules": { - "$.body": { - "match": "type" - }, - "$.body.id": { - "match": "regex", - "regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" - }, - "$.body.created_on": { - "match": "regex", - "regex": "\\d+-\\d+-\\d+T\\d+:\\d+:\\d+" - }, - "$.body.ip_address": { - "match": "regex", - "regex": "(\\d{1,3}\\.)+\\d{1,3}" - } - } - } - }, - { - "description": "a request for UserA", - "providerState": "UserA does not exist", - "request": { - "method": "get", - "path": "/users/UserA" - }, - "response": { - "status": 404, - "headers": { - } - } - } - ], - "metadata": { - "pactSpecification": { - "version": "2.0.0" - } - } -} diff --git a/examples/src/__init__.py b/examples/src/__init__.py new file mode 100644 index 000000000..87b9d2a43 --- /dev/null +++ b/examples/src/__init__.py @@ -0,0 +1,12 @@ +""" +Example Client Code. + +This module defines a simple consumer and a couple of implementation of simple +providers. The general premise here is that the consumers will be fetching user +information from the providers. + +The development of the consumer and provider sides would typically be done in +separate teams (and likely different languages). Within the Pact framework, the +consumer side is the one which defines the contract and the provider side is the +one which must satisfy the contract. +""" diff --git a/examples/src/consumer.py b/examples/src/consumer.py new file mode 100644 index 000000000..3aab181da --- /dev/null +++ b/examples/src/consumer.py @@ -0,0 +1,103 @@ +""" +Simple Consumer Implementation. + +This modules defines a simple +[consumer](https://docs.pact.io/getting_started/terminology#service-consumer) +with Pact. As Pact is a consumer-driven framework, the consumer defines the +interactions which the provider must then satisfy. + +The consumer is the application which makes requests to another service (the +provider) and receives a response to process. In this example, we have a simple +[`User`](User) class and the consumer fetches a user's information from a HTTP +endpoint. + +Note that the code in this module is agnostic of Pact. The `pact-python` +dependency only appears in the tests. This is because the consumer is not +concerned with Pact, only the tests are. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +import requests + + +@dataclass() +class User: + """User data class.""" + + id: int # noqa: A003 + name: str + created_on: datetime + + def __post_init__(self) -> None: + """ + Validate the User data. + + This performs the following checks: + + - The name cannot be empty + - The id must be a positive integer + + Raises: + ValueError: If any of the above checks fail. + """ + if not self.name: + msg = "User must have a name" + raise ValueError(msg) + + if self.id < 0: + msg = "User ID must be a positive integer" + raise ValueError(msg) + + def __repr__(self) -> str: + """Return the user's name.""" + return f"User({self.id}:{self.name})" + + +class UserConsumer: + """ + Example consumer. + + This class defines a simple consumer which will interact with a provider + over HTTP to fetch a user's information, and then return an instance of the + `User` class. + """ + + def __init__(self, base_uri: str) -> None: + """ + Initialise the consumer. + + Args: + base_uri: The uri of the provider + """ + self.base_uri = base_uri + + def get_user(self, user_id: int) -> User: + """ + Fetch a user by ID from the server. + + Args: + user_id: The ID of the user to fetch. + + Returns: + The user if found. + + In all other cases, an error dictionary is returned with the key + `error` and the value as the error message. + + Raises: + requests.HTTPError: If the server returns a non-200 response. + """ + uri = f"{self.base_uri}/users/{user_id}" + response = requests.get(uri, timeout=5) + response.raise_for_status() + data: dict[str, Any] = response.json() + return User( + id=data["id"], + name=data["name"], + created_on=datetime.fromisoformat(data["created_on"]), + ) diff --git a/examples/tests/test_00_consumer.py b/examples/tests/test_00_consumer.py new file mode 100644 index 000000000..029550714 --- /dev/null +++ b/examples/tests/test_00_consumer.py @@ -0,0 +1,130 @@ +""" +Test the consumer with Pact. + +This module tests the consumer defined in `src/consumer.py` against a mock +provider. The mock provider is set up by Pact, and is used to ensure that the +consumer is making the expected requests to the provider, and that the provider +is responding with the expected responses. Once these interactions are +validated, the contracts can be published to a Pact Broker. The contracts can +then be used to validate the provider's interactions. +""" + +from __future__ import annotations + +import logging +from http import HTTPStatus +from typing import TYPE_CHECKING, Any, Generator + +import pytest +import requests +from pact import Consumer, Format, Like, Provider +from yarl import URL + +from src.consumer import User, UserConsumer + +if TYPE_CHECKING: + from pathlib import Path + + from pact.pact import Pact + +log = logging.getLogger(__name__) + +MOCK_URL = URL("http://localhost:8080") + + +@pytest.fixture() +def user_consumer() -> UserConsumer: + """ + Returns an instance of the UserConsumer class. + + As we do not want to stand up all of the consumer's dependencies, we direct + the consumer to use Pact's mock provider. This allows us to define what + requests the consumer will make to the provider, and what responses the + provider will return. + """ + return UserConsumer(str(MOCK_URL)) + + +@pytest.fixture(scope="module") +def pact(broker: URL, pact_dir: Path) -> Generator[Pact, Any, None]: + """ + Set up Pact. + + In order to test the consumer in isolation, Pact sets up a mock version of + the provider. This mock provider will expect to receive defined requests + and will respond with defined responses. + + The fixture here simply defines the Consumer and Provide, and sets up the + mock provider. With each test, we define the expected request and response + from the provider as follows: + + ```python + pact.given("UserA exists and is not an admin") \ + .upon_receiving("A request for UserA") \ + .with_request("get", "/users/123") \ + .will_respond_with(200, body=Like(expected)) + ``` + """ + consumer = Consumer("UserConsumer") + pact = consumer.has_pact_with( + Provider("UserProvider"), + pact_dir=pact_dir, + publish_to_broker=True, + # Mock service configuration + host_name=MOCK_URL.host, + port=MOCK_URL.port, + # Broker configuration + broker_base_url=str(broker), + broker_username=broker.user, + broker_password=broker.password, + ) + + pact.start_service() + yield pact + pact.stop_service() + + +def test_get_existing_user(pact: Pact, user_consumer: UserConsumer) -> None: + """ + Test request for an existing user. + + This test defines the expected request and response from the provider. The + provider will be expected to return a response with a status code of 200, + """ + expected: dict[str, Any] = { + "id": Format().integer, + "name": "Verna Hampton", + "created_on": Format().iso_8601_datetime(), + } + + ( + pact.given("user 123 exists") + .upon_receiving("a request for user 123") + .with_request("get", "/users/123") + .will_respond_with(200, body=Like(expected)) + ) + + with pact: + user = user_consumer.get_user(123) + + assert isinstance(user, User) + assert user.name == "Verna Hampton" + + pact.verify() + + +def test_get_unknown_user(pact: Pact, user_consumer: UserConsumer) -> None: + expected = {"error": "User not found"} + + ( + pact.given("user 123 doesn't exist") + .upon_receiving("a request for user 123") + .with_request("get", "/users/123") + .will_respond_with(404, body=Like(expected)) + ) + + with pact: + with pytest.raises(requests.HTTPError) as excinfo: + user_consumer.get_user(123) + assert excinfo.value.response.status_code == HTTPStatus.NOT_FOUND + pact.verify() diff --git a/pyproject.toml b/pyproject.toml index 30e2533b8..bad6b3d48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ test = [ "pytest ~= 7.4", "pytest-cov ~= 4.1", "testcontainers ~= 3.7", + "yarl ~= 1.9", ] dev = [ "pact-python[types]", @@ -104,7 +105,7 @@ 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 +example = "PYTHONPATH=examples pytest examples/ {args}" all = ["lint", "tests"] # Test environment for running unit tests. This automatically tests against all From dd8827acea0722cf271a075495c9988e8ac830c4 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 18 Sep 2023 14:20:39 +1000 Subject: [PATCH 0035/1376] chore(example): migrate fastapi provider example This migrates the Pact FastAPI provider from the old standalone examples, and merges it with the new combined examples. The consumer tests are executed first which publishes the contracts with the broker. The provider test is then executed against the broker to verify compliance with the published contracts. This does, at this stage, create an unusual interdependence between tests which typically should be avoided. I plan to fix this at a later stage. Signed-off-by: JP-Ellis --- examples/fastapi_provider/.flake8 | 4 - examples/fastapi_provider/requirements.txt | 9 -- examples/fastapi_provider/run_pytest.sh | 6 - examples/fastapi_provider/src/provider.py | 25 --- examples/fastapi_provider/tests/__init__.py | 0 examples/fastapi_provider/tests/conftest.py | 27 ---- .../fastapi_provider/tests/pact_provider.py | 46 ------ .../tests/provider/__init__.py | 0 .../tests/provider/test_provider.py | 87 ----------- examples/fastapi_provider/verify_pact.sh | 37 ----- examples/src/fastapi.py | 52 +++++++ examples/tests/test_01_provider_fastapi.py | 143 ++++++++++++++++++ 12 files changed, 195 insertions(+), 241 deletions(-) delete mode 100644 examples/fastapi_provider/.flake8 delete mode 100644 examples/fastapi_provider/requirements.txt delete mode 100755 examples/fastapi_provider/run_pytest.sh delete mode 100644 examples/fastapi_provider/src/provider.py delete mode 100644 examples/fastapi_provider/tests/__init__.py delete mode 100644 examples/fastapi_provider/tests/conftest.py delete mode 100644 examples/fastapi_provider/tests/pact_provider.py delete mode 100644 examples/fastapi_provider/tests/provider/__init__.py delete mode 100644 examples/fastapi_provider/tests/provider/test_provider.py delete mode 100755 examples/fastapi_provider/verify_pact.sh create mode 100644 examples/src/fastapi.py create mode 100644 examples/tests/test_01_provider_fastapi.py diff --git a/examples/fastapi_provider/.flake8 b/examples/fastapi_provider/.flake8 deleted file mode 100644 index 5fb64e780..000000000 --- a/examples/fastapi_provider/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length = 160 -exclude = .direnv/* -max-complexity = 10 diff --git a/examples/fastapi_provider/requirements.txt b/examples/fastapi_provider/requirements.txt deleted file mode 100644 index eea0e8724..000000000 --- a/examples/fastapi_provider/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -fastapi==0.67.0 -pytest==7.0.1; python_version < '3.7' -pytest==7.1.3; python_version >= '3.7' -requests==2.27.1; python_version < '3.7' -requests>=2.28.0; python_version >= '3.7' -uvicorn==0.16.0; python_version < '3.7' -uvicorn>=0.19.0; python_version >= '3.7' -testcontainers==3.7.0; python_version < '3.7' -testcontainers==3.7.1; python_version >= '3.7' diff --git a/examples/fastapi_provider/run_pytest.sh b/examples/fastapi_provider/run_pytest.sh deleted file mode 100755 index 447e0c4b3..000000000 --- a/examples/fastapi_provider/run_pytest.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -o pipefail - -# Unlike in the Flask example, here the FastAPI service is started up as a pytest fixture. This is then including the -# main and pact routes via fastapi_provider.py to run the tests against -pytest --run-broker True --publish-pact 1\ diff --git a/examples/fastapi_provider/src/provider.py b/examples/fastapi_provider/src/provider.py deleted file mode 100644 index 206a87da8..000000000 --- a/examples/fastapi_provider/src/provider.py +++ /dev/null @@ -1,25 +0,0 @@ -import logging - -from fastapi import FastAPI, HTTPException, APIRouter -from fastapi.logger import logger - -fakedb = {} # Use a simple dict to represent a database - -logger.setLevel(logging.DEBUG) -router = APIRouter() -app = FastAPI() - - -@app.get("/users/{name}") -def get_user_by_name(name: str): - """Handle requests to retrieve a single user from the simulated database. - - :param name: Name of the user to "search for" - :return: The user data if found, HTTP 404 if not - """ - user_data = fakedb.get(name) - if not user_data: - logger.error(f"GET user for: '{name}', HTTP 404 not found") - raise HTTPException(status_code=404, detail="User not found") - logger.error(f"GET user for: '{name}', returning: {user_data}") - return user_data diff --git a/examples/fastapi_provider/tests/__init__.py b/examples/fastapi_provider/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/fastapi_provider/tests/conftest.py b/examples/fastapi_provider/tests/conftest.py deleted file mode 100644 index 6e3176f20..000000000 --- a/examples/fastapi_provider/tests/conftest.py +++ /dev/null @@ -1,27 +0,0 @@ -import sys -from multiprocessing import Process - -import pytest - -from .pact_provider import run_server - -# Load in the fixtures from common/sharedfixtures.py -sys.path.append("../common") - -pytest_plugins = [ - "sharedfixtures", -] - - -@pytest.fixture(scope="module") -def server(): - proc = Process(target=run_server, args=(), daemon=True) - proc.start() - yield proc - - # Cleanup after test - if sys.version_info >= (3, 7): - # multiprocessing.kill is new in 3.7 - proc.kill() - else: - proc.terminate() diff --git a/examples/fastapi_provider/tests/pact_provider.py b/examples/fastapi_provider/tests/pact_provider.py deleted file mode 100644 index d778cd433..000000000 --- a/examples/fastapi_provider/tests/pact_provider.py +++ /dev/null @@ -1,46 +0,0 @@ -import uvicorn - -from fastapi import APIRouter -from pydantic import BaseModel - -from src.provider import app, fakedb, router as main_router - -pact_router = APIRouter() - - -class ProviderState(BaseModel): - state: str # noqa: E999 - - -@pact_router.post("/_pact/provider_states") -async def provider_states(provider_state: ProviderState): - mapping = { - "UserA does not exist": setup_no_user_a, - "UserA exists and is not an administrator": setup_user_a_nonadmin, - } - mapping[provider_state.state]() - - return {"result": mapping[provider_state.state]} - - -# Make sure the app includes both routers. This needs to be done after the -# declaration of the provider_states -app.include_router(main_router) -app.include_router(pact_router) - - -def run_server(): - uvicorn.run(app) - - -def setup_no_user_a(): - if "UserA" in fakedb: - del fakedb["UserA"] - - -def setup_user_a_nonadmin(): - id = "00000000-0000-4000-a000-000000000000" - some_date = "2016-12-15T20:16:01" - ip_address = "198.0.0.1" - - fakedb["UserA"] = {"name": "UserA", "id": id, "created_on": some_date, "ip_address": ip_address, "admin": False} diff --git a/examples/fastapi_provider/tests/provider/__init__.py b/examples/fastapi_provider/tests/provider/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/fastapi_provider/tests/provider/test_provider.py b/examples/fastapi_provider/tests/provider/test_provider.py deleted file mode 100644 index bc903b481..000000000 --- a/examples/fastapi_provider/tests/provider/test_provider.py +++ /dev/null @@ -1,87 +0,0 @@ -"""pact test for user service client""" -import logging - -import pytest - -from pact import Verifier - -log = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -# For the purposes of this example, the broker is started up as a fixture defined -# in conftest.py. For normal usage this would be self-hosted or using PactFlow. -PACT_BROKER_URL = "http://localhost" -PACT_BROKER_USERNAME = "pactbroker" -PACT_BROKER_PASSWORD = "pactbroker" - -# For the purposes of this example, the FastAPI provider will be started up as -# a fixture in conftest.py ("server"). Alternatives could be, for example -# running a Docker container with a database of test data configured. -# This is the "real" provider to verify against. -PROVIDER_HOST = "127.0.0.1" -PROVIDER_PORT = 8000 -PROVIDER_URL = f"http://{PROVIDER_HOST}:{PROVIDER_PORT}" - - -def test_success(): - pass - - -@pytest.fixture -def broker_opts(): - return { - "broker_username": PACT_BROKER_USERNAME, - "broker_password": PACT_BROKER_PASSWORD, - "broker_url": PACT_BROKER_URL, - "publish_version": "3", - "publish_verification_results": True, - } - - -def test_user_service_provider_against_broker(server, broker_opts): - verifier = Verifier(provider="UserService", provider_base_url=PROVIDER_URL) - - # Request all Pact(s) from the Pact Broker to verify this Provider against. - # In the Pact Broker logs, this corresponds to the following entry: - # PactBroker::Api::Resources::ProviderPactsForVerification -- Fetching pacts for verification by UserService -- {:provider_name=>"UserService", :params=>{}} - success, logs = verifier.verify_with_broker( - **broker_opts, - verbose=True, - provider_states_setup_url=f"{PROVIDER_URL}/_pact/provider_states", - enable_pending=False, - ) - # If publish_verification_results is set to True, the results will be - # published to the Pact Broker. - # In the Pact Broker logs, this corresponds to the following entry: - # PactBroker::Verifications::Service -- Creating verification 200 for \ - # pact_version_sha=c8568cbb30d2e3933b2df4d6e1248b3d37f3be34 -- \ - # {"success"=>true, "providerApplicationVersion"=>"3", "wip"=>false, \ - # "pending"=>"true"} - - # Note: - # If "successful", then the return code here will be 0 - # This can still be 0 and so PASS if a Pact verification FAILS, as long as - # it has not resulted in a REGRESSION of an already verified interaction. - # See https://docs.pact.io/pact_broker/advanced_topics/pending_pacts/ for - # more details. - assert success == 0 - - -def test_user_service_provider_against_pact(server): - verifier = Verifier(provider="UserService", provider_base_url=PROVIDER_URL) - - # Rather than requesting the Pact interactions from the Pact Broker, this - # will perform the verification based on the Pact file locally. - # - # Because there is no way of knowing the previous state of an interaction, - # if it has been successful in the past (since this is what the Pact Broker - # is for), if the verification of an interaction fails then the success - # result will be != 0, and so the test will FAIL. - output, _ = verifier.verify_pacts( - "../pacts/userserviceclient-userservice.json", - verbose=False, - provider_states_setup_url="{}/_pact/provider_states".format(PROVIDER_URL), - ) - - assert output == 0 diff --git a/examples/fastapi_provider/verify_pact.sh b/examples/fastapi_provider/verify_pact.sh deleted file mode 100755 index 356b9f739..000000000 --- a/examples/fastapi_provider/verify_pact.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -o pipefail - -# Run the FastAPI server, using the pact_provider.py as the app to be able to -# inject the provider_states endpoint -uvicorn tests.pact_provider:app & &>/dev/null -FASTAPI_PID=$! - -# Make sure the FastAPI server is stopped when finished to avoid blocking the port -function teardown { - echo "Tearing down FastAPI server ${FASTAPI_PID}" - kill -9 $FASTAPI_PID -} -trap teardown EXIT - -# Wait a little in case FastAPI isn't quite ready -sleep 1 - -VERSION=$1 -if [ -x $VERSION ]; -then - echo "Validating provider locally" - - pact-verifier --provider-base-url=http://localhost:8000 \ - --provider-states-setup-url=http://localhost:8000/_pact/provider_states \ - ../pacts/userserviceclient-userservice.json -else - echo "Validating against Pact Broker" - - pact-verifier --provider-base-url=http://localhost:8000 \ - --provider-app-version $VERSION \ - --pact-url="http://127.0.0.1/pacts/provider/UserService/consumer/UserServiceClient/latest" \ - --pact-broker-username pactbroker \ - --pact-broker-password pactbroker \ - --publish-verification-results \ - --provider-states-setup-url=http://localhost:8000/_pact/provider_states -fi diff --git a/examples/src/fastapi.py b/examples/src/fastapi.py new file mode 100644 index 000000000..2589fcd3d --- /dev/null +++ b/examples/src/fastapi.py @@ -0,0 +1,52 @@ +""" +FastAPI provider example. + +This modules defines a simple +[provider](https://docs.pact.io/getting_started/terminology#service-provider) +with Pact. As Pact is a consumer-driven framework, the consumer defines the +contract which the provider must then satisfy. + +The provider is the application which receives requests from another service +(the consumer) and returns a response. In this example, we have a simple +endpoint which returns a user's information from a (fake) database. + +Note that the code in this module is agnostic of Pact. The `pact-python` +dependency only appears in the tests. This is because the consumer is not +concerned with Pact, only the tests are. +""" + +from __future__ import annotations + +from typing import Any + +from fastapi import FastAPI +from fastapi.responses import JSONResponse + +app = FastAPI() + +""" +As this is a simple example, we'll use a simple dict to represent a database. +This would be replaced with a real database in a real application. + +When testing the provider in a real application, the calls to the database +would be mocked out to avoid the need for a real database. An example of this +can be found in the test suite. +""" +FAKE_DB: dict[int, dict[str, Any]] = {} + + +@app.get("/users/{uid}") +async def get_user_by_id(uid: int) -> dict[str, Any]: + """ + Fetch a user by their ID. + + Args: + uid: The ID of the user to fetch + + Returns: + The user data if found, HTTP 404 if not + """ + user = FAKE_DB.get(uid) + if not user: + return JSONResponse(status_code=404, content={"error": "User not found"}) + return JSONResponse(status_code=200, content=user) diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/tests/test_01_provider_fastapi.py new file mode 100644 index 000000000..ce05964f0 --- /dev/null +++ b/examples/tests/test_01_provider_fastapi.py @@ -0,0 +1,143 @@ +""" +Test the FastAPI provider with Pact. + +This module tests the FastAPI provider defined in `src/fastapi.py` against the +mock consumer. The mock consumer is set up by Pact and will replay the requests +defined by the consumers. Pact will then validate that the provider responds +with the expected responses. + +The provider will be expected to be in a given state in order to respond to +certain requests. For example, when fetching a user's information, the provider +will need to have a user with the given ID in the database. In order to avoid +side effects, the provider's database calls are mocked out using functionalities +from `unittest.mock`. + +In order to set the provider into the correct state, this test module defines an +additional endpoint on the provider, in this case `/_pact/provider_states`. +Calls to this endpoint mock the relevant database calls to set the provider into +the correct state. +""" + +from __future__ import annotations + +from multiprocessing import Process +from typing import Any, Generator +from unittest.mock import MagicMock + +import pytest +import uvicorn +from pact import Verifier +from pydantic import BaseModel +from yarl import URL + +from src.fastapi import app + +PROVIDER_URL = URL("http://localhost:8080") + + +class ProviderState(BaseModel): + """Define the provider state.""" + + consumer: str + state: str + + +@app.post("/_pact/provider_states") +async def mock_pact_provider_states(state: ProviderState) -> dict[str, str | None]: + """ + Define the provider state. + + For Pact to be able to correctly tests compliance with the contract, the + internal state of the provider needs to be set up correctly. Naively, this + would be achieved by setting up the database with the correct data for the + test, but this can be slow and error-prone. Instead this is best achieved by + mocking the relevant calls to the database so as to avoid any side effects. + + For Pact to be able to correctly get the provider into the correct state, + this function is used to define an additional endpoint on the provider. This + endpoint is called by Pact before each test to ensure that the provider is + in the correct state. + """ + mapping = { + "user 123 doesn't exist": mock_user_123_doesnt_exist, + "user 123 exists": mock_user_123_exists, + } + return {"result": mapping[state.state]()} + + +def run_server() -> None: + """ + Run the FastAPI server. + + This function is required to run the FastAPI server in a separate process. A + lambda cannot be used as the target of a `multiprocessing.Process` as it + cannot be pickled. + """ + uvicorn.run(app, host=PROVIDER_URL.host, port=PROVIDER_URL.port) + + +@pytest.fixture(scope="module") +def verifier() -> Generator[Verifier, Any, None]: + """Set up the Pact verifier.""" + proc = Process(target=run_server, daemon=True) + verifier = Verifier( + provider="UserProvider", + provider_base_url=str(PROVIDER_URL), + ) + proc.start() + yield verifier + proc.kill() + + +def mock_user_123_doesnt_exist() -> None: + """Mock the database for the user 123 doesn't exist state.""" + import src.fastapi + + src.fastapi.FAKE_DB = MagicMock() + src.fastapi.FAKE_DB.get.return_value = None + + +def mock_user_123_exists() -> None: + """ + Mock the database for the user 123 exists state. + + You may notice that the return value here differs from the consumer's + expected response. This is because the consumer's expected response is + guided by what the consumer users. + + By using consumer-driven contracts and testing the provider against the + consumer's contract, we can ensure that the provider is only providing what + """ + import src.fastapi + + src.fastapi.FAKE_DB = MagicMock() + src.fastapi.FAKE_DB.get.return_value = { + "id": 123, + "name": "Verna Hampton", + "created_on": "2016-12-15T20:16:01", + "ip_address": "10.1.2.3", + "hobbies": ["hiking", "swimming"], + "admin": False, + } + + +def test_against_broker(broker: URL, verifier: Verifier) -> None: + """ + Test the provider against the broker. + + The broker will be used to retrieve the contract, and the provider will be + tested against the contract. + + As Pact is a consumer-driven, the provider is tested against the contract + defined by the consumer. The consumer defines the expected request to and + response from the provider. + + For an example of the consumer's contract, see the consumer's tests. + """ + code, _ = verifier.verify_with_broker( + broker_url=str(broker), + published_verification_results=True, + provider_states_setup_url=str(PROVIDER_URL / "_pact" / "provider_states"), + ) + + assert code == 0 From 9488f0ef2c77a88794741f3fbc0f6297b481765b Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 18 Sep 2023 15:46:38 +1000 Subject: [PATCH 0036/1376] chore(example): migrate flask provider example Following the changes to the FastAPI example, this migrates the Flask provider example to the new structure. The example relies on the consumer having published contracts, and the flask provider is verified against those contracts. Signed-off-by: JP-Ellis --- examples/flask_provider/.flake8 | 4 - examples/flask_provider/requirements.txt | 10 -- examples/flask_provider/run_pytest.sh | 20 --- examples/flask_provider/src/provider.py | 25 ---- examples/flask_provider/tests/conftest.py | 8 -- .../flask_provider/tests/pact_provider.py | 48 ------- .../tests/provider/test_provider.py | 83 ----------- examples/flask_provider/verify_pact.sh | 39 ----- examples/src/flask.py | 51 +++++++ examples/tests/test_01_provider_flask.py | 136 ++++++++++++++++++ pyproject.toml | 2 +- 11 files changed, 188 insertions(+), 238 deletions(-) delete mode 100644 examples/flask_provider/.flake8 delete mode 100644 examples/flask_provider/requirements.txt delete mode 100755 examples/flask_provider/run_pytest.sh delete mode 100644 examples/flask_provider/src/provider.py delete mode 100644 examples/flask_provider/tests/conftest.py delete mode 100644 examples/flask_provider/tests/pact_provider.py delete mode 100644 examples/flask_provider/tests/provider/test_provider.py delete mode 100755 examples/flask_provider/verify_pact.sh create mode 100644 examples/src/flask.py create mode 100644 examples/tests/test_01_provider_flask.py diff --git a/examples/flask_provider/.flake8 b/examples/flask_provider/.flake8 deleted file mode 100644 index 5fb64e780..000000000 --- a/examples/flask_provider/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length = 160 -exclude = .direnv/* -max-complexity = 10 diff --git a/examples/flask_provider/requirements.txt b/examples/flask_provider/requirements.txt deleted file mode 100644 index b3f83b690..000000000 --- a/examples/flask_provider/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -Flask==2.0.3; python_version < '3.7' -Flask==2.2.5; python_version >= '3.7' -pytest==7.0.1; python_version < '3.7' -pytest==7.1.3; python_version >= '3.7' -requests==2.27.1; python_version < '3.7' -requests>=2.28.0; python_version >= '3.7' -testcontainers==3.7.0; python_version < '3.7' -testcontainers==3.7.1; python_version >= '3.7' -markupsafe==2.0.1; python_version < '3.7' -markupsafe==2.1.2; python_version >= '3.7' diff --git a/examples/flask_provider/run_pytest.sh b/examples/flask_provider/run_pytest.sh deleted file mode 100755 index 37aa783a0..000000000 --- a/examples/flask_provider/run_pytest.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -set -o pipefail - -# Run the Flask server, using the pact_provider.py as the app to be able to -# inject the provider_states endpoint -FLASK_APP=tests/pact_provider.py python -m flask run -p 5001 & -FLASK_PID=$! - -# Make sure the Flask server is stopped when finished to avoid blocking the port -function teardown { - echo "Tearing down Flask server: ${FLASK_PID}" - kill -9 $FLASK_PID -} -trap teardown EXIT - -# Wait a little in case Flask isn't quite ready -sleep 1 - -# Now run the tests -pytest tests --run-broker True --publish-pact 1 diff --git a/examples/flask_provider/src/provider.py b/examples/flask_provider/src/provider.py deleted file mode 100644 index abdb4549f..000000000 --- a/examples/flask_provider/src/provider.py +++ /dev/null @@ -1,25 +0,0 @@ -from flask import Flask, abort, jsonify - -fakedb = {} # Use a simple dict to represent a database - -app = Flask(__name__) - - -@app.route("/users/") -def get_user_by_name(name: str): - """Handle requests to retrieve a single user from the simulated database. - - :param name: Name of the user to "search for" - :return: The user data if found, None (HTTP 404) if not - """ - user_data = fakedb.get(name) - if not user_data: - app.logger.debug(f"GET user for: '{name}', HTTP 404 not found") - abort(404) - response = jsonify(**user_data) - app.logger.debug(f"GET user for: '{name}', returning: {response.data}") - return response - - -if __name__ == "__main__": - app.run(debug=True, port=5001) diff --git a/examples/flask_provider/tests/conftest.py b/examples/flask_provider/tests/conftest.py deleted file mode 100644 index 90ac63b9a..000000000 --- a/examples/flask_provider/tests/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys - -# Load in the fixtures from common/sharedfixtures.py -sys.path.append("../common") - -pytest_plugins = [ - "sharedfixtures", -] diff --git a/examples/flask_provider/tests/pact_provider.py b/examples/flask_provider/tests/pact_provider.py deleted file mode 100644 index 0cb967486..000000000 --- a/examples/flask_provider/tests/pact_provider.py +++ /dev/null @@ -1,48 +0,0 @@ -"""additional endpoints to facilitate provider_states""" - -from flask import jsonify, request - -from src.provider import app, fakedb - - -@app.route("/_pact/provider_states", methods=["POST"]) -def provider_states(): - """Implement the "functionality" to change the state, to prepare for a test. - - When a Pact interaction is verified, it provides the "given" part of the - description from the Consumer in the X_PACT_PROVIDER_STATES header. - This can then be used to perform some operations on a database for example, - so that the actual request can be performed and respond as expected. - See: https://docs.pact.io/getting_started/provider_states - - This provider_states endpoint is deemed test only, and generally should not - be available once deployed to an environment. It would represent both a - potential data loss risk, as well as a security risk. - - As such, when running the Provider to test against, this is defined as the - FLASK_APP to run, adding this additional route to the app while keeping the - source separate. - """ - mapping = { - "UserA does not exist": setup_no_user_a, - "UserA exists and is not an administrator": setup_user_a_nonadmin, - } - mapping[request.json["state"]]() - return jsonify({"result": request.json["state"]}) - - -def setup_no_user_a(): - if "UserA" in fakedb: - del fakedb["UserA"] - - -def setup_user_a_nonadmin(): - id = "00000000-0000-4000-a000-000000000000" - some_date = "2016-12-15T20:16:01" - ip_address = "198.0.0.1" - - fakedb["UserA"] = {"name": "UserA", "id": id, "created_on": some_date, "ip_address": ip_address, "admin": False} - - -if __name__ == "__main__": - app.run(debug=True, port=5001) diff --git a/examples/flask_provider/tests/provider/test_provider.py b/examples/flask_provider/tests/provider/test_provider.py deleted file mode 100644 index f87257785..000000000 --- a/examples/flask_provider/tests/provider/test_provider.py +++ /dev/null @@ -1,83 +0,0 @@ -"""pact test for user service provider""" - -import logging - -import pytest - -from pact import Verifier - -log = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -# For the purposes of this example, the broker is started up as a fixture defined -# in conftest.py. For normal usage this would be self-hosted or using PactFlow. -PACT_BROKER_URL = "http://localhost" -PACT_BROKER_USERNAME = "pactbroker" -PACT_BROKER_PASSWORD = "pactbroker" - -# For the purposes of this example, the Flask provider will be started up as part -# of run_pytest.sh when running the tests. Alternatives could be, for example -# running a Docker container with a database of test data configured. -# This is the "real" provider to verify against. -PROVIDER_HOST = "localhost" -PROVIDER_PORT = 5001 -PROVIDER_URL = f"http://{PROVIDER_HOST}:{PROVIDER_PORT}" - - -@pytest.fixture -def broker_opts(): - return { - "broker_username": PACT_BROKER_USERNAME, - "broker_password": PACT_BROKER_PASSWORD, - "broker_url": PACT_BROKER_URL, - "publish_version": "3", - "publish_verification_results": True, - } - - -def test_user_service_provider_against_broker(broker_opts): - verifier = Verifier(provider="UserService", provider_base_url=PROVIDER_URL) - - # Request all Pact(s) from the Pact Broker to verify this Provider against. - # In the Pact Broker logs, this corresponds to the following entry: - # PactBroker::Api::Resources::ProviderPactsForVerification -- Fetching pacts for verification by UserService -- {:provider_name=>"UserService", :params=>{}} - success, logs = verifier.verify_with_broker( - **broker_opts, - verbose=True, - provider_states_setup_url=f"{PROVIDER_URL}/_pact/provider_states", - enable_pending=False, - ) - # If publish_verification_results is set to True, the results will be - # published to the Pact Broker. - # In the Pact Broker logs, this corresponds to the following entry: - # PactBroker::Verifications::Service -- Creating verification 200 for \ - # pact_version_sha=c8568cbb30d2e3933b2df4d6e1248b3d37f3be34 -- \ - # {"success"=>true, "providerApplicationVersion"=>"3", "wip"=>false, \ - # "pending"=>"true"} - - # Note: - # If "successful", then the return code here will be 0 - # This can still be 0 and so PASS if a Pact verification FAILS, as long as - # it has not resulted in a REGRESSION of an already verified interaction. - # See https://docs.pact.io/pact_broker/advanced_topics/pending_pacts/ for - # more details. - assert success == 0 - - -def test_user_service_provider_against_pact(): - verifier = Verifier(provider="UserService", provider_base_url=PROVIDER_URL) - - # Rather than requesting the Pact interactions from the Pact Broker, this - # will perform the verification based on the Pact file locally. - # - # Because there is no way of knowing the previous state of an interaction, - # if it has been successful in the past (since this is what the Pact Broker - # is for), if the verification of an interaction fails then the success - # result will be != 0, and so the test will FAIL. - output, _ = verifier.verify_pacts( - "../pacts/userserviceclient-userservice.json", - verbose=False, - provider_states_setup_url="{}/_pact/provider_states".format(PROVIDER_URL), - ) - - assert output == 0 diff --git a/examples/flask_provider/verify_pact.sh b/examples/flask_provider/verify_pact.sh deleted file mode 100755 index f70629699..000000000 --- a/examples/flask_provider/verify_pact.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -set -o pipefail - -# Run the Flask server, using the pact_provider.py as the app to be able to -# inject the provider_states endpoint -FLASK_APP=tests/pact_provider.py python -m flask run -p 5001 & -FLASK_PID=$! - -# Make sure the Flask server is stopped when finished to avoid blocking the port -function teardown { - echo "Tearing down Flask server: ${FLASK_PID}" - kill -9 $FLASK_PID -} -trap teardown EXIT - -# Wait a little in case Flask isn't quite ready -sleep 1 - -VERSION=$1 -if [ -z "$VERSION" ]; -then - echo "Validating provider locally" - - pact-verifier \ - --provider-base-url=http://localhost:5001 \ - --provider-states-setup-url=http://localhost:5001/_pact/provider_states \ - ../pacts/userserviceclient-userservice.json -else - echo "Validating against Pact Broker" - - pact-verifier \ - --provider-base-url=http://localhost:5001 \ - --provider-app-version $VERSION \ - --pact-url="http://127.0.0.1/pacts/provider/UserService/consumer/UserServiceClient/latest" \ - --pact-broker-username pactbroker \ - --pact-broker-password pactbroker \ - --publish-verification-results \ - --provider-states-setup-url=http://localhost:5001/_pact/provider_states -fi diff --git a/examples/src/flask.py b/examples/src/flask.py new file mode 100644 index 000000000..fd5095e03 --- /dev/null +++ b/examples/src/flask.py @@ -0,0 +1,51 @@ +""" +Flask provider example. + +This modules defines a simple +[provider](https://docs.pact.io/getting_started/terminology#service-provider) +with Pact. As Pact is a consumer-driven framework, the consumer defines the +contract which the provider must then satisfy. + +The provider is the application which receives requests from another service +(the consumer) and returns a response. In this example, we have a simple +endpoint which returns a user's information from a (fake) database. + +Note that the code in this module is agnostic of Pact. The `pact-python` +dependency only appears in the tests. This is because the consumer is not +concerned with Pact, only the tests are. +""" + +from __future__ import annotations + +from typing import Any + +from flask import Flask + +app = Flask(__name__) + +""" +As this is a simple example, we'll use a simple dict to represent a database. +This would be replaced with a real database in a real application. + +When testing the provider in a real application, the calls to the database +would be mocked out to avoid the need for a real database. An example of this +can be found in the test suite. +""" +FAKE_DB: dict[int, dict[str, Any]] = {} + + +@app.route("/users/") +def get_user_by_id(uid: int) -> dict[str, Any] | tuple[dict[str, Any], int]: + """ + Fetch a user by their ID. + + Args: + uid: The ID of the user to fetch + + Returns: + The user data if found, HTTP 404 if not + """ + user = FAKE_DB.get(uid) + if not user: + return {"error": "User not found"}, 404 + return user diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py new file mode 100644 index 000000000..1aff011e3 --- /dev/null +++ b/examples/tests/test_01_provider_flask.py @@ -0,0 +1,136 @@ +""" +Test the Flask provider with Pact. + +This module tests the Flask provider defined in `src/flask.py` against the mock +consumer. The mock consumer is set up by Pact and will replay the requests +defined by the consumers. Pact will then validate that the provider responds +with the expected responses. + +The provider will be expected to be in a given state in order to respond to +certain requests. For example, when fetching a user's information, the provider +will need to have a user with the given ID in the database. In order to avoid +side effects, the provider's database calls are mocked out using functionalities +from `unittest.mock`. + +In order to set the provider into the correct state, this test module defines an +additional endpoint on the provider, in this case `/_pact/provider_states`. +Calls to this endpoint mock the relevant database calls to set the provider into +the correct state. +""" + + +from __future__ import annotations + +from multiprocessing import Process +from typing import Any, Generator +from unittest.mock import MagicMock + +import pytest +from flask import request +from pact import Verifier +from yarl import URL + +from src.flask import app + +PROVIDER_URL = URL("http://localhost:8080") + + +@app.route("/_pact/provider_states", methods=["POST"]) +async def mock_pact_provider_states() -> dict[str, str | None]: + """ + Define the provider state. + + For Pact to be able to correctly tests compliance with the contract, the + internal state of the provider needs to be set up correctly. Naively, this + would be achieved by setting up the database with the correct data for the + test, but this can be slow and error-prone. Instead this is best achieved by + mocking the relevant calls to the database so as to avoid any side effects. + + For Pact to be able to correctly get the provider into the correct state, + this function is used to define an additional endpoint on the provider. This + endpoint is called by Pact before each test to ensure that the provider is + in the correct state. + """ + mapping = { + "user 123 doesn't exist": mock_user_123_doesnt_exist, + "user 123 exists": mock_user_123_exists, + } + return {"result": mapping[request.json["state"]]()} + + +def run_server() -> None: + """ + Run the Flask server. + + This function is required to run the Flask server in a separate process. A + lambda cannot be used as the target of a `multiprocessing.Process` as it + cannot be pickled. + """ + app.run(host=PROVIDER_URL.host, port=PROVIDER_URL.port) + + +@pytest.fixture(scope="module") +def verifier() -> Generator[Verifier, Any, None]: + """Set up the Pact verifier.""" + proc = Process(target=run_server, daemon=True) + verifier = Verifier( + provider="UserProvider", + provider_base_url=str(PROVIDER_URL), + ) + proc.start() + yield verifier + proc.kill() + + +def mock_user_123_doesnt_exist() -> None: + """Mock the database for the user 123 doesn't exist state.""" + import src.flask + + src.flask.FAKE_DB = MagicMock() + src.flask.FAKE_DB.get.return_value = None + + +def mock_user_123_exists() -> None: + """ + Mock the database for the user 123 exists state. + + You may notice that the return value here differs from the consumer's + expected response. This is because the consumer's expected response is + guided by what the consumer users. + + By using consumer-driven contracts and testing the provider against the + consumer's contract, we can ensure that the provider is only providing what + """ + import src.flask + + src.flask.FAKE_DB = MagicMock() + src.flask.FAKE_DB.get.return_value = { + "id": 123, + "name": "Verna Hampton", + "created_on": "2016-12-15T20:16:01", + "ip_address": "10.1.2.3", + "hobbies": ["hiking", "swimming"], + "admin": False, + } + + +def test_against_broker(broker: URL, verifier: Verifier) -> None: + """ + Test the provider against the broker. + + The broker will be used to retrieve the contract, and the provider will be + tested against the contract. + + As Pact is a consumer-driven, the provider is tested against the contract + defined by the consumer. The consumer defines the expected request to and + response from the provider. + + For an example of the consumer's contract, see the consumer's tests. + """ + code, _ = verifier.verify_with_broker( + broker_url=str(broker), + published_verification_results=True, + provider_states_setup_url=str(PROVIDER_URL / "_pact" / "provider_states"), + ) + + assert code == 0 diff --git a/pyproject.toml b/pyproject.toml index bad6b3d48..5fb225b05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ pact-verifier = "pact.cli.verify:main" types = ["mypy ~= 1.1", "types-requests ~= 2.31"] test = [ "coverage[toml] ~= 7.3", + "flask[async] ~= 2.3", "httpx ~= 0.24", "mock ~= 5.1", "pytest ~= 7.4", @@ -61,7 +62,6 @@ dev = [ "pact-python[types]", "pact-python[test]", "black ~= 23.7", - "flask ~= 2.3", "ruff ~= 0.0", ] From aa0f07e11ed1d725fa80c9b35c46d1fbba037dd5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Sep 2023 13:20:06 +1000 Subject: [PATCH 0037/1376] chore(example): update readme Update the README for the examples to match the new structure of the examples. Signed-off-by: JP-Ellis --- examples/README.md | 344 +++++++++++++++++++-------------------------- 1 file changed, 143 insertions(+), 201 deletions(-) diff --git a/examples/README.md b/examples/README.md index 885cdc6dc..4520e8fb7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,220 +1,162 @@ # Examples -## Table of Contents +This directory contains an end-to-end example of using Pact in Python. While +this document and the documentation within the examples themselves are intended +to be mostly self-contained, it is highly recommended that you read the [Pact +Documentation](https://docs.pact.io/) as well. - * [Overview](#overview) - * [broker](#broker) - * [common](#common) - * [consumer](#consumer) - * [flask_provider](#flask_provider) - * [fastapi_provider](#fastapi_provider) - * [message](#message) - * [pacts](#pacts) +Assuming you have [hatch](https://hatch.pypa.io/latest/) installed, the example +suite can be executed with: -## Overview - -Here you can find examples of how to use Pact using the python language. You can find more of an overview on Pact in the -[Pact Introduction]. - -Examples are given of both the [Consumer] and [Provider], this does not mean however that you must use python for both. -Different languages can be mixed and matched as required. - -In these examples, `1` is just used to meet the need of having *some* [Consumer] or [Provider] version. In reality, you -will generally want to use something more complicated and automated. Guidelines and best practices are available in the -[Versioning in the Pact Broker] - -## broker - -The [Pact Broker] stores [Pact file]s and [Pact verification] results. It is used here for the [consumer](#consumer), -[flask_provider](#flask-provider) and [message](#message) tests. - -### Running - -These examples run the [Pact Broker] as part of the tests when specified. It can be run outside the tests as well by -performing the following command from a separate terminal in the `examples/broker` folder: -```bash -docker-compose up +```sh +hatch run example ``` -You should then be able to open a browser and navigate to http://localhost where you will initially be able to see the -default Example App/Example API Pact. - -Running the [Pact Broker] outside the tests will mean you are able to then see the [Pact file]s submitted to the -[Pact Broker] as the various tests are performed. - -## common - -To avoid needing to duplicate certain fixtures, such as starting up a docker based Pact broker (to demonstrate how the -test process could work), the shared fixtures used by the pytests have all been placed into a single location.] -This means it is easier to see the relevant code for the example without having to go through the boilerplate fixtures. -See [Requiring/Loading plugins in a test module or conftest file] for further details of this approach. - -## consumer - -Pact is consumer-driven, which means first the contracts are created. These Pact contracts are generated during -execution of the consumer tests. - -### Running - -When the tests are run, the "minimum" is to generate the Pact contract JSON, additional options are available. The -following commands can be run from the `examples/consumer` folder: - -- Install any necessary dependencies: - ```bash - pip install -r requirements.txt - ``` -- To startup the broker, run the tests, and publish the results to the broker: - ```bash - pytest --run-broker True --publish-pact 1 - ``` -- Alternatively the same can be performed with the following command, which is called from a `make consumer`: - ```bash - ./run_pytest.sh - ``` -- To run the tests, and publish the results to the broker which is already running: - ```bash - pytest --publish-pact 1 - ``` -- To just run the tests: - ```bash - pytest - ``` - -### Output - -The following file(s) will be created when the tests are run: - -| Filename | Contents | -|---------------------------------------------| ----------| -| consumer/pact-mock-service.log | All interactions with the mock provider such as expected interactions, requests, and interaction verifications. | -| consumer/userserviceclient-userservice.json | This contains the Pact interactions between the `UserServiceClient` and `UserService`, as defined in the tests. The naming being derived from the named Pacticipants: `Consumer("UserServiceClient")` and `Provider("UserService")` | - -## flask_provider - -The Flask [Provider] example consists of a basic Flask app, with a single endpoint route. -This implements the service expected by the [consumer](#consumer). - -Functionally, this provides the same service and tests as the [fastapi_provider](#fastapi_provider). Both are included to -demonstrate how Pact can be used in different environments with different technology stacks and approaches. - -The [Provider] side is responsible for performing the tests to verify if it is compliant with the [Pact file] contracts -associated with it. - -As such, the tests use the pact-python Verifier to perform this verification. Two approaches are demonstrated: -- Testing against the [Pact broker]. Generally this is the preferred approach, see information on [Sharing Pacts]. -- Testing against the [Pact file] directly. If no [Pact broker] is available you can verify against a static [Pact file]. +The code within the examples is intended to be well documented and you are +encouraged to look through the code as well (or submit a PR if anything is +unclear!). -### Running +## Overview -To avoid package version conflicts with different applications, it is recommended to run these tests from a -[Virtual Environment] +Pact is a contract testing tool. Contract testing is a way to ensure that +services (such as an API provider and a client) can communicate with each other. +This example focuses on HTTP interactions, but Pact can be used to test more +general interactions as well such as through message queues. -The following commands can be run from within your [Virtual Environment], in the `examples/flask_provider`. +An interaction between a HTTP client (the _consumer_) and a server (the +_provider_) would typically look like this: -To perform the python tests: -```bash -pip install -r requirements.txt # Install the dependencies for the Flask example -pip install -e ../../ # Using setup.py in the pact-python root, install any pact dependencies and pact-python -./run_pytest.sh # Wrapper script to first run Flask, and then run the tests -``` +
-To perform verification using CLI to verify the [Pact file] against the Flask [Provider] instead of the python tests: -```bash -pip install -r requirements.txt # Install the dependencies for the Flask example -./verify_pact.sh # Wrapper script to first run Flask, and then use `pact-verifier` to verify locally +```mermaid +sequenceDiagram + participant Consumer + participant Provider + Consumer ->> Provider: GET /users/123 + Provider ->> Consumer: 200 OK + Consumer ->> Provider: GET /users/999 + Provider ->> Consumer: 404 Not Found ``` -To perform verification using CLI, but verifying the [Pact file] previously provided by a [Consumer], and publish the -results. This example requires that the [Pact broker] is already running, and the [Consumer] tests have been published -already, described in the [consumer](#consumer) section above. -```bash -pip install -r requirements.txt # Install the dependencies for the Flask example -./verify_pact.sh 1 # Wrapper script to first run Flask, and then use `pact-verifier` to verify and publish +
+ +To test this interaction naively would require both the consumer and provider to +be running at the same time. While this is straightforward in the above example, +this quickly becomes impractical as the number of interactions grows between +many microservices. Pact solves this by allowing the consumer and provider to be +tested independently. + +Pact achieves this be mocking the other side of the interaction: + +
+ +```mermaid +sequenceDiagram + box Consumer Side + participant Consumer + participant P1 as Pact + end + box Provider Side + participant P2 as Pact + participant Provider + end + Consumer->>P1: GET /users/123 + P1->>Consumer: 200 OK + Consumer->>P1: GET /users/999 + P1->>Consumer: 404 Not Found + + P1--)P2: Pact Broker + + P2->>Provider: GET /users/123 + Provider->>P2: 200 OK + P2->>Provider: GET /users/999 + Provider->>P2: 404 Not Found ``` -These examples demonstrate by first launching Flask via a `python -m flask run`, you may prefer to start Flask using an -`app.run()` call in the python code instead, see [How to Run a Flask Application]. Additionally for tests, you may want -to manage starting and stopping Flask as part of a fixture setup. Any approach can be chosen here, in line with your -existing Flask testing practices. - -### Output - -The following file(s) will be created when the tests are run - -| Filename | Contents | -|-----------------------------| ----------| -| flask_provider/log/pact.log | All Pact interactions with the Flask Provider. Every interaction example retrieved from the Pact Broker will be performed during the Verification test; the request/response logged here. | - -## fastapi_provider - -The FastAPI [Provider] example consists of a basic FastAPI app, with a single endpoint route. -This implements the service expected by the [consumer](#consumer). - -Functionally, this provides the same service and tests as the [flask_provider](#flask_provider). Both are included to -demonstrate how Pact can be used in different environments with different technology stacks and approaches. - -The [Provider] side is responsible for performing the tests to verify if it is compliant with the [Pact file] contracts -associated with it. - -As such, the tests use the pact-python Verifier to perform this verification. Two approaches are demonstrated: -- Testing against the [Pact broker]. Generally this is the preferred approach, see information on [Sharing Pacts]. -- Testing against the [Pact file] directly. If no [Pact broker] is available you can verify against a static [Pact file]. -- -### Running - -To avoid package version conflicts with different applications, it is recommended to run these tests from a -[Virtual Environment] - -The following commands can be run from within your [Virtual Environment], in the `examples/fastapi_provider`. - -To perform the python tests: -```bash -pip install -r requirements.txt # Install the dependencies for the FastAPI example -pip install -e ../../ # Using setup.py in the pact-python root, install any pact dependencies and pact-python -./run_pytest.sh # Wrapper script to first run FastAPI, and then run the tests -``` - -To perform verification using CLI to verify the [Pact file] against the FastAPI [Provider] instead of the python tests: -```bash -pip install -r requirements.txt # Install the dependencies for the FastAPI example -./verify_pact.sh # Wrapper script to first run FastAPI, and then use `pact-verifier` to verify locally +
+ +In the first stage, the consumer defines a number of interactions in the form +below. Pact sets up a mock server that will respond to the requests as defined +by the consumer. All these interactions, containing both the request and +expected response, are all sent to the Pact Broker. + +> Given {provider state} \ +> Upon receiving {description} \ +> With {request} \ +> Will respond with {response} + +In the second stage, the provider retrieves the interactions from the Pact +Broker. It then sets up a mock client that will make the requests as defined by +the consumer. Pact then verifies that the responses from the provider match the +expected responses defined by the consumer. + +In this way, Pact is consumer driven and can ensure that the provider is +compatible with the consumer. While this example showcases both sides in Python, +this is absolutely not required. The provider could be written in any language, +and satisfy contracts from a number of consumers all written in different +languages. + +### Consumer + +The consumer in this example is a simple Python script that makes a HTTP GET +request to a server. It is defined in [`src/consumer.py`](src/consumer.py). The +tests for the consumer are defined in +[`tests/test_00_consumer.py`](tests/test_00_consumer.py). Each interaction is +defined using the format mentioned above. Programmatically, this looks like: + +```py +expected: dict[str, Any] = { + "id": Format().integer, + "name": "Verna Hampton", + "created_on": Format().iso_8601_datetime(), +} +( + pact.given("user 123 exists") + .upon_receiving("a request for user 123") + .with_request("get", "/users/123") + .will_respond_with(200, body=Like(expected)) +) +# Code that makes the request to the server ``` -To perform verification using CLI, but verifying the [Pact file] previously provided by a [Consumer], and publish the -results. This example requires that the [Pact broker] is already running, and the [Consumer] tests have been published -already, described in the [consumer](#consumer) section above. -```bash -pip install -r requirements.txt # Install the dependencies for the FastAPI example -./verify_pact.sh 1 # Wrapper script to first run FastAPI, and then use `pact-verifier` to verify and publish +### Provider + +This example showcases to different providers, one written in Flask and one +written in FastAPI. Both are simple Python web servers that respond to a HTTP +GET request. The Flask provider is defined in [`src/flask.py`](src/flask.py) and +the FastAPI provider is defined in [`src/fastapi.py`](src/fastapi.py). The +tests for the providers are defined in +[`tests/test_01_provider_flask.py`](tests/test_01_provider_flask.py) and +[`tests/test_01_provider_fastapi.py`](tests/test_01_provider_fastapi.py). + +Unlike the consumer side, the provider side is responsible to responding to the +interactions defined by the consumers. In this regard, the provider testing +is rather simple: + +```py +code, _ = verifier.verify_with_broker( + broker_url=str(broker), + published_verification_results=True, + provider_states_setup_url=str(PROVIDER_URL / "_pact" / "provider_states"), +) +assert code == 0 ``` -### Output - -The following file(s) will be created when the tests are run - -| Filename | Contents | -|-------------------------------| ----------| -| fastapi_provider/log/pact.log | All Pact interactions with the FastAPI Provider. Every interaction example retrieved from the Pact Broker will be performed during the Verification test; the request/response logged here. | - - -## message - -TODO - -## pacts - -Both the Flask and the FastAPI [Provider] examples implement the same service the [Consumer] example interacts with. -This folder contains the generated [Pact file] for reference, which is also used when running the [Provider] tests -without a [Pact Broker]. - -[Pact Broker]: https://docs.pact.io/pact_broker -[Pact Introduction]: https://docs.pact.io/ -[Consumer]: https://docs.pact.io/getting_started/terminology#service-consumer -[Provider]: https://docs.pact.io/getting_started/terminology#service-provider -[Versioning in the Pact Broker]: https://docs.pact.io/getting_started/versioning_in_the_pact_broker/ -[Pact file]: https://docs.pact.io/getting_started/terminology#pact-file -[Pact verification]: https://docs.pact.io/getting_started/terminology#pact-verification] -[Virtual Environment]: https://docs.python.org/3/tutorial/venv.html -[Sharing Pacts]: https://docs.pact.io/getting_started/sharing_pacts/] -[How to Run a Flask Application]: https://www.twilio.com/blog/how-run-flask-application -[Requiring/Loading plugins in a test module or conftest file]: https://docs.pytest.org/en/6.2.x/writing_plugins.html#requiring-loading-plugins-in-a-test-module-or-conftest-file +The complication comes from the fact that the provider needs to know what state +to be in before responding to the request. In order to achieve this, a testing +endpoint is defined that sets the state of the provider as defined in the +`provider_states_setup_url` above. For example, the consumer requests has _Given +user 123 exists_ as the provider state, and the provider will need to ensure +that this state is satisfied. This would typically entail setting up a database +with the correct data, but it is advisable to achieve the equivalent state by +mocking the appropriate calls. This has been showcased in both provider +examples. + +### Broker + +The broker acts as the intermediary between these test suites. It stores the +interactions defined by the consumer and makes them available to the provider. +Once the provider has verified that it satisfies all interactions, the broker +also stores the verification results. The example here runs the open source +broker within a Docker container. An alternative is to use the hosted [Pactflow +service](https://pactflow.io). From 3a59235bf5589b013d71e949f22f2799d65ed5cf Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Sep 2023 13:47:26 +1000 Subject: [PATCH 0038/1376] chore(example): migrate message pact example Migrate the old isolated example and combine the test with the other examples so that they can all be run at once. Massive thanks to @YOU54F for identifying the switch up between the `given` and `expected`! Signed-off-by: JP-Ellis --- examples/message/README.md | 246 ------------------ examples/message/conftest.py | 8 - examples/message/requirements.txt | 6 - examples/message/run_pytest.sh | 7 - examples/message/src/message_handler.py | 19 -- examples/message/tests/consumer/__init__.py | 0 .../tests/consumer/test_message_consumer.py | 156 ----------- examples/message/tests/provider/__init__.py | 0 .../tests/provider/test_message_provider.py | 83 ------ examples/src/message.py | 96 +++++++ examples/tests/test_02_message_consumer.py | 128 +++++++++ examples/tests/test_03_message_provider.py | 59 +++++ 12 files changed, 283 insertions(+), 525 deletions(-) delete mode 100644 examples/message/README.md delete mode 100644 examples/message/conftest.py delete mode 100644 examples/message/requirements.txt delete mode 100755 examples/message/run_pytest.sh delete mode 100644 examples/message/src/message_handler.py delete mode 100644 examples/message/tests/consumer/__init__.py delete mode 100644 examples/message/tests/consumer/test_message_consumer.py delete mode 100644 examples/message/tests/provider/__init__.py delete mode 100644 examples/message/tests/provider/test_message_provider.py create mode 100644 examples/src/message.py create mode 100644 examples/tests/test_02_message_consumer.py create mode 100644 examples/tests/test_03_message_provider.py diff --git a/examples/message/README.md b/examples/message/README.md deleted file mode 100644 index 13c738057..000000000 --- a/examples/message/README.md +++ /dev/null @@ -1,246 +0,0 @@ -# Introduction - -This is an e2e example that uses messages, including a sample implementation of a message handler. - -## Consumer - -A Consumer is the system that will be reading a message from a queue or some intermediary. In this example, the consumer is a Lambda function that handles the message. - -From a Pact testing point of view, Pact takes the place of the intermediary (MQ/broker etc.) and confirms whether or not the consumer is able to handle a request. - -``` -+-----------+ +-------------------+ -| (Pact) | message |(Message Consumer) | -| MQ/broker |--------->|Lambda Function | -| | |check valid doc | -+-----------+ +-------------------+ -``` - -Below is a sample message handler that only accepts that the key `documentType` would only be `microsoft-word`. If not, the message handler will throw an exception (`CustomError`) - -```python -class CustomError(Exception): - def __init__(self, *args): - if args: - self.topic = args[0] - else: - self.topic = None - - def __str__(self): - if self.topic: - return 'Custom Error:, {0}'.format(self.topic) - -class MessageHandler(object): - def __init__(self, event): - self.pass_event(event) - - @staticmethod - def pass_event(event): - if event.get('documentType') != 'microsoft-word': - raise CustomError("Not correct document type") -``` - -Below is a snippet from a test where the message handler has no error. -Since the expected event contains a key `documentType` with value `microsoft-word`, message handler does not throw an error and a pact file `f"{PACT_FILE}""` is expected to be generated. - -```python -def test_generate_new_pact_file(pact): - cleanup_json(PACT_FILE) - - expected_event = { - 'documentName': 'document.doc', - 'creator': 'TP', - 'documentType': 'microsoft-word' - } - - (pact - .given('A document create in Document Service') - .expects_to_receive('Description') - .with_content(expected_event) - .with_metadata({ - 'Content-Type': 'application/json' - })) - - with pact: - # handler needs 'documentType' == 'microsoft-word' - MessageHandler(expected_event) - - progressive_delay(f"{PACT_FILE}") - assert isfile(f"{PACT_FILE}") == 1 -``` - -For a similar test where the event does not contain a key `documentType` with value `microsoft-word`, a `CustomError` is generated and there there is no generated json file `f"{PACT_FILE}"`. - -```python -def test_throw_exception_handler(pact): - cleanup_json(PACT_FILE) - wrong_event = { - 'documentName': 'spreadsheet.xls', - 'creator': 'WI', - 'documentType': 'microsoft-excel' - } - - (pact - .given('Another document in Document Service') - .expects_to_receive('Description') - .with_content(wrong_event) - .with_metadata({ - 'Content-Type': 'application/json' - })) - - with pytest.raises(CustomError): - with pact: - # handler needs 'documentType' == 'microsoft-word' - MessageHandler(wrong_event) - - progressive_delay(f"{PACT_FILE}") - assert isfile(f"{PACT_FILE}") == 0 -``` - -## Provider - -``` -+-------------------+ +-----------+ -|(Message Provider) | message | (Pact) | -|Document Upload |--------->| MQ/broker | -|Service | | | -+-------------------+ +-----------+ -``` - -```python -import pytest -from pact import MessageProvider - -def document_created_handler(): - return { - "event": "ObjectCreated:Put", - "documentName": "document.doc", - "creator": "TP", - "documentType": "microsoft-word" - } - - -def test_verify_success(): - provider = MessageProvider( - message_providers={ - 'A document created successfully': document_created_handler - }, - provider='ContentProvider', - consumer='DetectContentLambda', - pact_dir='pacts' - - ) - with provider: - provider.verify() -``` - - -### Provider with pact broker -```python -import pytest -from pact import MessageProvider - - -PACT_BROKER_URL = "http://localhost" -PACT_BROKER_USERNAME = "pactbroker" -PACT_BROKER_PASSWORD = "pactbroker" -PACT_DIR = "pacts" - - -@pytest.fixture -def default_opts(): - return { - 'broker_username': PACT_BROKER_USERNAME, - 'broker_password': PACT_BROKER_PASSWORD, - 'broker_url': PACT_BROKER_URL, - 'publish_version': '3', - 'publish_verification_results': False - } - -def document_created_handler(): - return { - "event": "ObjectCreated:Put", - "documentName": "document.doc", - "creator": "TP", - "documentType": "microsoft-word" - } - -def test_verify_from_broker(default_opts): - provider = MessageProvider( - message_providers={ - 'A document created successfully': document_created_handler, - }, - provider='ContentProvider', - consumer='DetectContentLambda', - pact_dir='pacts' - - ) - - with pytest.raises(AssertionError): - with provider: - provider.verify_with_broker(**default_opts) - -``` - -## E2E Messaging - -``` -+-------------------+ +-----------+ +-------------------+ -|(Message Provider) | message | (Pact) | message |(Message Consumer) | -|Document Upload |--------->| MQ/broker |--------->|Lambda Function | -|Service | | | |check valid doc | -+-------------------+ +-----------+ +-------------------+ -``` - -# Setup - -## Virtual Environment - -Go to the `example/message` directory Create your own virtualenv for this. Run - -```bash -pip install -r requirements.txt -pip install -e ../../ -pytest -``` - -## Message Consumer - -From the root directory run: - -```bash -pytest -``` - -Or you can run individual tests like: - -```bash -pytest tests/consumer/test_message_consumer.py::test_generate_new_pact_file -``` - -## With Broker - -The current consumer test can run even without a local broker, -but this is added for demo purposes. - -Open a separate terminal in the `examples/broker` folder and run: - -```bash -docker-compose up -``` - -Open a browser to http://localhost and see the broker you have succeeded. -If needed, log-in using the provided details in tests such as: - -``` -PACT_BROKER_USERNAME = "pactbroker" -PACT_BROKER_PASSWORD = "pactbroker" -``` - -To get the consumer to publish a pact to broker, -open a new terminal in the `examples/message` and run the following (2 is an arbitary version number). The first part makes sure that the an existing json has been generated: - -```bash -pytest tests/consumer/test_message_consumer.py::test_publish_to_broker -pytest tests/consumer/test_message_consumer.py::test_publish_to_broker --publish-pact 2 -``` diff --git a/examples/message/conftest.py b/examples/message/conftest.py deleted file mode 100644 index 90ac63b9a..000000000 --- a/examples/message/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys - -# Load in the fixtures from common/sharedfixtures.py -sys.path.append("../common") - -pytest_plugins = [ - "sharedfixtures", -] diff --git a/examples/message/requirements.txt b/examples/message/requirements.txt deleted file mode 100644 index 021958ce1..000000000 --- a/examples/message/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -Flask==2.0.3; python_version < '3.7' -Flask==2.2.5; python_version >= '3.7' -pytest==7.0.1; python_version < '3.7' -pytest==7.1.3; python_version >= '3.7' -requests==2.27.1; python_version < '3.7' -requests>=2.28.0; python_version >= '3.7' diff --git a/examples/message/run_pytest.sh b/examples/message/run_pytest.sh deleted file mode 100755 index 35dade4fb..000000000 --- a/examples/message/run_pytest.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -o pipefail - -pytest --run-broker True --publish-pact 2 - -# publish to broker assuming broker is active -# pytest tests/consumer/test_message_consumer.py::test_publish_to_broker --publish-pact 2 diff --git a/examples/message/src/message_handler.py b/examples/message/src/message_handler.py deleted file mode 100644 index 1be2b4641..000000000 --- a/examples/message/src/message_handler.py +++ /dev/null @@ -1,19 +0,0 @@ -class CustomError(Exception): - def __init__(self, *args): - if args: - self.topic = args[0] - else: - self.topic = None - - def __str__(self): - if self.topic: - return 'Custom Error:, {0}'.format(self.topic) - -class MessageHandler(object): - def __init__(self, event): - self.pass_event(event) - - @staticmethod - def pass_event(event): - if event.get('documentType') != 'microsoft-word': - raise CustomError("Not correct document type") diff --git a/examples/message/tests/consumer/__init__.py b/examples/message/tests/consumer/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/message/tests/consumer/test_message_consumer.py b/examples/message/tests/consumer/test_message_consumer.py deleted file mode 100644 index 3c7de4c1a..000000000 --- a/examples/message/tests/consumer/test_message_consumer.py +++ /dev/null @@ -1,156 +0,0 @@ -"""pact test for a message consumer""" - -import logging -import pytest -import time - -from os import remove -from os.path import isfile - -from pact import MessageConsumer, Provider -from src.message_handler import MessageHandler, CustomError - -log = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -PACT_BROKER_URL = "http://localhost" -PACT_BROKER_USERNAME = "pactbroker" -PACT_BROKER_PASSWORD = "pactbroker" -PACT_DIR = "pacts" - -CONSUMER_NAME = "DetectContentLambda" -PROVIDER_NAME = "ContentProvider" -PACT_FILE = (f"{PACT_DIR}/{CONSUMER_NAME.lower().replace(' ', '_')}-" - + f"{PROVIDER_NAME.lower().replace(' ', '_')}.json") - - -@pytest.fixture(scope="session") -def pact(request): - version = request.config.getoption("--publish-pact") - publish = True if version else False - - pact = MessageConsumer(CONSUMER_NAME, version=version).has_pact_with( - Provider(PROVIDER_NAME), - publish_to_broker=publish, broker_base_url=PACT_BROKER_URL, - broker_username=PACT_BROKER_USERNAME, broker_password=PACT_BROKER_PASSWORD, pact_dir=PACT_DIR) - - yield pact - - -@pytest.fixture(scope="session") -def pact_no_publish(request): - version = request.config.getoption("--publish-pact") - pact = MessageConsumer(CONSUMER_NAME, version=version).has_pact_with( - Provider(PROVIDER_NAME), - publish_to_broker=False, broker_base_url=PACT_BROKER_URL, - broker_username=PACT_BROKER_USERNAME, broker_password=PACT_BROKER_PASSWORD, pact_dir=PACT_DIR) - - yield pact - -def cleanup_json(file): - """ - Remove existing json file before test if any - """ - if (isfile(f"{file}")): - remove(f"{file}") - - -def progressive_delay(file, time_to_wait=10, second_interval=0.5, verbose=False): - """ - progressive delay - defaults to wait up to 5 seconds with 0.5 second intervals - """ - time_counter = 0 - while not isfile(file): - time.sleep(second_interval) - time_counter += 1 - if verbose: - print(f"Trying for {time_counter*second_interval} seconds") - if time_counter > time_to_wait: - if verbose: - print(f"Already waited {time_counter*second_interval} seconds") - break - - -def test_throw_exception_handler(pact_no_publish): - cleanup_json(PACT_FILE) - - wrong_event = { - "event": "ObjectCreated:Put", - "documentName": "spreadsheet.xls", - "creator": "WI", - "documentType": "microsoft-excel" - } - - (pact_no_publish - .given("Document unsupported type") - .expects_to_receive("Description") - .with_content(wrong_event) - .with_metadata({ - "Content-Type": "application/json" - })) - - with pytest.raises(CustomError): - with pact_no_publish: - # handler needs "documentType" == "microsoft-word" - MessageHandler(wrong_event) - - progressive_delay(f"{PACT_FILE}") - assert isfile(f"{PACT_FILE}") == 0 - - -def test_put_file(pact_no_publish): - cleanup_json(PACT_FILE) - - expected_event = { - "event": "ObjectCreated:Put", - "documentName": "document.doc", - "creator": "TP", - "documentType": "microsoft-word" - } - - (pact_no_publish - .given("A document created successfully") - .expects_to_receive("Description") - .with_content(expected_event) - .with_metadata({ - "Content-Type": "application/json" - })) - - with pact_no_publish: - MessageHandler(expected_event) - - progressive_delay(f"{PACT_FILE}") - assert isfile(f"{PACT_FILE}") == 1 - - -def test_publish_to_broker(pact): - """ - This test does not clean-up previously generated pact. - Sample execution where 2 is an arbitrary version: - - `pytest tests/consumer/test_message_consumer.py::test_publish_pact_to_broker` - - `pytest tests/consumer/test_message_consumer.py::test_publish_pact_to_broker --publish-pact 2` - """ - - expected_event = { - "event": "ObjectCreated:Delete", - "documentName": "document.doc", - "creator": "TP", - "documentType": "microsoft-word" - } - - (pact - .given("A document deleted successfully") - .expects_to_receive("Description with broker") - .with_content(expected_event) - .with_metadata({ - "Content-Type": "application/json" - })) - - with pact: - MessageHandler(expected_event) - - progressive_delay(f"{PACT_FILE}") - assert isfile(f"{PACT_FILE}") == 1 diff --git a/examples/message/tests/provider/__init__.py b/examples/message/tests/provider/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/message/tests/provider/test_message_provider.py b/examples/message/tests/provider/test_message_provider.py deleted file mode 100644 index ae20ab3a1..000000000 --- a/examples/message/tests/provider/test_message_provider.py +++ /dev/null @@ -1,83 +0,0 @@ -import pytest -from pact import MessageProvider - -PACT_BROKER_URL = "http://localhost" -PACT_BROKER_USERNAME = "pactbroker" -PACT_BROKER_PASSWORD = "pactbroker" -PACT_DIR = "pacts" - - -@pytest.fixture -def default_opts(): - return { - 'broker_username': PACT_BROKER_USERNAME, - 'broker_password': PACT_BROKER_PASSWORD, - 'broker_url': PACT_BROKER_URL, - 'publish_version': '3', - 'publish_verification_results': False - } - - -def document_created_handler(): - return { - "event": "ObjectCreated:Put", - "documentName": "document.doc", - "creator": "TP", - "documentType": "microsoft-word" - } - - -def document_deleted_handler(): - return { - "event": "ObjectCreated:Delete", - "documentName": "document.doc", - "creator": "TP", - "documentType": "microsoft-word" - } - - -def test_verify_success(): - provider = MessageProvider( - message_providers={ - 'A document created successfully': document_created_handler, - 'A document deleted successfully': document_deleted_handler - }, - provider='ContentProvider', - consumer='DetectContentLambda', - pact_dir='pacts' - - ) - with provider: - provider.verify() - - -def test_verify_failure_when_a_provider_missing(): - provider = MessageProvider( - message_providers={ - 'A document created successfully': document_created_handler, - }, - provider='ContentProvider', - consumer='DetectContentLambda', - pact_dir='pacts' - - ) - - with pytest.raises(AssertionError): - with provider: - provider.verify() - - -def test_verify_from_broker(default_opts): - provider = MessageProvider( - message_providers={ - 'A document created successfully': document_created_handler, - 'A document deleted successfully': document_deleted_handler - }, - provider='ContentProvider', - consumer='DetectContentLambda', - pact_dir='pacts' - - ) - - with provider: - provider.verify_with_broker(**default_opts) diff --git a/examples/src/message.py b/examples/src/message.py new file mode 100644 index 000000000..6a617b271 --- /dev/null +++ b/examples/src/message.py @@ -0,0 +1,96 @@ +""" +Handler for non-HTTP interactions. + +This module implements a very basic handler to handle JSON payloads which might +be sent from Kafka, or some queueing system. Unlike a HTTP interaction, the +handler is solely responsible for processing the message, and does not +necessarily need to send a response. +""" +from __future__ import annotations + +from pathlib import Path +from typing import Any + + +class Filesystem: + """Filesystem interface.""" + + def __init__(self) -> None: + """Initialize the filesystem connection.""" + + def write(self, _file: str, _contents: str) -> None: + """Write contents to a file.""" + raise NotImplementedError + + def read(self, file: str) -> str: + """Read contents from a file.""" + raise NotImplementedError + + +class Handler: + """ + Message queue handler. + + This class is responsible for handling messages from the queue. + """ + + def __init__(self) -> None: + """ + Initialize the handler. + + This ensures the underlying filesystem is ready to be used. + """ + self.fs = Filesystem() + + def process(self, event: dict[str, Any]) -> str | None: + """ + Process an event from the queue. + + The event is expected to be a dictionary with the following mandatory + keys: + + - `action`: The action to be performed, either `READ` or `WRITE`. + - `path`: The path to the file to be read or written. + + The event may also contain an optional `contents` key, which is the + contents to be written to the file. If the `contents` key is not + present, an empty file will be written. + """ + self.validate_event(event) + + if event["action"] == "WRITE": + return self.fs.write(event["path"], event.get("contents", "")) + if event["action"] == "READ": + return self.fs.read(event["path"]) + + msg = "Invalid action." + raise ValueError(msg) + + @staticmethod + def validate_event(event: dict[str, Any] | Any) -> None: # noqa: ANN401 + """ + Validates the event received from the queue. + + The event is expected to be a dictionary with the following mandatory + keys: + + - `action`: The action to be performed, either `READ` or `WRITe`. + - `path`: The path to the file to be read or written. + """ + if not isinstance(event, dict): + msg = "Event must be a dictionary." + raise TypeError(msg) + if "action" not in event: + msg = "Event must contain an 'action' key." + raise ValueError(msg) + if "path" not in event: + msg = "Event must contain a 'path' key." + raise ValueError(msg) + if event["action"] not in ["READ", "WRITE"]: + msg = "Event must contain a valid 'action' key." + raise ValueError(msg) + try: + Path(event["path"]) + except TypeError as err: + msg = "Event must contain a valid 'path' key." + raise ValueError(msg) from err diff --git a/examples/tests/test_02_message_consumer.py b/examples/tests/test_02_message_consumer.py new file mode 100644 index 000000000..7a5c269aa --- /dev/null +++ b/examples/tests/test_02_message_consumer.py @@ -0,0 +1,128 @@ +""" +Test Message Pact consumer. + +Pact was originally designed for HTTP interactions involving a request and a +response. Message Pact is an addition to Pact that allows for testing of +non-HTTP interactions, such as message queues. This example demonstrates how to +use Message Pact to test whether a consumer can handle the messages it. + +A note on terminology, the _consumer_ for Message Pact is the system that +receives the message, and the _provider_ is the system that sends the message. +Pact is still consumer-driven, and the consumer defines the expected messages it +will receive from the provider. When the provider is being verified, Pact +ensures that the provider sends the expected messages. + +In this example, Pact simply ensures that the consumer is capable of processing +the message. The consumer need not send back a message, and any sideffects of +the message must be verified separately (such as through `assert` statements). + +> :warning: There is currently a bug whereby the `given` and +`expects_to_receive` have swapped meanings. This will be addressed in a future +release. +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any, Generator +from unittest.mock import MagicMock + +import pytest +from pact import MessageConsumer, MessagePact, Provider + +from src.message import Handler + +if TYPE_CHECKING: + from pathlib import Path + + from yarl import URL + +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="module") +def pact(broker: URL, pact_dir: Path) -> Generator[MessagePact, Any, None]: + """ + Set up Message Pact Consumer. + + This fixtures sets up the Message Pact consumer and the pact it has with a + provider. The consumer defines the expected messages it will receive from + the provider, and the Python test suite verifies that the correct actions + are taken. + + For each interaction, the consumer defines the following: + + ```python + ( + pact.given("a request to write test.txt") + .expects_to_receive("empty filesystem") + .with_content(msg) + .with_metadata({"Content-Type": "application/json"}) + ) + + NOTE: There is currently a bug whereby the `given` and `expects_to_receive` + have swapped meanings. This will be addressed in a future release. + ``` + """ + consumer = MessageConsumer("MessageConsumer") + pact = consumer.has_pact_with( + Provider("MessageProvider"), + pact_dir=pact_dir, + publish_to_broker=True, + # Broker configuration + broker_base_url=str(broker), + broker_username=broker.user, + broker_password=broker.password, + ) + with pact: + yield pact + + +@pytest.fixture() +def handler() -> Handler: + """ + Fixture for the Handler. + + This fixture mocks the filesystem calls in the handler, so that we can + verify that the handler is calling the filesystem correctly. + """ + handler = Handler() + handler.fs = MagicMock() + handler.fs.write.return_value = None + handler.fs.read.return_value = "Hello world!" + return handler + + +def test_write_file(pact: MessagePact, handler: Handler) -> None: + """ + Test write file. + + This test will be run against the mock provider. The mock provider will + expect to receive a request to write a file, and will respond with a 200 + status code. + """ + msg = {"action": "WRITE", "path": "test.txt", "contents": "Hello world!"} + ( + pact.given("a request to write test.txt") + .expects_to_receive("empty filesystem") + .with_content(msg) + .with_metadata({"Content-Type": "application/json"}) + ) + + result = handler.process(msg) + handler.fs.write.assert_called_once_with("test.txt", "Hello world!") + assert result is None + + +def test_read_file(pact: MessagePact, handler: Handler) -> None: + msg = {"action": "READ", "path": "test.txt"} + ( + pact.given("a request to read test.txt") + .expects_to_receive("test.txt exists") + .with_content(msg) + .with_metadata({"Content-Type": "application/json"}) + ) + + result = handler.process(msg) + handler.fs.read.assert_called_once_with("test.txt") + assert result == "Hello world!" diff --git a/examples/tests/test_03_message_provider.py b/examples/tests/test_03_message_provider.py new file mode 100644 index 000000000..4f4570a63 --- /dev/null +++ b/examples/tests/test_03_message_provider.py @@ -0,0 +1,59 @@ +""" +Test Message Pact provider. + +Unlike the standard Pact, which is designed for HTTP interactions, the Message +Pact is designed for non-HTTP interactions. This example demonstrates how to use +the Message Pact to test whether a provider generates the correct messages. + +In such examples, Pact simply checks the kind of messages produced. The consumer +need not send back a message, and any sideffects of the message must be verified +separately. + +The below example verifies that the consumer makes the correct filesystem calls +when it receives a message to read or write a file. The calls themselves are +mocked out so as to avoid actually writing to the filesystem. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +from flask import Flask +from pact import MessageProvider + +if TYPE_CHECKING: + from yarl import URL + +app = Flask(__name__) +PACT_DIR = (Path(__file__).parent / "pacts").resolve() + + +def generate_write_message() -> dict[str, str]: + return { + "action": "WRITE", + "path": "test.txt", + "contents": "Hello world!", + } + + +def generate_read_message() -> dict[str, str]: + return { + "action": "READ", + "path": "test.txt", + } + + +def test_verify(broker: URL) -> None: + provider = MessageProvider( + provider="MessageProvider", + consumer="MessageConsumer", + pact_dir=str(PACT_DIR), + message_providers={ + "a request to write test.txt": generate_write_message, + "a request to read test.txt": generate_read_message, + }, + ) + + with provider: + provider.verify_with_broker(broker_url=str(broker)) From 045083bce0fcc29ce8dff14dfed8a761d45655fa Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 21 Sep 2023 12:46:44 +1000 Subject: [PATCH 0039/1376] chore(ci): split tests examples and lints Try splitting the CI workflow to make use of services Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 84 +++++++++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ea1ae8d5c..def2b9d3a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,12 +14,12 @@ concurrency: env: STABLE_PYTHON_VERSION: "3.11" + PYTEST_ADDOPTS: --color=yes jobs: - run: + test: name: >- - Python ${{ matrix.python-version }} - on ${{ matrix.os }} + Tests py${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} @@ -47,14 +47,76 @@ jobs: - name: Install Hatch run: pip install --upgrade hatch - - # TODO: Fix lints before enabling this - name: Lint - if: matrix.python-version == env.STABLE_PYTHON_VERSION && runner.os == 'Linux' - run: echo hatch run lint + - name: Run tests + run: hatch run test + + - name: Upload coverage + # TODO: Configure code coverage monitoring + if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' + uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + example: + name: Example + + runs-on: ubuntu-latest + services: + broker: + image: pactfoundation/pact-broker:latest + ports: + - "9292:9292" + env: + # Basic auth credentials for the Broker + PACT_BROKER_ALLOW_PUBLIC_READ: "true" + PACT_BROKER_BASIC_AUTH_USERNAME: pactbroker + PACT_BROKER_BASIC_AUTH_PASSWORD: pactbroker + # Database + PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3 + uses: actions/setup-python@v4 + with: + python-version: ${{ env.STABLE_PYTHON_VERSION }} + + - name: Install Hatch + run: pip install --upgrade hatch + + - name: Ensure broker is live + run: | + i=0 + until curl -sSf http://localhost:9292/diagnostic/status/heartbeat; do + i=$((i+1)) + if [ $i -gt 120 ]; then + echo "Broker failed to start" + exit 1 + fi + sleep 1 + done - name: Examples - if: matrix.python-version == env.STABLE_PYTHON_VERSION && runner.os == 'Linux' - run: hatch run example --color=yes --capture=no + run: > + hatch run example --broker-url=http://pactbroker:pactbroker@localhost:9292 - - name: Run tests and track code coverage - run: hatch run test + # TODO: Fix lints before enabling this + # lint: + # name: Lint + + # runs-on: ubuntu-latest + + # steps: + # - uses: actions/checkout@v4 + + # - name: Set up Python + # uses: actions/setup-python@v4 + # with: + # python-version: ${{ env.STABLE_PYTHON_VERSION }} + + # - name: Install Hatch + # run: pip install --upgrade hatch + + # - name: Lint + # run: hatch run lint From e86b7eb9c8d249b8058da2a1c97fa61833f5f8f4 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 22 Sep 2023 13:28:26 +1000 Subject: [PATCH 0040/1376] chore(example): avoid changing python path Initially, when having the tests completely separate, the examples were scoped to their own space and required setting `PYTHONPATH` to work. This is not needed if we change `import src.{mod}` to `import examples.src.{mod}`. Signed-off-by: JP-Ellis --- examples/tests/test_00_consumer.py | 3 +-- examples/tests/test_01_provider_fastapi.py | 15 +++++++-------- examples/tests/test_01_provider_flask.py | 15 +++++++-------- examples/tests/test_02_message_consumer.py | 3 +-- pyproject.toml | 2 +- 5 files changed, 17 insertions(+), 21 deletions(-) diff --git a/examples/tests/test_00_consumer.py b/examples/tests/test_00_consumer.py index 029550714..af3220fb9 100644 --- a/examples/tests/test_00_consumer.py +++ b/examples/tests/test_00_consumer.py @@ -17,11 +17,10 @@ import pytest import requests +from examples.src.consumer import User, UserConsumer from pact import Consumer, Format, Like, Provider from yarl import URL -from src.consumer import User, UserConsumer - if TYPE_CHECKING: from pathlib import Path diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/tests/test_01_provider_fastapi.py index ce05964f0..75503b18d 100644 --- a/examples/tests/test_01_provider_fastapi.py +++ b/examples/tests/test_01_provider_fastapi.py @@ -26,12 +26,11 @@ import pytest import uvicorn +from examples.src.fastapi import app from pact import Verifier from pydantic import BaseModel from yarl import URL -from src.fastapi import app - PROVIDER_URL = URL("http://localhost:8080") @@ -91,10 +90,10 @@ def verifier() -> Generator[Verifier, Any, None]: def mock_user_123_doesnt_exist() -> None: """Mock the database for the user 123 doesn't exist state.""" - import src.fastapi + import examples.src.fastapi - src.fastapi.FAKE_DB = MagicMock() - src.fastapi.FAKE_DB.get.return_value = None + examples.src.fastapi.FAKE_DB = MagicMock() + examples.src.fastapi.FAKE_DB.get.return_value = None def mock_user_123_exists() -> None: @@ -108,10 +107,10 @@ def mock_user_123_exists() -> None: By using consumer-driven contracts and testing the provider against the consumer's contract, we can ensure that the provider is only providing what """ - import src.fastapi + import examples.src.fastapi - src.fastapi.FAKE_DB = MagicMock() - src.fastapi.FAKE_DB.get.return_value = { + examples.src.fastapi.FAKE_DB = MagicMock() + examples.src.fastapi.FAKE_DB.get.return_value = { "id": 123, "name": "Verna Hampton", "created_on": "2016-12-15T20:16:01", diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py index 1aff011e3..532057a4d 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/tests/test_01_provider_flask.py @@ -26,12 +26,11 @@ from unittest.mock import MagicMock import pytest +from examples.src.flask import app from flask import request from pact import Verifier from yarl import URL -from src.flask import app - PROVIDER_URL = URL("http://localhost:8080") @@ -84,10 +83,10 @@ def verifier() -> Generator[Verifier, Any, None]: def mock_user_123_doesnt_exist() -> None: """Mock the database for the user 123 doesn't exist state.""" - import src.flask + import examples.src.flask - src.flask.FAKE_DB = MagicMock() - src.flask.FAKE_DB.get.return_value = None + examples.src.flask.FAKE_DB = MagicMock() + examples.src.flask.FAKE_DB.get.return_value = None def mock_user_123_exists() -> None: @@ -101,10 +100,10 @@ def mock_user_123_exists() -> None: By using consumer-driven contracts and testing the provider against the consumer's contract, we can ensure that the provider is only providing what """ - import src.flask + import examples.src.flask - src.flask.FAKE_DB = MagicMock() - src.flask.FAKE_DB.get.return_value = { + examples.src.flask.FAKE_DB = MagicMock() + examples.src.flask.FAKE_DB.get.return_value = { "id": 123, "name": "Verna Hampton", "created_on": "2016-12-15T20:16:01", diff --git a/examples/tests/test_02_message_consumer.py b/examples/tests/test_02_message_consumer.py index 7a5c269aa..73457198a 100644 --- a/examples/tests/test_02_message_consumer.py +++ b/examples/tests/test_02_message_consumer.py @@ -28,10 +28,9 @@ from unittest.mock import MagicMock import pytest +from examples.src.message import Handler from pact import MessageConsumer, MessagePact, Provider -from src.message import Handler - if TYPE_CHECKING: from pathlib import Path diff --git a/pyproject.toml b/pyproject.toml index 5fb225b05..fdb53cb32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ 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/" -example = "PYTHONPATH=examples pytest examples/ {args}" +example = "pytest examples/ {args}" all = ["lint", "tests"] # Test environment for running unit tests. This automatically tests against all From 9896320b3ed71fc424b926ed98d1b0c3f18adbc3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Sep 2023 10:31:56 +1000 Subject: [PATCH 0041/1376] chore: address pr comments Signed-off-by: JP-Ellis --- Makefile | 111 ++++++--------------- examples/conftest.py | 4 +- examples/docker-compose.yml | 1 + examples/src/consumer.py | 9 +- examples/src/fastapi.py | 18 ++-- examples/src/flask.py | 14 +-- examples/src/message.py | 6 +- examples/tests/test_00_consumer.py | 19 +++- examples/tests/test_01_provider_fastapi.py | 17 +++- examples/tests/test_01_provider_flask.py | 15 ++- examples/tests/test_02_message_consumer.py | 15 ++- examples/tests/test_03_message_provider.py | 33 +++--- hatch_build.py | 4 +- pyproject.toml | 9 +- 14 files changed, 139 insertions(+), 136 deletions(-) diff --git a/Makefile b/Makefile index cdf2c9100..aa7981fb1 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,23 @@ -DOCS_DIR := ./docs - -PROJECT := pact-python -PYTHON_MAJOR_VERSION := 3.11 - -sgr0 := $(shell tput sgr0) -red := $(shell tput setaf 1) -green := $(shell tput setaf 2) - help: @echo "" @echo " clean to clear build and distribution directories" - @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" - @echo " flask to run the example Flask provider tests" - @echo " messaging to run the example messaging e2e tests" - @echo " package to create a distribution package in /dist/" + @echo " package to build a wheel and sdist" @echo " release to perform a release build, including deps, test, and package targets" - @echo " test to run all tests" @echo "" + @echo " test to run all tests on the current python version" + @echo " test-all to run all tests on all supported python versions" + @echo " example to run the example end to end tests (requires docker)" + @echo " lint to run the lints" + @echo " ci to run test and lints" + @echo "" + @echo " help to show this help message" + @echo "" + @echo "Most of these targets are just wrappers around hatch commands." + @echo "See https://hatch.pypa.org for information to install hatch." .PHONY: release -release: test package +release: clean test package .PHONY: clean @@ -30,77 +25,31 @@ clean: hatch clean -define CONSUMER - echo "consumer make" - cd examples/consumer - pip install -q -r requirements.txt - pip install -e ../../ - ./run_pytest.sh -endef -export CONSUMER - - -define FLASK_PROVIDER - echo "flask make" - cd examples/flask_provider - pip install -q -r requirements.txt - pip install -e ../../ - ./run_pytest.sh -endef -export FLASK_PROVIDER - - -define FASTAPI_PROVIDER - echo "fastapi make" - cd examples/fastapi_provider - pip install -q -r requirements.txt - pip install -e ../../ - ./run_pytest.sh -endef -export FASTAPI_PROVIDER - - -define MESSAGING - echo "messaging make" - cd examples/message - pip install -q -r requirements.txt - pip install -e ../../ - ./run_pytest.sh -endef -export MESSAGING - - -.PHONY: consumer -consumer: - bash -c "$$CONSUMER" - - -.PHONY: flask -flask: - bash -c "$$FLASK_PROVIDER" +.PHONY: package +package: + hatch build -.PHONY: fastapi -fastapi: - bash -c "$$FASTAPI_PROVIDER" +.PHONY: test +test: + hatch run test + hatch run coverage report -m --fail-under=100 -.PHONY: messaging -messaging: - bash -c "$$MESSAGING" +.PHONY: test-all +test-all: + hatch run test:test -.PHONY: examples -examples: consumer flask fastapi messaging +.PHONY: example +example: + hatch run example -.PHONY: package -package: - hatch build +.PHONY: lint +lint: + hatch run lint -.PHONY: test -test: - hatch run all - hatch run test:all - coverage report -m --fail-under=100 +.PHONY: ci +ci: test lint diff --git a/examples/conftest.py b/examples/conftest.py index 3c0a5994a..aa1de61b1 100644 --- a/examples/conftest.py +++ b/examples/conftest.py @@ -12,7 +12,7 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Generator +from typing import Any, Generator, Union import pytest from testcontainers.compose import DockerCompose @@ -45,7 +45,7 @@ def broker(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: Otherwise, the Pact broker is started in a container. The URL of the containerised broker is then returned. """ - broker_url: str | None = request.config.getoption("--broker-url") + broker_url: Union[str, None] = request.config.getoption("--broker-url") # If we have been given a broker URL, there's nothing more to do here and we # can return early. diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml index c845b1ee1..2405a49a9 100644 --- a/examples/docker-compose.yml +++ b/examples/docker-compose.yml @@ -18,6 +18,7 @@ services: - postgres ports: - "9292:9292" + restart: always environment: # Basic auth credentials for the Broker PACT_BROKER_ALLOW_PUBLIC_READ: "true" diff --git a/examples/src/consumer.py b/examples/src/consumer.py index 3aab181da..18819d4f7 100644 --- a/examples/src/consumer.py +++ b/examples/src/consumer.py @@ -3,8 +3,9 @@ This modules defines a simple [consumer](https://docs.pact.io/getting_started/terminology#service-consumer) -with Pact. As Pact is a consumer-driven framework, the consumer defines the -interactions which the provider must then satisfy. +which will be tested with Pact in the [consumer +test](../tests/test_00_consumer.py). As Pact is a consumer-driven framework, the +consumer defines the interactions which the provider must then satisfy. The consumer is the application which makes requests to another service (the provider) and receives a response to process. In this example, we have a simple @@ -20,7 +21,7 @@ from dataclasses import dataclass from datetime import datetime -from typing import Any +from typing import Any, Dict import requests @@ -95,7 +96,7 @@ def get_user(self, user_id: int) -> User: uri = f"{self.base_uri}/users/{user_id}" response = requests.get(uri, timeout=5) response.raise_for_status() - data: dict[str, Any] = response.json() + data: Dict[str, Any] = response.json() return User( id=data["id"], name=data["name"], diff --git a/examples/src/fastapi.py b/examples/src/fastapi.py index 2589fcd3d..52c6e3ff3 100644 --- a/examples/src/fastapi.py +++ b/examples/src/fastapi.py @@ -3,8 +3,10 @@ This modules defines a simple [provider](https://docs.pact.io/getting_started/terminology#service-provider) -with Pact. As Pact is a consumer-driven framework, the consumer defines the -contract which the provider must then satisfy. +which will be tested with Pact in the [provider +test](../tests/test_01_provider_fastapi.py). As Pact is a consumer-driven +framework, the consumer defines the contract which the provider must then +satisfy. The provider is the application which receives requests from another service (the consumer) and returns a response. In this example, we have a simple @@ -17,7 +19,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, Dict from fastapi import FastAPI from fastapi.responses import JSONResponse @@ -28,15 +30,15 @@ As this is a simple example, we'll use a simple dict to represent a database. This would be replaced with a real database in a real application. -When testing the provider in a real application, the calls to the database -would be mocked out to avoid the need for a real database. An example of this -can be found in the test suite. +When testing the provider in a real application, the calls to the database would +be mocked out to avoid the need for a real database. An example of this can be +found in the [test suite](../tests/test_01_provider_fastapi.py). """ -FAKE_DB: dict[int, dict[str, Any]] = {} +FAKE_DB: Dict[int, Dict[str, Any]] = {} @app.get("/users/{uid}") -async def get_user_by_id(uid: int) -> dict[str, Any]: +async def get_user_by_id(uid: int) -> Dict[str, Any]: """ Fetch a user by their ID. diff --git a/examples/src/flask.py b/examples/src/flask.py index fd5095e03..da5424087 100644 --- a/examples/src/flask.py +++ b/examples/src/flask.py @@ -3,8 +3,10 @@ This modules defines a simple [provider](https://docs.pact.io/getting_started/terminology#service-provider) -with Pact. As Pact is a consumer-driven framework, the consumer defines the -contract which the provider must then satisfy. +which will be tested with Pact in the [provider +test](../tests/test_01_provider_flask.py). As Pact is a consumer-driven +framework, the consumer defines the contract which the provider must then +satisfy. The provider is the application which receives requests from another service (the consumer) and returns a response. In this example, we have a simple @@ -17,7 +19,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, Dict, Union from flask import Flask @@ -29,13 +31,13 @@ When testing the provider in a real application, the calls to the database would be mocked out to avoid the need for a real database. An example of this -can be found in the test suite. +can be found in the [test suite](../tests/test_01_provider_flask.py). """ -FAKE_DB: dict[int, dict[str, Any]] = {} +FAKE_DB: Dict[int, Dict[str, Any]] = {} @app.route("/users/") -def get_user_by_id(uid: int) -> dict[str, Any] | tuple[dict[str, Any], int]: +def get_user_by_id(uid: int) -> Union[Dict[str, Any], tuple[Dict[str, Any], int]]: """ Fetch a user by their ID. diff --git a/examples/src/message.py b/examples/src/message.py index 6a617b271..ed0a755bf 100644 --- a/examples/src/message.py +++ b/examples/src/message.py @@ -9,7 +9,7 @@ from __future__ import annotations from pathlib import Path -from typing import Any +from typing import Any, Dict, Union class Filesystem: @@ -42,7 +42,7 @@ def __init__(self) -> None: """ self.fs = Filesystem() - def process(self, event: dict[str, Any]) -> str | None: + def process(self, event: Dict[str, Any]) -> Union[str, None]: """ Process an event from the queue. @@ -67,7 +67,7 @@ def process(self, event: dict[str, Any]) -> str | None: raise ValueError(msg) @staticmethod - def validate_event(event: dict[str, Any] | Any) -> None: # noqa: ANN401 + def validate_event(event: Union[Dict[str, Any], Any]) -> None: # noqa: ANN401 """ Validates the event received from the queue. diff --git a/examples/tests/test_00_consumer.py b/examples/tests/test_00_consumer.py index af3220fb9..7cb8369bd 100644 --- a/examples/tests/test_00_consumer.py +++ b/examples/tests/test_00_consumer.py @@ -7,13 +7,17 @@ is responding with the expected responses. Once these interactions are validated, the contracts can be published to a Pact Broker. The contracts can then be used to validate the provider's interactions. + +A good resource for understanding the consumer tests is the [Pact Consumer +Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-consumer-pact-test) +section of the Pact documentation. """ from __future__ import annotations import logging from http import HTTPStatus -from typing import TYPE_CHECKING, Any, Generator +from typing import TYPE_CHECKING, Any, Dict, Generator import pytest import requests @@ -40,6 +44,11 @@ def user_consumer() -> UserConsumer: the consumer to use Pact's mock provider. This allows us to define what requests the consumer will make to the provider, and what responses the provider will return. + + The ability for the client to specify the expected response from the + provider is critical to Pact's consumer-driven approach as it allows the + consumer to declare the minimal response it requires from the provider (even + if the provider is returning more data than the consumer needs). """ return UserConsumer(str(MOCK_URL)) @@ -53,7 +62,7 @@ def pact(broker: URL, pact_dir: Path) -> Generator[Pact, Any, None]: the provider. This mock provider will expect to receive defined requests and will respond with defined responses. - The fixture here simply defines the Consumer and Provide, and sets up the + The fixture here simply defines the Consumer and Provider, and sets up the mock provider. With each test, we define the expected request and response from the provider as follows: @@ -90,7 +99,11 @@ def test_get_existing_user(pact: Pact, user_consumer: UserConsumer) -> None: This test defines the expected request and response from the provider. The provider will be expected to return a response with a status code of 200, """ - expected: dict[str, Any] = { + # When setting up the expected response, the consumer should only define + # what it needs from the provider (as opposed to the full schema). Should + # the provider later decide to add or remove fields, Pact's consumer-driven + # approach will ensure that interaction is still valid. + expected: Dict[str, Any] = { "id": Format().integer, "name": "Verna Hampton", "created_on": Format().iso_8601_datetime(), diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/tests/test_01_provider_fastapi.py index 75503b18d..d2d7486b8 100644 --- a/examples/tests/test_01_provider_fastapi.py +++ b/examples/tests/test_01_provider_fastapi.py @@ -16,12 +16,16 @@ additional endpoint on the provider, in this case `/_pact/provider_states`. Calls to this endpoint mock the relevant database calls to set the provider into the correct state. + +A good resource for understanding the provider tests is the [Pact Provider +Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-provider-pact-test) +section of the Pact documentation. """ from __future__ import annotations from multiprocessing import Process -from typing import Any, Generator +from typing import Any, Dict, Generator, Union from unittest.mock import MagicMock import pytest @@ -42,7 +46,9 @@ class ProviderState(BaseModel): @app.post("/_pact/provider_states") -async def mock_pact_provider_states(state: ProviderState) -> dict[str, str | None]: +async def mock_pact_provider_states( + state: ProviderState, +) -> Dict[str, Union[str, None]]: """ Define the provider state. @@ -102,10 +108,13 @@ def mock_user_123_exists() -> None: You may notice that the return value here differs from the consumer's expected response. This is because the consumer's expected response is - guided by what the consumer users. + guided by what the consumer uses. By using consumer-driven contracts and testing the provider against the - consumer's contract, we can ensure that the provider is only providing what + consumer's contract, we can ensure that the provider is what the consumer + needs. This allows the provider to safely evolve their API (by both adding + and removing fields) without fear of breaking the interactions with the + consumers. """ import examples.src.fastapi diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py index 532057a4d..50ad3fc34 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/tests/test_01_provider_flask.py @@ -16,13 +16,17 @@ additional endpoint on the provider, in this case `/_pact/provider_states`. Calls to this endpoint mock the relevant database calls to set the provider into the correct state. + +A good resource for understanding the provider tests is the [Pact Provider +Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-provider-pact-test) +section of the Pact documentation. """ from __future__ import annotations from multiprocessing import Process -from typing import Any, Generator +from typing import Any, Dict, Generator, Union from unittest.mock import MagicMock import pytest @@ -35,7 +39,7 @@ @app.route("/_pact/provider_states", methods=["POST"]) -async def mock_pact_provider_states() -> dict[str, str | None]: +async def mock_pact_provider_states() -> Dict[str, Union[str, None]]: """ Define the provider state. @@ -95,10 +99,13 @@ def mock_user_123_exists() -> None: You may notice that the return value here differs from the consumer's expected response. This is because the consumer's expected response is - guided by what the consumer users. + guided by what the consumer uses. By using consumer-driven contracts and testing the provider against the - consumer's contract, we can ensure that the provider is only providing what + consumer's contract, we can ensure that the provider is what the consumer + needs. This allows the provider to safely evolve their API (by both adding + and removing fields) without fear of breaking the interactions with the + consumers. """ import examples.src.flask diff --git a/examples/tests/test_02_message_consumer.py b/examples/tests/test_02_message_consumer.py index 73457198a..b87b14a2e 100644 --- a/examples/tests/test_02_message_consumer.py +++ b/examples/tests/test_02_message_consumer.py @@ -4,7 +4,9 @@ Pact was originally designed for HTTP interactions involving a request and a response. Message Pact is an addition to Pact that allows for testing of non-HTTP interactions, such as message queues. This example demonstrates how to -use Message Pact to test whether a consumer can handle the messages it. +use Message Pact to test whether a consumer can handle the messages it. Due to +the large number of possible transports, Message Pact does not provide a mock +provider and the tests only verifies the messages. A note on terminology, the _consumer_ for Message Pact is the system that receives the message, and the _provider_ is the system that sends the message. @@ -14,11 +16,16 @@ In this example, Pact simply ensures that the consumer is capable of processing the message. The consumer need not send back a message, and any sideffects of -the message must be verified separately (such as through `assert` statements). +the message must be verified separately (such as through `assert` statements or +as part of the usual unit testing suite). > :warning: There is currently a bug whereby the `given` and -`expects_to_receive` have swapped meanings. This will be addressed in a future -release. +`expects_to_receive` have swapped meanings compared to the reference +implementation. This will be addressed in a future release. + +A good resource for understanding the message pact testing can be found [in the +Pact +documentation](https://docs.pact.io/getting_started/how_pact_works#non-http-testing-message-pact). """ from __future__ import annotations diff --git a/examples/tests/test_03_message_provider.py b/examples/tests/test_03_message_provider.py index 4f4570a63..bf75e736a 100644 --- a/examples/tests/test_03_message_provider.py +++ b/examples/tests/test_03_message_provider.py @@ -1,23 +1,32 @@ """ Test Message Pact provider. -Unlike the standard Pact, which is designed for HTTP interactions, the Message -Pact is designed for non-HTTP interactions. This example demonstrates how to use -the Message Pact to test whether a provider generates the correct messages. +Pact was originally designed for HTTP interactions involving a request and a +response. Message Pact is an addition to Pact that allows for testing of +non-HTTP interactions, such as message queues. This example demonstrates how to +use Message Pact to test whether a consumer can handle the messages it. Due to +the large number of possible transports, Message Pact does not provide a mock +provider and the tests only verifies the messages. -In such examples, Pact simply checks the kind of messages produced. The consumer -need not send back a message, and any sideffects of the message must be verified -separately. +A note on terminology, the _consumer_ for Message Pact is the system that +receives the message, and the _provider_ is the system that sends the message. +Pact is still consumer-driven, and the consumer defines the expected messages it +will receive from the provider. When the provider is being verified, Pact +ensures that the provider sends the expected messages. -The below example verifies that the consumer makes the correct filesystem calls -when it receives a message to read or write a file. The calls themselves are -mocked out so as to avoid actually writing to the filesystem. +The below example verifies that the provider sends the expected messages. The +consumer need not send back a message, and any sideffects of the message must +be verified on the consumer side. + +A good resource for understanding the message pact testing can be found [in the +Pact +documentation](https://docs.pact.io/getting_started/how_pact_works#non-http-testing-message-pact). """ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict from flask import Flask from pact import MessageProvider @@ -29,7 +38,7 @@ PACT_DIR = (Path(__file__).parent / "pacts").resolve() -def generate_write_message() -> dict[str, str]: +def generate_write_message() -> Dict[str, str]: return { "action": "WRITE", "path": "test.txt", @@ -37,7 +46,7 @@ def generate_write_message() -> dict[str, str]: } -def generate_read_message() -> dict[str, str]: +def generate_read_message() -> Dict[str, str]: return { "action": "READ", "path": "test.txt", diff --git a/hatch_build.py b/hatch_build.py index 940dba7c1..02651af21 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -6,7 +6,7 @@ import shutil import typing from pathlib import Path -from typing import Any +from typing import Any, Dict from hatchling.builders.hooks.plugin.interface import BuildHookInterface from packaging.tags import sys_tags @@ -37,7 +37,7 @@ def clean(self, versions: list[str]) -> None: # noqa: ARG002 def initialize( self, version: str, # noqa: ARG002 - build_data: dict[str, Any], + build_data: Dict[str, Any], ) -> None: """Hook into Hatchling's build process.""" build_data["infer_tag"] = True diff --git a/pyproject.toml b/pyproject.toml index fdb53cb32..e7f2d3825 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,7 @@ extra-dependencies = ["hatchling", "packaging", "requests"] lint = ["black --check --diff {args:.}", "ruff {args:.}", "mypy {args:.}"] test = "pytest --cov-config=pyproject.toml --cov=pact --cov=tests tests/" example = "pytest examples/ {args}" -all = ["lint", "tests"] +all = ["lint", "test", "example"] # Test environment for running unit tests. This automatically tests against all # supported Python versions. @@ -118,8 +118,8 @@ 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"] +example = "pytest examples/ {args}" +all = ["test", "example"] ################################################################################ ## PyTest Configuration @@ -154,5 +154,8 @@ ignore = [ "ANN102", # `cls` must be typed ] +[tool.ruff.pyupgrade] +keep-runtime-typing = true + [tool.ruff.pydocstyle] convention = "google" From e4315ea023f2766c60a9873e494c6e666e771a98 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 28 Sep 2023 11:27:36 +1000 Subject: [PATCH 0042/1376] fix(github): fix typo in template The template was adapted from the amazing Docusaurus templates, and unfortunately a mention to Docusaurus was missed. Signed-off-by: JP-Ellis --- .github/ISSUE_TEMPLATE/feature.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index a27030432..abcba7276 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -51,4 +51,4 @@ body: If you do check this box, please send a pull request. If circumstances change and you can't work on it anymore, let us know and we can re-assign it. options: - - label: I'd be willing to contribute this feature to Docusaurus myself. + - label: I'd be willing to contribute this feature to Pact Python myself. From ae019b0321b97e2af50790b040d149af7bef88e9 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 28 Sep 2023 13:48:26 +1000 Subject: [PATCH 0043/1376] chore(gitignore): update from upstream templates Signed-off-by: JP-Ellis --- .gitignore | 352 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 332 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 1dfef317e..cd43d0c34 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,28 @@ -# pact-python specific ignores -e2e/pacts -userserviceclient-userservice.json -detectcontentlambda-contentprovider.json +################################################################################ +## Pact Python Specific +################################################################################ pact/bin -pact/lib pact/data +################################################################################ +## Standard Templates +################################################################################ +## The below sections are sourced from github/gitignore +## +## The following files are included: +## +## - Python.gitignore +## - Global/Backup.gitignore +## - Global/Linux.gitignore +## - Global/Windows.gitignore +## - Global/macOS.gitignore +## - Global/VisualStudioCode.gitignore +## - Global/Emacs.gitignore +## - Global/JetBrains.gitignore + +######################################## +## Python.gitignore +######################################## # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -16,7 +33,6 @@ __pycache__/ # Distribution / packaging .Python -env/ build/ develop-eggs/ dist/ @@ -28,9 +44,12 @@ lib64/ parts/ sdist/ var/ +wheels/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST # PyInstaller # Usually these files are written by a python script from a template @@ -45,13 +64,17 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml -*,cover +*.cover +*.py,cover .hypothesis/ +.pytest_cache/ +cover/ # Translations *.mo @@ -60,14 +83,13 @@ coverage.xml # Django stuff: *.log local_settings.py +db.sqlite3 +db.sqlite3-journal # Flask stuff: instance/ .webassets-cache -# Intellij stuff -.idea/ - # Scrapy stuff: .scrapy @@ -75,34 +97,324 @@ instance/ docs/_build/ # PyBuilder +.pybuilder/ target/ -# IPython Notebook +# Jupyter Notebook .ipynb_checkpoints +# IPython +profile_default/ +ipython_config.py + # pyenv -.python-version +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock -# celery beat schedule file +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff celerybeat-schedule +celerybeat.pid -# dotenv -.env +# SageMath parsed files +*.sage.py -# virtualenv +# Environments +.env +.venv +env/ venv/ -.venv/ ENV/ +env.bak/ +venv.bak/ # Spyder project settings .spyderproject +.spyproject # Rope project settings .ropeproject -# VCode stuff +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + + +######################################## +## Global/Backup.gitignore +######################################## +*.bak +*.gho +*.ori +*.orig +*.tmp + + +######################################## +## Global/Linux.gitignore +######################################## +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +######################################## +## Global/Windows.gitignore +######################################### Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +######################################## +## Global/macOS.gitignore +######################################## +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +######################################## +## Global/VisualStudioCode.gitignore +######################################## .vscode/* -*.code-workspace +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + + +######################################## +## Global/Emacs.gitignore +######################################## +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + +######################################## +## Global/JetBrains.gitignore +######################################## +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +# Editor-based Rest Client +.idea/httpRequests -.noseids +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser From 91850f00dc546b74f3cf123522435a2c59741ed0 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 25 Sep 2023 11:12:45 +1000 Subject: [PATCH 0044/1376] feat: bump pact standalone to 2.0.7 Signed-off-by: JP-Ellis --- hatch_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hatch_build.py b/hatch_build.py index 02651af21..630fab65e 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -12,7 +12,7 @@ from packaging.tags import sys_tags ROOT_DIR = Path(__file__).parent.resolve() -PACT_VERSION = "2.0.3" +PACT_VERSION = "2.0.7" 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"), From e266ae5c3969bc8990b0f8905754141ea0aa0aba Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 28 Sep 2023 10:49:59 +1000 Subject: [PATCH 0045/1376] fix(ci): pypi publish Fix a couple of minor issues with the publishing workflow: - Only publish releases from tags starting with `v` - Fix an incorrect name in the downloading of artifacts - Fix snake_case to kebab-case in a publish parameter Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 054dfb29e..b6a0e9213 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -152,20 +152,20 @@ jobs: publish: name: Publish wheels - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') needs: [check] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v3 with: - name: artifacts + name: wheels path: wheelhouse - name: Push build artifacts to PyPI uses: pypa/gh-action-pypi-publish@v1.8.10 with: - skip_existing: true + skip-existing: true user: ${{ secrets.PYPI_USERNAME }} password: ${{ secrets.PYPI_PASSWORD }} packages-dir: wheelhouse From 77124ee0a15a9d9cb334760e4951ebebaa3e59fd Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 28 Sep 2023 08:51:49 +1000 Subject: [PATCH 0046/1376] chore: v2.1.0 Signed-off-by: JP-Ellis --- CHANGELOG.md | 1179 ++++++++++++++++++++++++------------------- pact/__version__.py | 2 +- 2 files changed, 651 insertions(+), 530 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62aa65a2d..63e10b051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,619 +1,740 @@ +### 2.1.0 + +- 82df76f - feat: bump pact standalone to 2.0.7 (JP-Ellis, Mon Sep 25 11:12:45 2023 +1000) +- 9896320 - chore: address pr comments (JP-Ellis, Wed Sep 27 10:31:56 2023 +1000) +- e86b7eb - chore(example): avoid changing python path (JP-Ellis, Fri Sep 22 13:28:26 2023 +1000) +- 045083b - chore(ci): split tests examples and lints (JP-Ellis, Thu Sep 21 12:46:44 2023 +1000) +- 3a59235 - chore(example): migrate message pact example (JP-Ellis, Wed Sep 20 13:47:26 2023 +1000) +- aa0f07e - chore(example): update readme (JP-Ellis, Wed Sep 20 13:20:06 2023 +1000) +- 9488f0e - chore(example): migrate flask provider example (JP-Ellis, Mon Sep 18 15:46:38 2023 +1000) +- dd8827a - chore(example): migrate fastapi provider example (JP-Ellis, Mon Sep 18 14:20:39 2023 +1000) +- 4c47843 - chore(example): migrate consumer example (JP-Ellis, Fri Sep 15 16:30:20 2023 +1000) +- 8ee2eb0 - feat(example): simplify docker-compose (JP-Ellis, Thu Sep 14 17:00:14 2023 +1000) +- 7c60dd5 - docs: incorporate suggestions from @YOU54F (JP-Ellis, Fri Sep 22 11:37:22 2023 +1000) +- 6d223fc - docs: add issue and pr templates (JP-Ellis, Wed Sep 20 18:04:43 2023 +1000) +- 3634a5c - docs: rewrite contributing.md (JP-Ellis, Wed Sep 20 18:00:19 2023 +1000) +- 093d9b8 - chore(ci): migrate cicd to hatch (JP-Ellis, Wed Sep 13 13:21:36 2023 +1000) +- 5b19665 - chore!: migrate to pyproject.toml and hatch (JP-Ellis, Tue Sep 12 16:13:24 2023 +1000) +- 04deeec - chore: update pre-commit config (JP-Ellis, Wed Sep 13 10:57:51 2023 +1000) +- d5017f8 - style: add pre-commit hooks and editorconfig (JP-Ellis, Thu Sep 14 11:22:58 2023 +1000) +- ed5f86c - chore: add pact-foundation triage automation (Matt Fellows, Fri Aug 4 16:37:05 2023 +1000) + ### 2.0.1 - * d3397b7 - chore(examples): update docker setup for non linux os (Yousaf Nabi, Tue Jul 25 14:55:42 2023 +0100) - * ef12e56 - feat: update standalone to 2.0.3 (Yousaf Nabi, Tue Jul 25 14:00:38 2023 +0100) - * 1429d2f - chore: update MANIFEST file to note 2.0.2 standalone (Yousaf Nabi, Tue Jul 25 13:56:08 2023 +0100) + +- d3397b7 - chore(examples): update docker setup for non linux os (Yousaf Nabi, Tue Jul 25 14:55:42 2023 +0100) +- ef12e56 - feat: update standalone to 2.0.3 (Yousaf Nabi, Tue Jul 25 14:00:38 2023 +0100) +- 1429d2f - chore: update MANIFEST file to note 2.0.2 standalone (Yousaf Nabi, Tue Jul 25 13:56:08 2023 +0100) + ### 2.0.0 - * 2a244ea - chore: update to 2.0.2 pact-ruby-standalone (Yousaf Nabi, Sat Jul 8 14:12:18 2023 +0100) - * 819f0a7 - test: v2.0.1 (pact-2.0.1) - pact-ruby-standalone (Yousaf Nabi, Thu May 18 23:30:30 2023 +0100) - * 9bc3e21 - chore(docs): improve table alignment and abs links (Yousaf Nabi, Thu May 4 12:10:39 2023 +0100) - * 80f06cf - chore(docs): correct table (Yousaf Nabi, Wed May 3 19:20:37 2023 +0100) - * c70573c - chore(docs): update provider verifier options table (Yousaf Nabi, Wed May 3 19:17:28 2023 +0100) - * fc6ced8 - style: add missing newline/linefeed (Serghei Iakovlev, Thu May 4 09:51:19 2023 +0200) - * 7b14aa3 - build(deps-dev): bump flask from 2.2.2 to 2.2.5 (dependabot[bot], Wed May 3 20:30:31 2023 +0000) - * a0efd69 - build(deps): bump flask from 2.2.2 to 2.2.5 in /examples/flask_provider (dependabot[bot], Wed May 3 20:30:24 2023 +0000) - * 1267d7d - build(deps): bump flask from 2.2.2 to 2.2.5 in /examples/message (dependabot[bot], Wed May 3 19:51:09 2023 +0000) - * 4e3ca38 - feat: use pact-ruby-standalone 2.0.0 release (Yousaf Nabi, Sat Apr 29 00:43:31 2023 +0100) - * 2c673ea - ci: skip 3.6 python arm64 failing in cirrus, passing locally with cirrus run (Yousaf Nabi, Fri Apr 21 15:46:22 2023 +0100) - * 7aff538 - feat: support x86 and x86_64 windows (Yousaf Nabi, Fri Apr 21 15:44:48 2023 +0100) - * 28440da - ci: test arm64 on cirrus-ci / test win/osx on gh (Yousaf Nabi, Fri Apr 21 15:37:30 2023 +0100) - * 93db8ae - feat: support arm64 osx/linux (Yousaf Nabi, Fri Apr 21 12:35:23 2023 +0100) - * 19be499 - fix: fix cors parameter not doing anything (Lukas Riedersberger, Fri Apr 14 12:22:21 2023 +0200) - * e721d81 - docs: reformat releasing documentation (Serghei Iakovlev, Wed Apr 5 12:39:35 2023 +0200) - * 71f1529 - chore: do not add merge commits to the change log (Serghei Iakovlev, Wed Apr 5 12:27:49 2023 +0200) - * 9ce2d69 - chore: Releasing version 1.7.0 (Elliott Murray, Sun Feb 19 11:28:01 2023 +0000) - * 429e171 - build: use a single Dockerfile, providing args for the Python version instead of multiple files (Mike Geeves, Mon Apr 3 09:01:37 2023 +0100) - * e99e7fb - docs: rephrase the instructions for running the tests (Serghei Iakovlev, Sun Apr 2 22:20:07 2023 +0200) - * a5d3a2e - docs: paraphrase the instructions for running the tests (Serghei Iakovlev, Sun Apr 2 22:19:37 2023 +0200) - * 24c2dbf - docs: fix instruction to build python 3.11 image (Serghei Iakovlev, Sun Apr 2 22:18:10 2023 +0200) - * 55dcaf2 - feat(test): add docker images for Python 3.9-3.11 for testing purposes (Serghei Iakovlev, Fri Mar 17 11:24:42 2023 +0100) - * 28fc7d3 - docs: fix link for GitHub badge (Serghei Iakovlev, Fri Mar 31 22:50:23 2023 +0200) - * 26eaaac - fix: remove dead code (Serghei Iakovlev, Sun Mar 5 02:05:14 2023 +0100) - * f7c5006 - docs: add Python 3.11 to CONTRIBUTING.md (Serghei Iakovlev, Thu Mar 30 23:22:22 2023 +0200) - * 348bf5e - build: use compatible dependency versions for Python 3.6 (Serghei Iakovlev, Thu Mar 30 23:18:57 2023 +0200) - * 4d9f4cd - feat: describe classifiers and python version for pypi package (Serghei Iakovlev, Sun Mar 5 09:16:29 2023 +0100) - * 7603815 - ci: add python 3.11 to test matrix (Serghei Iakovlev, Sun Mar 5 09:15:23 2023 +0100) - * bea1563 - doc: improve commit messages guide (Serghei Iakovlev, Sat Mar 4 00:30:56 2023 +0100) - * 60f2aac - doc: correct links in contributing manual (Serghei Iakovlev, Fri Mar 3 21:38:58 2023 +0100) - * a219f49 - fix: actualize doc on how to make contributions (Serghei Iakovlev, Thu Mar 2 08:56:48 2023 +0100) - * 4919772 - feat: add matchers for ISO 8601 date format (Serghei Iakovlev, Sun Mar 12 16:03:44 2023 +0100) + +- 2a244ea - chore: update to 2.0.2 pact-ruby-standalone (Yousaf Nabi, Sat Jul 8 14:12:18 2023 +0100) +- 819f0a7 - test: v2.0.1 (pact-2.0.1) - pact-ruby-standalone (Yousaf Nabi, Thu May 18 23:30:30 2023 +0100) +- 9bc3e21 - chore(docs): improve table alignment and abs links (Yousaf Nabi, Thu May 4 12:10:39 2023 +0100) +- 80f06cf - chore(docs): correct table (Yousaf Nabi, Wed May 3 19:20:37 2023 +0100) +- c70573c - chore(docs): update provider verifier options table (Yousaf Nabi, Wed May 3 19:17:28 2023 +0100) +- fc6ced8 - style: add missing newline/linefeed (Serghei Iakovlev, Thu May 4 09:51:19 2023 +0200) +- 7b14aa3 - build(deps-dev): bump flask from 2.2.2 to 2.2.5 (dependabot[bot], Wed May 3 20:30:31 2023 +0000) +- a0efd69 - build(deps): bump flask from 2.2.2 to 2.2.5 in /examples/flask_provider (dependabot[bot], Wed May 3 20:30:24 2023 +0000) +- 1267d7d - build(deps): bump flask from 2.2.2 to 2.2.5 in /examples/message (dependabot[bot], Wed May 3 19:51:09 2023 +0000) +- 4e3ca38 - feat: use pact-ruby-standalone 2.0.0 release (Yousaf Nabi, Sat Apr 29 00:43:31 2023 +0100) +- 2c673ea - ci: skip 3.6 python arm64 failing in cirrus, passing locally with cirrus run (Yousaf Nabi, Fri Apr 21 15:46:22 2023 +0100) +- 7aff538 - feat: support x86 and x86_64 windows (Yousaf Nabi, Fri Apr 21 15:44:48 2023 +0100) +- 28440da - ci: test arm64 on cirrus-ci / test win/osx on gh (Yousaf Nabi, Fri Apr 21 15:37:30 2023 +0100) +- 93db8ae - feat: support arm64 osx/linux (Yousaf Nabi, Fri Apr 21 12:35:23 2023 +0100) +- 19be499 - fix: fix cors parameter not doing anything (Lukas Riedersberger, Fri Apr 14 12:22:21 2023 +0200) +- e721d81 - docs: reformat releasing documentation (Serghei Iakovlev, Wed Apr 5 12:39:35 2023 +0200) +- 71f1529 - chore: do not add merge commits to the change log (Serghei Iakovlev, Wed Apr 5 12:27:49 2023 +0200) +- 9ce2d69 - chore: Releasing version 1.7.0 (Elliott Murray, Sun Feb 19 11:28:01 2023 +0000) +- 429e171 - build: use a single Dockerfile, providing args for the Python version instead of multiple files (Mike Geeves, Mon Apr 3 09:01:37 2023 +0100) +- e99e7fb - docs: rephrase the instructions for running the tests (Serghei Iakovlev, Sun Apr 2 22:20:07 2023 +0200) +- a5d3a2e - docs: paraphrase the instructions for running the tests (Serghei Iakovlev, Sun Apr 2 22:19:37 2023 +0200) +- 24c2dbf - docs: fix instruction to build python 3.11 image (Serghei Iakovlev, Sun Apr 2 22:18:10 2023 +0200) +- 55dcaf2 - feat(test): add docker images for Python 3.9-3.11 for testing purposes (Serghei Iakovlev, Fri Mar 17 11:24:42 2023 +0100) +- 28fc7d3 - docs: fix link for GitHub badge (Serghei Iakovlev, Fri Mar 31 22:50:23 2023 +0200) +- 26eaaac - fix: remove dead code (Serghei Iakovlev, Sun Mar 5 02:05:14 2023 +0100) +- f7c5006 - docs: add Python 3.11 to CONTRIBUTING.md (Serghei Iakovlev, Thu Mar 30 23:22:22 2023 +0200) +- 348bf5e - build: use compatible dependency versions for Python 3.6 (Serghei Iakovlev, Thu Mar 30 23:18:57 2023 +0200) +- 4d9f4cd - feat: describe classifiers and python version for pypi package (Serghei Iakovlev, Sun Mar 5 09:16:29 2023 +0100) +- 7603815 - ci: add python 3.11 to test matrix (Serghei Iakovlev, Sun Mar 5 09:15:23 2023 +0100) +- bea1563 - doc: improve commit messages guide (Serghei Iakovlev, Sat Mar 4 00:30:56 2023 +0100) +- 60f2aac - doc: correct links in contributing manual (Serghei Iakovlev, Fri Mar 3 21:38:58 2023 +0100) +- a219f49 - fix: actualize doc on how to make contributions (Serghei Iakovlev, Thu Mar 2 08:56:48 2023 +0100) +- 4919772 - feat: add matchers for ISO 8601 date format (Serghei Iakovlev, Sun Mar 12 16:03:44 2023 +0100) + ### 1.7.0 - * 44cda33 - chore: /s/Pactflow/PactFlow (Yousaf Nabi, Thu Jan 26 16:11:54 2023 +0000) - * 1bbdd37 - feat: Enhance provider states for pact-message (#322) (nsfrias, Tue Jan 24 17:04:29 2023 +0000) - * 53ca129 - chore: add workflow to create a jira issue for pactflow team when smartbear-supported label added to github issue (Beth Skurrie, Wed Jan 18 10:51:05 2023 +1100) - * d87d54b - fix: setup security issue (#318) (Elliott Murray, Mon Nov 21 09:39:41 2022 +0000) - * 55f2a64 - fix: requirements_dev.txt to reduce vulnerabilities (#317) (Matt Fellows, Sun Nov 6 02:12:30 2022 +1100) + +- 44cda33 - chore: /s/Pactflow/PactFlow (Yousaf Nabi, Thu Jan 26 16:11:54 2023 +0000) +- 1bbdd37 - feat: Enhance provider states for pact-message (#322) (nsfrias, Tue Jan 24 17:04:29 2023 +0000) +- 53ca129 - chore: add workflow to create a jira issue for pactflow team when smartbear-supported label added to github issue (Beth Skurrie, Wed Jan 18 10:51:05 2023 +1100) +- d87d54b - fix: setup security issue (#318) (Elliott Murray, Mon Nov 21 09:39:41 2022 +0000) +- 55f2a64 - fix: requirements_dev.txt to reduce vulnerabilities (#317) (Matt Fellows, Sun Nov 6 02:12:30 2022 +1100) + ### 1.6.0 - * ceff89b - Publish verify branches (#306) (Yousaf Nabi, Sun Sep 11 11:33:44 2022 +0100) - * 89733d6 - feat: Support verify with branch (#302) (B3nnyL, Sun Sep 11 20:14:13 2022 +1000) - * 42e0db8 - feat: Support publish pact with branch (#300) (B3nnyL, Sun Sep 11 20:06:27 2022 +1000) - * 80d7b13 - chore(test): fix consumer message test (#301) (B3nnyL, Tue Aug 23 23:50:27 2022 +1000) - * 2015f72 - build: Correct download logic when installing. Add a helper target to setup a pyenv via make (#297) (mikegeeves, Sun Jun 19 09:27:07 2022 +0100) - * c17ac70 - docs: Update docs to reflect usage for native Python (#227) (Jiayun Fang, Wed Apr 27 10:00:50 2022 -0700) + +- ceff89b - Publish verify branches (#306) (Yousaf Nabi, Sun Sep 11 11:33:44 2022 +0100) +- 89733d6 - feat: Support verify with branch (#302) (B3nnyL, Sun Sep 11 20:14:13 2022 +1000) +- 42e0db8 - feat: Support publish pact with branch (#300) (B3nnyL, Sun Sep 11 20:06:27 2022 +1000) +- 80d7b13 - chore(test): fix consumer message test (#301) (B3nnyL, Tue Aug 23 23:50:27 2022 +1000) +- 2015f72 - build: Correct download logic when installing. Add a helper target to setup a pyenv via make (#297) (mikegeeves, Sun Jun 19 09:27:07 2022 +0100) +- c17ac70 - docs: Update docs to reflect usage for native Python (#227) (Jiayun Fang, Wed Apr 27 10:00:50 2022 -0700) + ### 1.5.2 - * 25823ae - chore: update PACT_STANDALONE_VERSION to 1.88.83 (#292) (Yousaf Nabi, Mon Mar 21 22:14:40 2022 +0000) + +- 25823ae - chore: update PACT_STANDALONE_VERSION to 1.88.83 (#292) (Yousaf Nabi, Mon Mar 21 22:14:40 2022 +0000) + ### 1.5.1 - * e645b24 - feat: message_pact -> with_metadata() updated to accept term (#289) (sunsathish88, Tue Mar 8 12:08:34 2022 -0500) - * b981865 - docs(examples-consumer): add pip install requirements to the consumer… (#291) (mikegeeves, Sun Mar 6 10:12:32 2022 +0000) - * 4c76ae8 - test(examples): move shared fixtures to a common folder so they can b… (#280) (mikegeeves, Sun Mar 6 10:10:11 2022 +0000) + +- e645b24 - feat: message_pact -> with_metadata() updated to accept term (#289) (sunsathish88, Tue Mar 8 12:08:34 2022 -0500) +- b981865 - docs(examples-consumer): add pip install requirements to the consumer… (#291) (mikegeeves, Sun Mar 6 10:12:32 2022 +0000) +- 4c76ae8 - test(examples): move shared fixtures to a common folder so they can b… (#280) (mikegeeves, Sun Mar 6 10:10:11 2022 +0000) + ### 1.5.0 - * 8085be0 - feat: No include pending (#284) (Abraham Gonzalez, Wed Feb 2 13:20:39 2022 +0100) - * f169f3b - ci: python36-support-removed (#283) (mikegeeves, Sat Jan 22 10:26:44 2022 +0000) + +- 8085be0 - feat: No include pending (#284) (Abraham Gonzalez, Wed Feb 2 13:20:39 2022 +0100) +- f169f3b - ci: python36-support-removed (#283) (mikegeeves, Sat Jan 22 10:26:44 2022 +0000) + ### 1.4.6 - * 6c25844 - chore: flake8 config to ignore direnv (Elliott Murray, Mon Jan 3 18:33:47 2022 +0000) - * 891134a - feat(matcher): Allow bytes type in from_term function (#281) (joshua-badger, Mon Jan 3 11:23:40 2022 -0700) - * 588b55d - fix(consumer): ensure a description is provided for all interactions (#278) (mikegeeves, Thu Dec 30 16:57:03 2021 +0000) - * 02643d4 - test(examples-fastapi): tidy FastAPI example, making consistent with Flask (#274) (mikegeeves, Sun Oct 31 21:52:54 2021 +0000) - * bf110e2 - docs: Docs/examples (#273) (Elliott Murray, Tue Oct 26 21:54:00 2021 +0100) + +- 6c25844 - chore: flake8 config to ignore direnv (Elliott Murray, Mon Jan 3 18:33:47 2022 +0000) +- 891134a - feat(matcher): Allow bytes type in from_term function (#281) (joshua-badger, Mon Jan 3 11:23:40 2022 -0700) +- 588b55d - fix(consumer): ensure a description is provided for all interactions (#278) (mikegeeves, Thu Dec 30 16:57:03 2021 +0000) +- 02643d4 - test(examples-fastapi): tidy FastAPI example, making consistent with Flask (#274) (mikegeeves, Sun Oct 31 21:52:54 2021 +0000) +- bf110e2 - docs: Docs/examples (#273) (Elliott Murray, Tue Oct 26 21:54:00 2021 +0100) + ### 1.4.5 - * 695d51f - fix: update standalone to 1.88.77 to fix Let's Encrypt CA issue (Matt Fellows, Mon Oct 11 13:29:34 2021 +1100) + +- 695d51f - fix: update standalone to 1.88.77 to fix Let's Encrypt CA issue (Matt Fellows, Mon Oct 11 13:29:34 2021 +1100) + ### 1.4.4 - * b90cf3d - fix(ruby): update ruby standalone to support disabling SSL verification via an environment variable (m-aciek, Sat Oct 2 03:04:14 2021 +0200) + +- b90cf3d - fix(ruby): update ruby standalone to support disabling SSL verification via an environment variable (m-aciek, Sat Oct 2 03:04:14 2021 +0200) + ### 1.4.3 - * 08f0dc0 - feat: added support for message provider using pact broker (#257) (Fabio Pulvirenti, Sun Sep 5 22:49:51 2021 +0200) + +- 08f0dc0 - feat: added support for message provider using pact broker (#257) (Fabio Pulvirenti, Sun Sep 5 22:49:51 2021 +0200) + ### 1.4.2 - * f2230b6 - chore: Bundle Ruby standalones into dist artifact. (#256) (Taj Pereira, Sun Aug 22 19:53:53 2021 +0930) - * e370786 - chore: Releasing version 1.4.1 (Elliott Murray, Tue Aug 17 18:55:53 2021 +0100) - * 7dc8864 - fix: make uvicorn versions over 0.14 (#255) (Elliott Murray, Tue Aug 17 18:51:52 2021 +0100) - * da49cd7 - chore: Releasing version 1.4.0 (Elliott Murray, Sat Aug 7 10:17:26 2021 +0100) - * 0089937 - fix: issue originating from snyk with requests and urllib (#252) (Elliott Murray, Sat Jul 31 12:46:15 2021 +0100) - * 903371b - feat: added support for message provider (#251) (Fabio Pulvirenti, Sat Jul 31 13:24:19 2021 +0200) - * 2c81029 - chore(snyk): update fastapi (#239) (Elliott Murray, Fri Jun 11 09:12:38 2021 +0100) + +- f2230b6 - chore: Bundle Ruby standalones into dist artifact. (#256) (Taj Pereira, Sun Aug 22 19:53:53 2021 +0930) +- e370786 - chore: Releasing version 1.4.1 (Elliott Murray, Tue Aug 17 18:55:53 2021 +0100) +- 7dc8864 - fix: make uvicorn versions over 0.14 (#255) (Elliott Murray, Tue Aug 17 18:51:52 2021 +0100) +- da49cd7 - chore: Releasing version 1.4.0 (Elliott Murray, Sat Aug 7 10:17:26 2021 +0100) +- 0089937 - fix: issue originating from snyk with requests and urllib (#252) (Elliott Murray, Sat Jul 31 12:46:15 2021 +0100) +- 903371b - feat: added support for message provider (#251) (Fabio Pulvirenti, Sat Jul 31 13:24:19 2021 +0200) +- 2c81029 - chore(snyk): update fastapi (#239) (Elliott Murray, Fri Jun 11 09:12:38 2021 +0100) + ### 1.4.1 - * 7dc8864 - fix: make uvicorn versions over 0.14 (#255) (Elliott Murray, Tue Aug 17 18:51:52 2021 +0100) + +- 7dc8864 - fix: make uvicorn versions over 0.14 (#255) (Elliott Murray, Tue Aug 17 18:51:52 2021 +0100) + ### 1.4.0 - * 0089937 - fix: issue originating from snyk with requests and urllib (#252) (Elliott Murray, Sat Jul 31 12:46:15 2021 +0100) - * 903371b - feat: added support for message provider (#251) (Fabio Pulvirenti, Sat Jul 31 13:24:19 2021 +0200) - * 2c81029 - chore(snyk): update fastapi (#239) (Elliott Murray, Fri Jun 11 09:12:38 2021 +0100) + +- 0089937 - fix: issue originating from snyk with requests and urllib (#252) (Elliott Murray, Sat Jul 31 12:46:15 2021 +0100) +- 903371b - feat: added support for message provider (#251) (Fabio Pulvirenti, Sat Jul 31 13:24:19 2021 +0200) +- 2c81029 - chore(snyk): update fastapi (#239) (Elliott Murray, Fri Jun 11 09:12:38 2021 +0100) + ### 1.3.9 - * 98d9a4b - chore(ruby): update ruby standalen (#233) (Elliott Murray, Thu May 13 20:21:10 2021 +0100) - * 657e770 - fix: change default from empty string to empty list (#235) (Vasile Tofan, Thu May 13 22:20:47 2021 +0300) - * 99fd965 - chore: Releasing version 1.3.8 (Elliott Murray, Sat May 1 12:26:47 2021 +0100) - * 3c909f1 - docs: example uses date matcher (#231) (Elliott Murray, Sat May 1 11:51:28 2021 +0100) - * 6390144 - fix: fix datetime serialization issues in Format (#230) (Syed Muhammad Dawoud Sheraz Ali, Thu Apr 29 01:49:53 2021 +0500) + +- 98d9a4b - chore(ruby): update ruby standalen (#233) (Elliott Murray, Thu May 13 20:21:10 2021 +0100) +- 657e770 - fix: change default from empty string to empty list (#235) (Vasile Tofan, Thu May 13 22:20:47 2021 +0300) +- 99fd965 - chore: Releasing version 1.3.8 (Elliott Murray, Sat May 1 12:26:47 2021 +0100) +- 3c909f1 - docs: example uses date matcher (#231) (Elliott Murray, Sat May 1 11:51:28 2021 +0100) +- 6390144 - fix: fix datetime serialization issues in Format (#230) (Syed Muhammad Dawoud Sheraz Ali, Thu Apr 29 01:49:53 2021 +0500) + ### 1.3.8 - * 3c909f1 - docs: example uses date matcher (#231) (Elliott Murray, Sat May 1 11:51:28 2021 +0100) - * 6390144 - fix: fix datetime serialization issues in Format (#230) (Syed Muhammad Dawoud Sheraz Ali, Thu Apr 29 01:49:53 2021 +0500) + +- 3c909f1 - docs: example uses date matcher (#231) (Elliott Murray, Sat May 1 11:51:28 2021 +0100) +- 6390144 - fix: fix datetime serialization issues in Format (#230) (Syed Muhammad Dawoud Sheraz Ali, Thu Apr 29 01:49:53 2021 +0500) + ### 1.3.7 - * 20f828f - fix(broker): token added to verify steps (#226) (Elliott Murray, Sat Apr 24 13:47:22 2021 +0100) - * c4fe422 - chore: Releasing version 1.3.6 (Elliott Murray, Tue Apr 20 20:58:50 2021 +0100) - * 34160a8 - fix: publish verification results was wrong (#222) (Elliott Murray, Tue Apr 20 20:58:20 2021 +0100) - * 2c0252c - Merge pull request #219 from pact-foundation/ci/revert_snyk_36 (Elliott Murray, Sat Apr 3 11:13:49 2021 +0100) - * 1a162cf - ci: revert docker36 back (Elliott Murray, Sat Apr 3 11:00:37 2021 +0100) - * 4282de4 - Merge pull request #217 from pact-foundation/snyk-fix-8f994ea63cfa41070b04b182dbd11c74 (Elliott Murray, Sat Apr 3 10:54:58 2021 +0100) - * 4eb3fbb - Merge pull request #216 from pact-foundation/snyk-fix-fc54d9c7fe536fffe78fbd34fc5fd7ea (Elliott Murray, Sat Apr 3 10:54:10 2021 +0100) - * 47373ff - fix: docker/py37.Dockerfile to reduce vulnerabilities (snyk-bot, Fri Apr 2 03:13:49 2021 +0000) - * e572221 - fix: docker/py38.Dockerfile to reduce vulnerabilities (snyk-bot, Fri Apr 2 01:10:57 2021 +0000) - * f293dbb - Merge pull request #215 from pact-foundation/snyk-fix-ab489d8931bdf95d6ef0d217aa1b2eb6 (Elliott Murray, Wed Mar 31 09:27:02 2021 +0100) - * 5946872 - fix: docker/py36.Dockerfile to reduce vulnerabilities (snyk-bot, Tue Mar 30 21:41:35 2021 +0000) + +- 20f828f - fix(broker): token added to verify steps (#226) (Elliott Murray, Sat Apr 24 13:47:22 2021 +0100) +- c4fe422 - chore: Releasing version 1.3.6 (Elliott Murray, Tue Apr 20 20:58:50 2021 +0100) +- 34160a8 - fix: publish verification results was wrong (#222) (Elliott Murray, Tue Apr 20 20:58:20 2021 +0100) +- 2c0252c - Merge pull request #219 from pact-foundation/ci/revert_snyk_36 (Elliott Murray, Sat Apr 3 11:13:49 2021 +0100) +- 1a162cf - ci: revert docker36 back (Elliott Murray, Sat Apr 3 11:00:37 2021 +0100) +- 4282de4 - Merge pull request #217 from pact-foundation/snyk-fix-8f994ea63cfa41070b04b182dbd11c74 (Elliott Murray, Sat Apr 3 10:54:58 2021 +0100) +- 4eb3fbb - Merge pull request #216 from pact-foundation/snyk-fix-fc54d9c7fe536fffe78fbd34fc5fd7ea (Elliott Murray, Sat Apr 3 10:54:10 2021 +0100) +- 47373ff - fix: docker/py37.Dockerfile to reduce vulnerabilities (snyk-bot, Fri Apr 2 03:13:49 2021 +0000) +- e572221 - fix: docker/py38.Dockerfile to reduce vulnerabilities (snyk-bot, Fri Apr 2 01:10:57 2021 +0000) +- f293dbb - Merge pull request #215 from pact-foundation/snyk-fix-ab489d8931bdf95d6ef0d217aa1b2eb6 (Elliott Murray, Wed Mar 31 09:27:02 2021 +0100) +- 5946872 - fix: docker/py36.Dockerfile to reduce vulnerabilities (snyk-bot, Tue Mar 30 21:41:35 2021 +0000) + ### 1.3.6 - * 34160a8 - fix: publish verification results was wrong (#222) (Elliott Murray, Tue Apr 20 20:58:20 2021 +0100) - * 2c0252c - Merge pull request #219 from pact-foundation/ci/revert_snyk_36 (Elliott Murray, Sat Apr 3 11:13:49 2021 +0100) - * 1a162cf - ci: revert docker36 back (Elliott Murray, Sat Apr 3 11:00:37 2021 +0100) - * 4282de4 - Merge pull request #217 from pact-foundation/snyk-fix-8f994ea63cfa41070b04b182dbd11c74 (Elliott Murray, Sat Apr 3 10:54:58 2021 +0100) - * 4eb3fbb - Merge pull request #216 from pact-foundation/snyk-fix-fc54d9c7fe536fffe78fbd34fc5fd7ea (Elliott Murray, Sat Apr 3 10:54:10 2021 +0100) - * 47373ff - fix: docker/py37.Dockerfile to reduce vulnerabilities (snyk-bot, Fri Apr 2 03:13:49 2021 +0000) - * e572221 - fix: docker/py38.Dockerfile to reduce vulnerabilities (snyk-bot, Fri Apr 2 01:10:57 2021 +0000) - * f293dbb - Merge pull request #215 from pact-foundation/snyk-fix-ab489d8931bdf95d6ef0d217aa1b2eb6 (Elliott Murray, Wed Mar 31 09:27:02 2021 +0100) - * 5946872 - fix: docker/py36.Dockerfile to reduce vulnerabilities (snyk-bot, Tue Mar 30 21:41:35 2021 +0000) - * d6c5f4a - chore: Releasing version 1.3.5 (Elliott Murray, Sun Mar 28 15:32:45 2021 +0100) - * 5864e47 - Merge pull request #213 from pact-foundation/fix/revert_some_publish (Elliott Murray, Sun Mar 28 15:27:50 2021 +0100) - * 94e597a - fix(publish): fixing the fix. Pact Python api uses only publish_version and ensures it follows that (Elliott Murray, Sun Mar 28 15:20:04 2021 +0100) - * e00f320 - chore: Releasing version 1.3.4 (Elliott Murray, Sat Mar 27 21:26:29 2021 +0000) - * c778c71 - Merge pull request #212 from pact-foundation/fix/verify_in_provider (Elliott Murray, Sat Mar 27 18:59:25 2021 +0000) - * ea0b64a - fix: verifier should now publish (Elliott Murray, Sat Mar 27 16:21:25 2021 +0000) - * 2c8779b - chore: Releasing version 1.3.3 (Elliott Murray, Thu Mar 25 21:23:29 2021 +0000) - * 5e282ff - Merge pull request #211 from anneschuth/fix/pass-pact-dir (Elliott Murray, Thu Mar 25 21:21:56 2021 +0000) - * 987c4fc - fix: pass pact_dir to publish() (Anne Schuth, Thu Mar 25 10:06:54 2021 +0100) - * 23a5129 - chore: Releasing version 1.3.2 (Elliott Murray, Sun Mar 21 14:32:50 2021 +0000) - * 57c8ae8 - Merge pull request #209 from pact-foundation/bug/fix_test_dir (Elliott Murray, Sun Mar 21 14:29:40 2021 +0000) - * af3dadf - fix: remove pacts from examples (Elliott Murray, Sun Mar 21 14:21:10 2021 +0000) - * 579f3f8 - fix: ensure path is passed to broker and allow running from root rather than test file (Elliott Murray, Sun Mar 21 13:18:01 2021 +0000) - * 7e0feab - Merge pull request #208 from pact-foundation/dependabot/pip/examples/e2e/jinja2-2.11.3 (Elliott Murray, Sat Mar 20 12:46:32 2021 +0000) - * f82c008 - chore(deps): bump jinja2 from 2.11.2 to 2.11.3 in /examples/e2e (dependabot[bot], Sat Mar 20 04:53:58 2021 +0000) - * ea6f635 - Merge pull request #206 from pact-foundation/chore/use-testcontainers (Elliott Murray, Sun Mar 14 11:05:01 2021 +0000) - * 565bc80 - chore: added some docs about how to use the e2e example (Elliott Murray, Sun Mar 14 11:02:35 2021 +0000) - * 1b4be80 - chore: spiking testcontainers (Elliott Murray, Sat Mar 13 11:15:52 2021 +0000) - * 01e4ec4 - chore: wip on using test containers on examples (Elliott Murray, Tue Mar 2 21:30:48 2021 +0000) - * 882f4a2 - Merge pull request #204 from pact-foundation/chore/use_pytest (Elliott Murray, Sat Feb 27 10:58:19 2021 +0000) - * 261f24b - chore: more clean up (Elliott Murray, Sat Feb 27 10:10:23 2021 +0000) - * 5a0934c - chore: update ci stuff (Elliott Murray, Sat Feb 27 09:57:31 2021 +0000) - * 66e79e2 - chore: move from nose to pytests as we are now 3.6+ (Elliott Murray, Sat Feb 27 09:34:37 2021 +0000) - * 6aee3e2 - chore: Releasing version 1.3.1 (Elliott Murray, Sat Feb 27 09:16:35 2021 +0000) - * 4440022 - Merge pull request #203 from pact-foundation/fix/version_confusion (Elliott Murray, Sat Feb 27 09:15:08 2021 +0000) - * 9cac2d7 - fix: introduced and renamed specification version (Elliott Murray, Tue Feb 23 21:22:36 2021 +0000) - * 64d7bdc - chore: Releasing version 1.3.0 (Elliott Murray, Tue Jan 26 18:45:58 2021 +0000) - * eaa90e1 - Merge pull request #194 from williaminfante/feat/pact-message-2 (Elliott Murray, Mon Jan 25 08:48:00 2021 +0000) - * 5ed73db - test: consider publish to broker with no pact_dir argument (William Infante, Mon Jan 25 17:19:08 2021 +1100) - * e097153 - docs: update readme (William Infante, Mon Jan 25 17:18:35 2021 +1100) - * fc0d91c - feat: address PR comments (William Infante, Mon Jan 25 10:30:33 2021 +1100) - * 5448d8c - test: remove mock and check generated json file (William Infante, Wed Jan 20 21:07:39 2021 +1100) - * abd3574 - fix: few more tests to improve coverage (Tuan Pham, Wed Jan 20 09:23:13 2021 +1100) - * 0ef971f - fix: improve test coverage (Tuan Pham, Tue Jan 19 15:11:11 2021 +1100) - * e543f04 - chore: add missing import (William Infante, Tue Jan 19 13:58:17 2021 +1100) - * bc7ff78 - chore: pydocstyle (Tuan Pham, Tue Jan 19 10:40:12 2021 +1100) - * d4235f9 - chore: flake8, clean up deadcode (Tuan Pham, Tue Jan 19 00:53:03 2021 +1100) - * 19827d0 - chore: remove test param for provider (Tuan Pham, Mon Jan 18 16:06:42 2021 +1100) - * 912b477 - chore: flake8 revert (Tuan Pham, Mon Jan 18 16:04:00 2021 +1100) - * 4a730ae - fix: revert changes to quotes (Tuan Pham, Mon Jan 18 15:57:14 2021 +1100) - * cfe35cc - feat: update message hander to be independent of pact (William Infante, Mon Jan 18 13:12:17 2021 +1100) - * 12b4a50 - fix: flake8 warning (Tuan Pham, Mon Jan 18 12:50:50 2021 +1100) - * 7afe693 - test: update message handler condition based on content (William Infante, Mon Jan 18 12:37:50 2021 +1100) - * 79106bb - feat: move publish function to broker class (Tuan Pham, Mon Jan 18 11:30:35 2021 +1100) - * a04b954 - docs: add readme for message consumer (William Infante, Mon Jan 18 09:48:01 2021 +1100) - * 8672e2f - feat: update handler to handle error exceptions (William Infante, Fri Jan 15 16:24:38 2021 +1100) - * 47b7434 - feat: change dummy handler to a message handler (William Infante, Fri Jan 15 14:06:55 2021 +1100) - * a18ce3e - test: create external dummy handler in test (William Infante, Fri Jan 15 12:31:49 2021 +1100) - * 7358049 - chore: remove log_dir, refactor test (Tuan Pham, Fri Jan 15 09:11:55 2021 +1100) - * 6718b4c - feat: update message pact tests (William Infante, Thu Jan 14 16:12:44 2021 +1100) - * bf9864f - feat: add more test (William Infante, Thu Jan 14 16:09:52 2021 +1100) - * 84681b4 - fix: try different way to mock (Tuan Pham, Thu Jan 14 15:10:36 2021 +1100) - * 86cfe8e - chore: add generate_pact_test (Tuan Pham, Thu Jan 14 14:26:42 2021 +1100) - * a1c19b6 - fix: add missing conftest (Tuan Pham, Thu Jan 14 11:02:08 2021 +1100) - * a98850a - chore: add missing files in src (Tuan Pham, Thu Jan 14 10:53:35 2021 +1100) - * 63452aa - chore: fix bad merge (Tuan Pham, Thu Jan 14 10:47:43 2021 +1100) - * 85fc77f - feat: add pact-message integration test (Tuan Pham, Thu Jan 14 10:32:02 2021 +1100) - * 11793f5 - feat: add pact-message integration (Tuan Pham, Thu Jan 14 10:30:35 2021 +1100) - * 8546a26 - fix: linting (Tuan Pham, Thu Jan 14 10:38:52 2021 +1100) - * 65b69d7 - fix: remove publish fn for now (Tuan Pham, Thu Jan 14 10:38:10 2021 +1100) - * e31dd45 - feat: add constants test (William Infante, Wed Jan 13 17:28:45 2021 +1100) - * 955dbe1 - feat: update MessageConsumer and tests (William Infante, Wed Jan 13 16:38:30 2021 +1100) - * af5c9fb - feat: create basic tests for single pact message (William Infante, Wed Jan 13 15:52:07 2021 +1100) - * fea27c8 - feat: single message flow (William Infante, Wed Jan 13 15:03:41 2021 +1100) - * 9047855 - feat: add MessageConsumer (William Infante, Wed Jan 13 12:45:19 2021 +1100) - * 40edd39 - feat: initial interface (William Infante, Tue Jan 12 16:59:46 2021 +1100) - * 2946242 - Merge pull request #198 from pact-foundation/chore/deprecate_python35 (Elliott Murray, Sat Jan 23 15:26:01 2021 +0000) - * 0111698 - fix: add e2e example test into ci back in (Elliott Murray, Sat Jan 23 15:25:07 2021 +0000) - * 0cdc7e9 - chore: remove python35 and 34 and add 39 (Elliott Murray, Sat Jan 23 15:20:47 2021 +0000) - * 3885c60 - Merge pull request #197 from pact-foundation/fix/pull_request_trigger_workflow (Elliott Murray, Sat Jan 23 10:51:06 2021 +0000) - * 925b0ac - ci: pr not triggering workflow (Elliott Murray, Sat Jan 23 10:44:36 2021 +0000) - * 8f9a925 - Merge pull request #195 from cdambo/pass-pact-dir-to-cli (Elliott Murray, Sat Jan 23 10:39:53 2021 +0000) - * 545fc37 - fix: send to cli pact_files with the pact_dir in their path (Chanan Damboritz, Tue Jan 19 18:51:17 2021 +0200) + +- 34160a8 - fix: publish verification results was wrong (#222) (Elliott Murray, Tue Apr 20 20:58:20 2021 +0100) +- 2c0252c - Merge pull request #219 from pact-foundation/ci/revert_snyk_36 (Elliott Murray, Sat Apr 3 11:13:49 2021 +0100) +- 1a162cf - ci: revert docker36 back (Elliott Murray, Sat Apr 3 11:00:37 2021 +0100) +- 4282de4 - Merge pull request #217 from pact-foundation/snyk-fix-8f994ea63cfa41070b04b182dbd11c74 (Elliott Murray, Sat Apr 3 10:54:58 2021 +0100) +- 4eb3fbb - Merge pull request #216 from pact-foundation/snyk-fix-fc54d9c7fe536fffe78fbd34fc5fd7ea (Elliott Murray, Sat Apr 3 10:54:10 2021 +0100) +- 47373ff - fix: docker/py37.Dockerfile to reduce vulnerabilities (snyk-bot, Fri Apr 2 03:13:49 2021 +0000) +- e572221 - fix: docker/py38.Dockerfile to reduce vulnerabilities (snyk-bot, Fri Apr 2 01:10:57 2021 +0000) +- f293dbb - Merge pull request #215 from pact-foundation/snyk-fix-ab489d8931bdf95d6ef0d217aa1b2eb6 (Elliott Murray, Wed Mar 31 09:27:02 2021 +0100) +- 5946872 - fix: docker/py36.Dockerfile to reduce vulnerabilities (snyk-bot, Tue Mar 30 21:41:35 2021 +0000) +- d6c5f4a - chore: Releasing version 1.3.5 (Elliott Murray, Sun Mar 28 15:32:45 2021 +0100) +- 5864e47 - Merge pull request #213 from pact-foundation/fix/revert_some_publish (Elliott Murray, Sun Mar 28 15:27:50 2021 +0100) +- 94e597a - fix(publish): fixing the fix. Pact Python api uses only publish_version and ensures it follows that (Elliott Murray, Sun Mar 28 15:20:04 2021 +0100) +- e00f320 - chore: Releasing version 1.3.4 (Elliott Murray, Sat Mar 27 21:26:29 2021 +0000) +- c778c71 - Merge pull request #212 from pact-foundation/fix/verify_in_provider (Elliott Murray, Sat Mar 27 18:59:25 2021 +0000) +- ea0b64a - fix: verifier should now publish (Elliott Murray, Sat Mar 27 16:21:25 2021 +0000) +- 2c8779b - chore: Releasing version 1.3.3 (Elliott Murray, Thu Mar 25 21:23:29 2021 +0000) +- 5e282ff - Merge pull request #211 from anneschuth/fix/pass-pact-dir (Elliott Murray, Thu Mar 25 21:21:56 2021 +0000) +- 987c4fc - fix: pass pact_dir to publish() (Anne Schuth, Thu Mar 25 10:06:54 2021 +0100) +- 23a5129 - chore: Releasing version 1.3.2 (Elliott Murray, Sun Mar 21 14:32:50 2021 +0000) +- 57c8ae8 - Merge pull request #209 from pact-foundation/bug/fix_test_dir (Elliott Murray, Sun Mar 21 14:29:40 2021 +0000) +- af3dadf - fix: remove pacts from examples (Elliott Murray, Sun Mar 21 14:21:10 2021 +0000) +- 579f3f8 - fix: ensure path is passed to broker and allow running from root rather than test file (Elliott Murray, Sun Mar 21 13:18:01 2021 +0000) +- 7e0feab - Merge pull request #208 from pact-foundation/dependabot/pip/examples/e2e/jinja2-2.11.3 (Elliott Murray, Sat Mar 20 12:46:32 2021 +0000) +- f82c008 - chore(deps): bump jinja2 from 2.11.2 to 2.11.3 in /examples/e2e (dependabot[bot], Sat Mar 20 04:53:58 2021 +0000) +- ea6f635 - Merge pull request #206 from pact-foundation/chore/use-testcontainers (Elliott Murray, Sun Mar 14 11:05:01 2021 +0000) +- 565bc80 - chore: added some docs about how to use the e2e example (Elliott Murray, Sun Mar 14 11:02:35 2021 +0000) +- 1b4be80 - chore: spiking testcontainers (Elliott Murray, Sat Mar 13 11:15:52 2021 +0000) +- 01e4ec4 - chore: wip on using test containers on examples (Elliott Murray, Tue Mar 2 21:30:48 2021 +0000) +- 882f4a2 - Merge pull request #204 from pact-foundation/chore/use_pytest (Elliott Murray, Sat Feb 27 10:58:19 2021 +0000) +- 261f24b - chore: more clean up (Elliott Murray, Sat Feb 27 10:10:23 2021 +0000) +- 5a0934c - chore: update ci stuff (Elliott Murray, Sat Feb 27 09:57:31 2021 +0000) +- 66e79e2 - chore: move from nose to pytests as we are now 3.6+ (Elliott Murray, Sat Feb 27 09:34:37 2021 +0000) +- 6aee3e2 - chore: Releasing version 1.3.1 (Elliott Murray, Sat Feb 27 09:16:35 2021 +0000) +- 4440022 - Merge pull request #203 from pact-foundation/fix/version_confusion (Elliott Murray, Sat Feb 27 09:15:08 2021 +0000) +- 9cac2d7 - fix: introduced and renamed specification version (Elliott Murray, Tue Feb 23 21:22:36 2021 +0000) +- 64d7bdc - chore: Releasing version 1.3.0 (Elliott Murray, Tue Jan 26 18:45:58 2021 +0000) +- eaa90e1 - Merge pull request #194 from williaminfante/feat/pact-message-2 (Elliott Murray, Mon Jan 25 08:48:00 2021 +0000) +- 5ed73db - test: consider publish to broker with no pact_dir argument (William Infante, Mon Jan 25 17:19:08 2021 +1100) +- e097153 - docs: update readme (William Infante, Mon Jan 25 17:18:35 2021 +1100) +- fc0d91c - feat: address PR comments (William Infante, Mon Jan 25 10:30:33 2021 +1100) +- 5448d8c - test: remove mock and check generated json file (William Infante, Wed Jan 20 21:07:39 2021 +1100) +- abd3574 - fix: few more tests to improve coverage (Tuan Pham, Wed Jan 20 09:23:13 2021 +1100) +- 0ef971f - fix: improve test coverage (Tuan Pham, Tue Jan 19 15:11:11 2021 +1100) +- e543f04 - chore: add missing import (William Infante, Tue Jan 19 13:58:17 2021 +1100) +- bc7ff78 - chore: pydocstyle (Tuan Pham, Tue Jan 19 10:40:12 2021 +1100) +- d4235f9 - chore: flake8, clean up deadcode (Tuan Pham, Tue Jan 19 00:53:03 2021 +1100) +- 19827d0 - chore: remove test param for provider (Tuan Pham, Mon Jan 18 16:06:42 2021 +1100) +- 912b477 - chore: flake8 revert (Tuan Pham, Mon Jan 18 16:04:00 2021 +1100) +- 4a730ae - fix: revert changes to quotes (Tuan Pham, Mon Jan 18 15:57:14 2021 +1100) +- cfe35cc - feat: update message hander to be independent of pact (William Infante, Mon Jan 18 13:12:17 2021 +1100) +- 12b4a50 - fix: flake8 warning (Tuan Pham, Mon Jan 18 12:50:50 2021 +1100) +- 7afe693 - test: update message handler condition based on content (William Infante, Mon Jan 18 12:37:50 2021 +1100) +- 79106bb - feat: move publish function to broker class (Tuan Pham, Mon Jan 18 11:30:35 2021 +1100) +- a04b954 - docs: add readme for message consumer (William Infante, Mon Jan 18 09:48:01 2021 +1100) +- 8672e2f - feat: update handler to handle error exceptions (William Infante, Fri Jan 15 16:24:38 2021 +1100) +- 47b7434 - feat: change dummy handler to a message handler (William Infante, Fri Jan 15 14:06:55 2021 +1100) +- a18ce3e - test: create external dummy handler in test (William Infante, Fri Jan 15 12:31:49 2021 +1100) +- 7358049 - chore: remove log_dir, refactor test (Tuan Pham, Fri Jan 15 09:11:55 2021 +1100) +- 6718b4c - feat: update message pact tests (William Infante, Thu Jan 14 16:12:44 2021 +1100) +- bf9864f - feat: add more test (William Infante, Thu Jan 14 16:09:52 2021 +1100) +- 84681b4 - fix: try different way to mock (Tuan Pham, Thu Jan 14 15:10:36 2021 +1100) +- 86cfe8e - chore: add generate_pact_test (Tuan Pham, Thu Jan 14 14:26:42 2021 +1100) +- a1c19b6 - fix: add missing conftest (Tuan Pham, Thu Jan 14 11:02:08 2021 +1100) +- a98850a - chore: add missing files in src (Tuan Pham, Thu Jan 14 10:53:35 2021 +1100) +- 63452aa - chore: fix bad merge (Tuan Pham, Thu Jan 14 10:47:43 2021 +1100) +- 85fc77f - feat: add pact-message integration test (Tuan Pham, Thu Jan 14 10:32:02 2021 +1100) +- 11793f5 - feat: add pact-message integration (Tuan Pham, Thu Jan 14 10:30:35 2021 +1100) +- 8546a26 - fix: linting (Tuan Pham, Thu Jan 14 10:38:52 2021 +1100) +- 65b69d7 - fix: remove publish fn for now (Tuan Pham, Thu Jan 14 10:38:10 2021 +1100) +- e31dd45 - feat: add constants test (William Infante, Wed Jan 13 17:28:45 2021 +1100) +- 955dbe1 - feat: update MessageConsumer and tests (William Infante, Wed Jan 13 16:38:30 2021 +1100) +- af5c9fb - feat: create basic tests for single pact message (William Infante, Wed Jan 13 15:52:07 2021 +1100) +- fea27c8 - feat: single message flow (William Infante, Wed Jan 13 15:03:41 2021 +1100) +- 9047855 - feat: add MessageConsumer (William Infante, Wed Jan 13 12:45:19 2021 +1100) +- 40edd39 - feat: initial interface (William Infante, Tue Jan 12 16:59:46 2021 +1100) +- 2946242 - Merge pull request #198 from pact-foundation/chore/deprecate_python35 (Elliott Murray, Sat Jan 23 15:26:01 2021 +0000) +- 0111698 - fix: add e2e example test into ci back in (Elliott Murray, Sat Jan 23 15:25:07 2021 +0000) +- 0cdc7e9 - chore: remove python35 and 34 and add 39 (Elliott Murray, Sat Jan 23 15:20:47 2021 +0000) +- 3885c60 - Merge pull request #197 from pact-foundation/fix/pull_request_trigger_workflow (Elliott Murray, Sat Jan 23 10:51:06 2021 +0000) +- 925b0ac - ci: pr not triggering workflow (Elliott Murray, Sat Jan 23 10:44:36 2021 +0000) +- 8f9a925 - Merge pull request #195 from cdambo/pass-pact-dir-to-cli (Elliott Murray, Sat Jan 23 10:39:53 2021 +0000) +- 545fc37 - fix: send to cli pact_files with the pact_dir in their path (Chanan Damboritz, Tue Jan 19 18:51:17 2021 +0200) + ### 1.3.5 - * 5864e47 - Merge pull request #213 from pact-foundation/fix/revert_some_publish (Elliott Murray, Sun Mar 28 15:27:50 2021 +0100) - * 94e597a - fix(publish): fixing the fix. Pact Python api uses only publish_version and ensures it follows that (Elliott Murray, Sun Mar 28 15:20:04 2021 +0100) + +- 5864e47 - Merge pull request #213 from pact-foundation/fix/revert_some_publish (Elliott Murray, Sun Mar 28 15:27:50 2021 +0100) +- 94e597a - fix(publish): fixing the fix. Pact Python api uses only publish_version and ensures it follows that (Elliott Murray, Sun Mar 28 15:20:04 2021 +0100) + ### 1.3.4 - * c778c71 - Merge pull request #212 from pact-foundation/fix/verify_in_provider (Elliott Murray, Sat Mar 27 18:59:25 2021 +0000) - * ea0b64a - fix: verifier should now publish (Elliott Murray, Sat Mar 27 16:21:25 2021 +0000) + +- c778c71 - Merge pull request #212 from pact-foundation/fix/verify_in_provider (Elliott Murray, Sat Mar 27 18:59:25 2021 +0000) +- ea0b64a - fix: verifier should now publish (Elliott Murray, Sat Mar 27 16:21:25 2021 +0000) + ### 1.3.3 - * 5e282ff - Merge pull request #211 from anneschuth/fix/pass-pact-dir (Elliott Murray, Thu Mar 25 21:21:56 2021 +0000) - * 987c4fc - fix: pass pact_dir to publish() (Anne Schuth, Thu Mar 25 10:06:54 2021 +0100) + +- 5e282ff - Merge pull request #211 from anneschuth/fix/pass-pact-dir (Elliott Murray, Thu Mar 25 21:21:56 2021 +0000) +- 987c4fc - fix: pass pact_dir to publish() (Anne Schuth, Thu Mar 25 10:06:54 2021 +0100) + ### 1.3.2 - * 57c8ae8 - Merge pull request #209 from pact-foundation/bug/fix_test_dir (Elliott Murray, Sun Mar 21 14:29:40 2021 +0000) - * af3dadf - fix: remove pacts from examples (Elliott Murray, Sun Mar 21 14:21:10 2021 +0000) - * 579f3f8 - fix: ensure path is passed to broker and allow running from root rather than test file (Elliott Murray, Sun Mar 21 13:18:01 2021 +0000) - * 7e0feab - Merge pull request #208 from pact-foundation/dependabot/pip/examples/e2e/jinja2-2.11.3 (Elliott Murray, Sat Mar 20 12:46:32 2021 +0000) - * f82c008 - chore(deps): bump jinja2 from 2.11.2 to 2.11.3 in /examples/e2e (dependabot[bot], Sat Mar 20 04:53:58 2021 +0000) - * ea6f635 - Merge pull request #206 from pact-foundation/chore/use-testcontainers (Elliott Murray, Sun Mar 14 11:05:01 2021 +0000) - * 565bc80 - chore: added some docs about how to use the e2e example (Elliott Murray, Sun Mar 14 11:02:35 2021 +0000) - * 1b4be80 - chore: spiking testcontainers (Elliott Murray, Sat Mar 13 11:15:52 2021 +0000) - * 01e4ec4 - chore: wip on using test containers on examples (Elliott Murray, Tue Mar 2 21:30:48 2021 +0000) - * 882f4a2 - Merge pull request #204 from pact-foundation/chore/use_pytest (Elliott Murray, Sat Feb 27 10:58:19 2021 +0000) - * 261f24b - chore: more clean up (Elliott Murray, Sat Feb 27 10:10:23 2021 +0000) - * 5a0934c - chore: update ci stuff (Elliott Murray, Sat Feb 27 09:57:31 2021 +0000) - * 66e79e2 - chore: move from nose to pytests as we are now 3.6+ (Elliott Murray, Sat Feb 27 09:34:37 2021 +0000) + +- 57c8ae8 - Merge pull request #209 from pact-foundation/bug/fix_test_dir (Elliott Murray, Sun Mar 21 14:29:40 2021 +0000) +- af3dadf - fix: remove pacts from examples (Elliott Murray, Sun Mar 21 14:21:10 2021 +0000) +- 579f3f8 - fix: ensure path is passed to broker and allow running from root rather than test file (Elliott Murray, Sun Mar 21 13:18:01 2021 +0000) +- 7e0feab - Merge pull request #208 from pact-foundation/dependabot/pip/examples/e2e/jinja2-2.11.3 (Elliott Murray, Sat Mar 20 12:46:32 2021 +0000) +- f82c008 - chore(deps): bump jinja2 from 2.11.2 to 2.11.3 in /examples/e2e (dependabot[bot], Sat Mar 20 04:53:58 2021 +0000) +- ea6f635 - Merge pull request #206 from pact-foundation/chore/use-testcontainers (Elliott Murray, Sun Mar 14 11:05:01 2021 +0000) +- 565bc80 - chore: added some docs about how to use the e2e example (Elliott Murray, Sun Mar 14 11:02:35 2021 +0000) +- 1b4be80 - chore: spiking testcontainers (Elliott Murray, Sat Mar 13 11:15:52 2021 +0000) +- 01e4ec4 - chore: wip on using test containers on examples (Elliott Murray, Tue Mar 2 21:30:48 2021 +0000) +- 882f4a2 - Merge pull request #204 from pact-foundation/chore/use_pytest (Elliott Murray, Sat Feb 27 10:58:19 2021 +0000) +- 261f24b - chore: more clean up (Elliott Murray, Sat Feb 27 10:10:23 2021 +0000) +- 5a0934c - chore: update ci stuff (Elliott Murray, Sat Feb 27 09:57:31 2021 +0000) +- 66e79e2 - chore: move from nose to pytests as we are now 3.6+ (Elliott Murray, Sat Feb 27 09:34:37 2021 +0000) + ### 1.3.1 - * 4440022 - Merge pull request #203 from pact-foundation/fix/version_confusion (Elliott Murray, Sat Feb 27 09:15:08 2021 +0000) - * 9cac2d7 - fix: introduced and renamed specification version (Elliott Murray, Tue Feb 23 21:22:36 2021 +0000) + +- 4440022 - Merge pull request #203 from pact-foundation/fix/version_confusion (Elliott Murray, Sat Feb 27 09:15:08 2021 +0000) +- 9cac2d7 - fix: introduced and renamed specification version (Elliott Murray, Tue Feb 23 21:22:36 2021 +0000) + ### 1.3.0 - * eaa90e1 - Merge pull request #194 from williaminfante/feat/pact-message-2 (Elliott Murray, Mon Jan 25 08:48:00 2021 +0000) - * 5ed73db - test: consider publish to broker with no pact_dir argument (William Infante, Mon Jan 25 17:19:08 2021 +1100) - * e097153 - docs: update readme (William Infante, Mon Jan 25 17:18:35 2021 +1100) - * fc0d91c - feat: address PR comments (William Infante, Mon Jan 25 10:30:33 2021 +1100) - * 5448d8c - test: remove mock and check generated json file (William Infante, Wed Jan 20 21:07:39 2021 +1100) - * abd3574 - fix: few more tests to improve coverage (Tuan Pham, Wed Jan 20 09:23:13 2021 +1100) - * 0ef971f - fix: improve test coverage (Tuan Pham, Tue Jan 19 15:11:11 2021 +1100) - * e543f04 - chore: add missing import (William Infante, Tue Jan 19 13:58:17 2021 +1100) - * bc7ff78 - chore: pydocstyle (Tuan Pham, Tue Jan 19 10:40:12 2021 +1100) - * d4235f9 - chore: flake8, clean up deadcode (Tuan Pham, Tue Jan 19 00:53:03 2021 +1100) - * 19827d0 - chore: remove test param for provider (Tuan Pham, Mon Jan 18 16:06:42 2021 +1100) - * 912b477 - chore: flake8 revert (Tuan Pham, Mon Jan 18 16:04:00 2021 +1100) - * 4a730ae - fix: revert changes to quotes (Tuan Pham, Mon Jan 18 15:57:14 2021 +1100) - * cfe35cc - feat: update message hander to be independent of pact (William Infante, Mon Jan 18 13:12:17 2021 +1100) - * 12b4a50 - fix: flake8 warning (Tuan Pham, Mon Jan 18 12:50:50 2021 +1100) - * 7afe693 - test: update message handler condition based on content (William Infante, Mon Jan 18 12:37:50 2021 +1100) - * 79106bb - feat: move publish function to broker class (Tuan Pham, Mon Jan 18 11:30:35 2021 +1100) - * a04b954 - docs: add readme for message consumer (William Infante, Mon Jan 18 09:48:01 2021 +1100) - * 8672e2f - feat: update handler to handle error exceptions (William Infante, Fri Jan 15 16:24:38 2021 +1100) - * 47b7434 - feat: change dummy handler to a message handler (William Infante, Fri Jan 15 14:06:55 2021 +1100) - * a18ce3e - test: create external dummy handler in test (William Infante, Fri Jan 15 12:31:49 2021 +1100) - * 7358049 - chore: remove log_dir, refactor test (Tuan Pham, Fri Jan 15 09:11:55 2021 +1100) - * 6718b4c - feat: update message pact tests (William Infante, Thu Jan 14 16:12:44 2021 +1100) - * bf9864f - feat: add more test (William Infante, Thu Jan 14 16:09:52 2021 +1100) - * 84681b4 - fix: try different way to mock (Tuan Pham, Thu Jan 14 15:10:36 2021 +1100) - * 86cfe8e - chore: add generate_pact_test (Tuan Pham, Thu Jan 14 14:26:42 2021 +1100) - * a1c19b6 - fix: add missing conftest (Tuan Pham, Thu Jan 14 11:02:08 2021 +1100) - * a98850a - chore: add missing files in src (Tuan Pham, Thu Jan 14 10:53:35 2021 +1100) - * 63452aa - chore: fix bad merge (Tuan Pham, Thu Jan 14 10:47:43 2021 +1100) - * 85fc77f - feat: add pact-message integration test (Tuan Pham, Thu Jan 14 10:32:02 2021 +1100) - * 11793f5 - feat: add pact-message integration (Tuan Pham, Thu Jan 14 10:30:35 2021 +1100) - * 8546a26 - fix: linting (Tuan Pham, Thu Jan 14 10:38:52 2021 +1100) - * 65b69d7 - fix: remove publish fn for now (Tuan Pham, Thu Jan 14 10:38:10 2021 +1100) - * e31dd45 - feat: add constants test (William Infante, Wed Jan 13 17:28:45 2021 +1100) - * 955dbe1 - feat: update MessageConsumer and tests (William Infante, Wed Jan 13 16:38:30 2021 +1100) - * af5c9fb - feat: create basic tests for single pact message (William Infante, Wed Jan 13 15:52:07 2021 +1100) - * fea27c8 - feat: single message flow (William Infante, Wed Jan 13 15:03:41 2021 +1100) - * 9047855 - feat: add MessageConsumer (William Infante, Wed Jan 13 12:45:19 2021 +1100) - * 40edd39 - feat: initial interface (William Infante, Tue Jan 12 16:59:46 2021 +1100) - * 2946242 - Merge pull request #198 from pact-foundation/chore/deprecate_python35 (Elliott Murray, Sat Jan 23 15:26:01 2021 +0000) - * 0111698 - fix: add e2e example test into ci back in (Elliott Murray, Sat Jan 23 15:25:07 2021 +0000) - * 0cdc7e9 - chore: remove python35 and 34 and add 39 (Elliott Murray, Sat Jan 23 15:20:47 2021 +0000) - * 3885c60 - Merge pull request #197 from pact-foundation/fix/pull_request_trigger_workflow (Elliott Murray, Sat Jan 23 10:51:06 2021 +0000) - * 925b0ac - ci: pr not triggering workflow (Elliott Murray, Sat Jan 23 10:44:36 2021 +0000) - * 8f9a925 - Merge pull request #195 from cdambo/pass-pact-dir-to-cli (Elliott Murray, Sat Jan 23 10:39:53 2021 +0000) - * 545fc37 - fix: send to cli pact_files with the pact_dir in their path (Chanan Damboritz, Tue Jan 19 18:51:17 2021 +0200) + +- eaa90e1 - Merge pull request #194 from williaminfante/feat/pact-message-2 (Elliott Murray, Mon Jan 25 08:48:00 2021 +0000) +- 5ed73db - test: consider publish to broker with no pact_dir argument (William Infante, Mon Jan 25 17:19:08 2021 +1100) +- e097153 - docs: update readme (William Infante, Mon Jan 25 17:18:35 2021 +1100) +- fc0d91c - feat: address PR comments (William Infante, Mon Jan 25 10:30:33 2021 +1100) +- 5448d8c - test: remove mock and check generated json file (William Infante, Wed Jan 20 21:07:39 2021 +1100) +- abd3574 - fix: few more tests to improve coverage (Tuan Pham, Wed Jan 20 09:23:13 2021 +1100) +- 0ef971f - fix: improve test coverage (Tuan Pham, Tue Jan 19 15:11:11 2021 +1100) +- e543f04 - chore: add missing import (William Infante, Tue Jan 19 13:58:17 2021 +1100) +- bc7ff78 - chore: pydocstyle (Tuan Pham, Tue Jan 19 10:40:12 2021 +1100) +- d4235f9 - chore: flake8, clean up deadcode (Tuan Pham, Tue Jan 19 00:53:03 2021 +1100) +- 19827d0 - chore: remove test param for provider (Tuan Pham, Mon Jan 18 16:06:42 2021 +1100) +- 912b477 - chore: flake8 revert (Tuan Pham, Mon Jan 18 16:04:00 2021 +1100) +- 4a730ae - fix: revert changes to quotes (Tuan Pham, Mon Jan 18 15:57:14 2021 +1100) +- cfe35cc - feat: update message hander to be independent of pact (William Infante, Mon Jan 18 13:12:17 2021 +1100) +- 12b4a50 - fix: flake8 warning (Tuan Pham, Mon Jan 18 12:50:50 2021 +1100) +- 7afe693 - test: update message handler condition based on content (William Infante, Mon Jan 18 12:37:50 2021 +1100) +- 79106bb - feat: move publish function to broker class (Tuan Pham, Mon Jan 18 11:30:35 2021 +1100) +- a04b954 - docs: add readme for message consumer (William Infante, Mon Jan 18 09:48:01 2021 +1100) +- 8672e2f - feat: update handler to handle error exceptions (William Infante, Fri Jan 15 16:24:38 2021 +1100) +- 47b7434 - feat: change dummy handler to a message handler (William Infante, Fri Jan 15 14:06:55 2021 +1100) +- a18ce3e - test: create external dummy handler in test (William Infante, Fri Jan 15 12:31:49 2021 +1100) +- 7358049 - chore: remove log_dir, refactor test (Tuan Pham, Fri Jan 15 09:11:55 2021 +1100) +- 6718b4c - feat: update message pact tests (William Infante, Thu Jan 14 16:12:44 2021 +1100) +- bf9864f - feat: add more test (William Infante, Thu Jan 14 16:09:52 2021 +1100) +- 84681b4 - fix: try different way to mock (Tuan Pham, Thu Jan 14 15:10:36 2021 +1100) +- 86cfe8e - chore: add generate_pact_test (Tuan Pham, Thu Jan 14 14:26:42 2021 +1100) +- a1c19b6 - fix: add missing conftest (Tuan Pham, Thu Jan 14 11:02:08 2021 +1100) +- a98850a - chore: add missing files in src (Tuan Pham, Thu Jan 14 10:53:35 2021 +1100) +- 63452aa - chore: fix bad merge (Tuan Pham, Thu Jan 14 10:47:43 2021 +1100) +- 85fc77f - feat: add pact-message integration test (Tuan Pham, Thu Jan 14 10:32:02 2021 +1100) +- 11793f5 - feat: add pact-message integration (Tuan Pham, Thu Jan 14 10:30:35 2021 +1100) +- 8546a26 - fix: linting (Tuan Pham, Thu Jan 14 10:38:52 2021 +1100) +- 65b69d7 - fix: remove publish fn for now (Tuan Pham, Thu Jan 14 10:38:10 2021 +1100) +- e31dd45 - feat: add constants test (William Infante, Wed Jan 13 17:28:45 2021 +1100) +- 955dbe1 - feat: update MessageConsumer and tests (William Infante, Wed Jan 13 16:38:30 2021 +1100) +- af5c9fb - feat: create basic tests for single pact message (William Infante, Wed Jan 13 15:52:07 2021 +1100) +- fea27c8 - feat: single message flow (William Infante, Wed Jan 13 15:03:41 2021 +1100) +- 9047855 - feat: add MessageConsumer (William Infante, Wed Jan 13 12:45:19 2021 +1100) +- 40edd39 - feat: initial interface (William Infante, Tue Jan 12 16:59:46 2021 +1100) +- 2946242 - Merge pull request #198 from pact-foundation/chore/deprecate_python35 (Elliott Murray, Sat Jan 23 15:26:01 2021 +0000) +- 0111698 - fix: add e2e example test into ci back in (Elliott Murray, Sat Jan 23 15:25:07 2021 +0000) +- 0cdc7e9 - chore: remove python35 and 34 and add 39 (Elliott Murray, Sat Jan 23 15:20:47 2021 +0000) +- 3885c60 - Merge pull request #197 from pact-foundation/fix/pull_request_trigger_workflow (Elliott Murray, Sat Jan 23 10:51:06 2021 +0000) +- 925b0ac - ci: pr not triggering workflow (Elliott Murray, Sat Jan 23 10:44:36 2021 +0000) +- 8f9a925 - Merge pull request #195 from cdambo/pass-pact-dir-to-cli (Elliott Murray, Sat Jan 23 10:39:53 2021 +0000) +- 545fc37 - fix: send to cli pact_files with the pact_dir in their path (Chanan Damboritz, Tue Jan 19 18:51:17 2021 +0200) + ### 1.2.11 - * ba10318 - Merge pull request #192 from pact-foundation/fix/deploy_wheel_fix (Elliott Murray, Tue Dec 29 20:05:52 2020 +0000) - * 289e784 - fix: not creating wheel (Elliott Murray, Tue Dec 29 20:00:19 2020 +0000) - * d217e67 - chore: Releasing version 1.2.10 (Elliott Murray, Sat Dec 19 12:41:02 2020 +0000) - * 9438449 - Merge pull request #191 from pact-foundation/build-and-test-with-github-actions (Elliott Murray, Sat Dec 19 12:38:23 2020 +0000) - * 2796ef5 - docs: Added badge to README (Elliott Murray, Sat Dec 19 09:37:22 2020 +0000) - * f1b6968 - ci: add publishing actions (Matthew Balvanz, Thu Dec 17 09:09:10 2020 -0600) - * 287a32e - ci: removed Travis CI configuration (Matthew Balvanz, Wed Dec 16 21:18:09 2020 -0600) - * 77dd4d5 - ci(github actions): added Github Actions configuration for build and test (Matthew Balvanz, Wed Dec 16 21:07:06 2020 -0600) - * d6f02ca - Merge pull request #189 from noelslice/master (Elliott Murray, Fri Dec 4 08:39:33 2020 +0000) - * c24a73f - docs: typo in pact-verifier help string: PUT -> POST for --provider-states-setup-url (Noel Dawe, Thu Dec 3 22:39:31 2020 -0500) - * 4cdabd1 - Merge pull request #188 from pact-foundation/docs/example/fastapi (Elliott Murray, Sun Nov 29 14:53:24 2020 +0000) - * 6728cb9 - docs(example): created example and have relative imports kinda working. Provider not working as it cant find one of our urls (Elliott Murray, Sun Nov 29 13:34:42 2020 +0000) - * 9358097 - Merge pull request #184 from pact-foundation/chore/update_python_version (Elliott Murray, Sat Nov 28 10:06:53 2020 +0000) - * 48a2a21 - Merge pull request #186 from jstoebel/patch-2 (Elliott Murray, Sat Nov 21 10:39:48 2020 +0000) - * 74f9a4f - docs: fix small typo in `with_request` doc string (Jacob Stoebel, Wed Nov 18 14:51:03 2020 -0500) - * 4e4ed26 - chore: added run test to travis (Elliott Murray, Sun Nov 1 11:49:38 2020 +0000) - * 37e2f3a - chore: wqshell script to run flask in exmaples (Elliott Murray, Sun Nov 1 11:41:59 2020 +0000) - * b5d9d7b - chore(upgrade): upgrade python version to 3.8 (Elliott Murray, Sun Nov 1 11:12:10 2020 +0000) + +- ba10318 - Merge pull request #192 from pact-foundation/fix/deploy_wheel_fix (Elliott Murray, Tue Dec 29 20:05:52 2020 +0000) +- 289e784 - fix: not creating wheel (Elliott Murray, Tue Dec 29 20:00:19 2020 +0000) +- d217e67 - chore: Releasing version 1.2.10 (Elliott Murray, Sat Dec 19 12:41:02 2020 +0000) +- 9438449 - Merge pull request #191 from pact-foundation/build-and-test-with-github-actions (Elliott Murray, Sat Dec 19 12:38:23 2020 +0000) +- 2796ef5 - docs: Added badge to README (Elliott Murray, Sat Dec 19 09:37:22 2020 +0000) +- f1b6968 - ci: add publishing actions (Matthew Balvanz, Thu Dec 17 09:09:10 2020 -0600) +- 287a32e - ci: removed Travis CI configuration (Matthew Balvanz, Wed Dec 16 21:18:09 2020 -0600) +- 77dd4d5 - ci(github actions): added Github Actions configuration for build and test (Matthew Balvanz, Wed Dec 16 21:07:06 2020 -0600) +- d6f02ca - Merge pull request #189 from noelslice/master (Elliott Murray, Fri Dec 4 08:39:33 2020 +0000) +- c24a73f - docs: typo in pact-verifier help string: PUT -> POST for --provider-states-setup-url (Noel Dawe, Thu Dec 3 22:39:31 2020 -0500) +- 4cdabd1 - Merge pull request #188 from pact-foundation/docs/example/fastapi (Elliott Murray, Sun Nov 29 14:53:24 2020 +0000) +- 6728cb9 - docs(example): created example and have relative imports kinda working. Provider not working as it cant find one of our urls (Elliott Murray, Sun Nov 29 13:34:42 2020 +0000) +- 9358097 - Merge pull request #184 from pact-foundation/chore/update_python_version (Elliott Murray, Sat Nov 28 10:06:53 2020 +0000) +- 48a2a21 - Merge pull request #186 from jstoebel/patch-2 (Elliott Murray, Sat Nov 21 10:39:48 2020 +0000) +- 74f9a4f - docs: fix small typo in `with_request` doc string (Jacob Stoebel, Wed Nov 18 14:51:03 2020 -0500) +- 4e4ed26 - chore: added run test to travis (Elliott Murray, Sun Nov 1 11:49:38 2020 +0000) +- 37e2f3a - chore: wqshell script to run flask in exmaples (Elliott Murray, Sun Nov 1 11:41:59 2020 +0000) +- b5d9d7b - chore(upgrade): upgrade python version to 3.8 (Elliott Murray, Sun Nov 1 11:12:10 2020 +0000) + ### 1.2.10 - * 9438449 - Merge pull request #191 from pact-foundation/build-and-test-with-github-actions (Elliott Murray, Sat Dec 19 12:38:23 2020 +0000) - * 2796ef5 - docs: Added badge to README (Elliott Murray, Sat Dec 19 09:37:22 2020 +0000) - * f1b6968 - ci: add publishing actions (Matthew Balvanz, Thu Dec 17 09:09:10 2020 -0600) - * 287a32e - ci: removed Travis CI configuration (Matthew Balvanz, Wed Dec 16 21:18:09 2020 -0600) - * 77dd4d5 - ci(github actions): added Github Actions configuration for build and test (Matthew Balvanz, Wed Dec 16 21:07:06 2020 -0600) - * d6f02ca - Merge pull request #189 from noelslice/master (Elliott Murray, Fri Dec 4 08:39:33 2020 +0000) - * c24a73f - docs: typo in pact-verifier help string: PUT -> POST for --provider-states-setup-url (Noel Dawe, Thu Dec 3 22:39:31 2020 -0500) - * 4cdabd1 - Merge pull request #188 from pact-foundation/docs/example/fastapi (Elliott Murray, Sun Nov 29 14:53:24 2020 +0000) - * 6728cb9 - docs(example): created example and have relative imports kinda working. Provider not working as it cant find one of our urls (Elliott Murray, Sun Nov 29 13:34:42 2020 +0000) - * 9358097 - Merge pull request #184 from pact-foundation/chore/update_python_version (Elliott Murray, Sat Nov 28 10:06:53 2020 +0000) - * 48a2a21 - Merge pull request #186 from jstoebel/patch-2 (Elliott Murray, Sat Nov 21 10:39:48 2020 +0000) - * 74f9a4f - docs: fix small typo in `with_request` doc string (Jacob Stoebel, Wed Nov 18 14:51:03 2020 -0500) - * 4e4ed26 - chore: added run test to travis (Elliott Murray, Sun Nov 1 11:49:38 2020 +0000) - * 37e2f3a - chore: wqshell script to run flask in exmaples (Elliott Murray, Sun Nov 1 11:41:59 2020 +0000) - * b5d9d7b - chore(upgrade): upgrade python version to 3.8 (Elliott Murray, Sun Nov 1 11:12:10 2020 +0000) + +- 9438449 - Merge pull request #191 from pact-foundation/build-and-test-with-github-actions (Elliott Murray, Sat Dec 19 12:38:23 2020 +0000) +- 2796ef5 - docs: Added badge to README (Elliott Murray, Sat Dec 19 09:37:22 2020 +0000) +- f1b6968 - ci: add publishing actions (Matthew Balvanz, Thu Dec 17 09:09:10 2020 -0600) +- 287a32e - ci: removed Travis CI configuration (Matthew Balvanz, Wed Dec 16 21:18:09 2020 -0600) +- 77dd4d5 - ci(github actions): added Github Actions configuration for build and test (Matthew Balvanz, Wed Dec 16 21:07:06 2020 -0600) +- d6f02ca - Merge pull request #189 from noelslice/master (Elliott Murray, Fri Dec 4 08:39:33 2020 +0000) +- c24a73f - docs: typo in pact-verifier help string: PUT -> POST for --provider-states-setup-url (Noel Dawe, Thu Dec 3 22:39:31 2020 -0500) +- 4cdabd1 - Merge pull request #188 from pact-foundation/docs/example/fastapi (Elliott Murray, Sun Nov 29 14:53:24 2020 +0000) +- 6728cb9 - docs(example): created example and have relative imports kinda working. Provider not working as it cant find one of our urls (Elliott Murray, Sun Nov 29 13:34:42 2020 +0000) +- 9358097 - Merge pull request #184 from pact-foundation/chore/update_python_version (Elliott Murray, Sat Nov 28 10:06:53 2020 +0000) +- 48a2a21 - Merge pull request #186 from jstoebel/patch-2 (Elliott Murray, Sat Nov 21 10:39:48 2020 +0000) +- 74f9a4f - docs: fix small typo in `with_request` doc string (Jacob Stoebel, Wed Nov 18 14:51:03 2020 -0500) +- 4e4ed26 - chore: added run test to travis (Elliott Murray, Sun Nov 1 11:49:38 2020 +0000) +- 37e2f3a - chore: wqshell script to run flask in exmaples (Elliott Murray, Sun Nov 1 11:41:59 2020 +0000) +- b5d9d7b - chore(upgrade): upgrade python version to 3.8 (Elliott Murray, Sun Nov 1 11:12:10 2020 +0000) + ### 1.2.9 - * 4430681 - Merge pull request #183 from thatguysimon/feat/verifier-class-consumer-version-selectors (Elliott Murray, Mon Oct 19 15:35:47 2020 +0100) - * 683a931 - fix: Fix flaky tests using OrderedDict (Simon Nizov, Mon Oct 19 17:21:21 2020 +0300) - * 33be267 - style: Fix one more linting issue (Simon Nizov, Mon Oct 19 11:22:05 2020 +0300) - * e7c87ce - style: Fix linting issues (Simon Nizov, Mon Oct 19 11:16:59 2020 +0300) - * ee2eda0 - feat(verifier): Allow setting consumer_version_selectors on Verifier (Simon Nizov, Mon Oct 19 11:01:18 2020 +0300) + +- 4430681 - Merge pull request #183 from thatguysimon/feat/verifier-class-consumer-version-selectors (Elliott Murray, Mon Oct 19 15:35:47 2020 +0100) +- 683a931 - fix: Fix flaky tests using OrderedDict (Simon Nizov, Mon Oct 19 17:21:21 2020 +0300) +- 33be267 - style: Fix one more linting issue (Simon Nizov, Mon Oct 19 11:22:05 2020 +0300) +- e7c87ce - style: Fix linting issues (Simon Nizov, Mon Oct 19 11:16:59 2020 +0300) +- ee2eda0 - feat(verifier): Allow setting consumer_version_selectors on Verifier (Simon Nizov, Mon Oct 19 11:01:18 2020 +0300) + ### 1.2.8 - * 4c68fd4 - Merge pull request #182 from thatguysimon/feat/enable-wip-pacts (Elliott Murray, Sat Oct 17 16:00:50 2020 +0100) - * 9ea14d3 - refactor: Extract input validation in call_verify out into a dedicated method (Simon Nizov, Sat Oct 17 17:27:49 2020 +0300) - * 5a5969d - fix: Fix command building bug (Simon Nizov, Sat Oct 17 15:40:55 2020 +0300) - * b8c0006 - style: Fix linting (Simon Nizov, Sat Oct 17 15:18:29 2020 +0300) - * fc3d7ae - feat(verifier): Support include-wip-pacts-since in CLI (Simon Nizov, Sat Oct 17 15:03:38 2020 +0300) - * a0eca4c - Merge pull request #180 from elliottmurray/docs/example_flaskr (Elliott Murray, Fri Oct 16 11:13:11 2020 +0100) - * a8a07d4 - docs(examples): changed provider example to use atexit (Elliott Murray, Fri Oct 16 10:54:25 2020 +0100) - * 186f4f4 - Merge pull request #179 from pact-foundation/docs/example_readme (Elliott Murray, Thu Oct 15 10:13:13 2020 +0100) - * 2f66618 - docs(examples): tweak to readme (Elliott Murray, Thu Oct 15 10:08:52 2020 +0100) + +- 4c68fd4 - Merge pull request #182 from thatguysimon/feat/enable-wip-pacts (Elliott Murray, Sat Oct 17 16:00:50 2020 +0100) +- 9ea14d3 - refactor: Extract input validation in call_verify out into a dedicated method (Simon Nizov, Sat Oct 17 17:27:49 2020 +0300) +- 5a5969d - fix: Fix command building bug (Simon Nizov, Sat Oct 17 15:40:55 2020 +0300) +- b8c0006 - style: Fix linting (Simon Nizov, Sat Oct 17 15:18:29 2020 +0300) +- fc3d7ae - feat(verifier): Support include-wip-pacts-since in CLI (Simon Nizov, Sat Oct 17 15:03:38 2020 +0300) +- a0eca4c - Merge pull request #180 from elliottmurray/docs/example_flaskr (Elliott Murray, Fri Oct 16 11:13:11 2020 +0100) +- a8a07d4 - docs(examples): changed provider example to use atexit (Elliott Murray, Fri Oct 16 10:54:25 2020 +0100) +- 186f4f4 - Merge pull request #179 from pact-foundation/docs/example_readme (Elliott Murray, Thu Oct 15 10:13:13 2020 +0100) +- 2f66618 - docs(examples): tweak to readme (Elliott Murray, Thu Oct 15 10:08:52 2020 +0100) + ### 1.2.7 - * 90b71d2 - Merge pull request #178 from pact-foundation/fix/custom_header_typo (Elliott Murray, Fri Oct 9 12:47:37 2020 +0100) - * b07ef69 - fix(verifier): headers not propogated properly (Elliott Murray, Fri Oct 9 12:24:25 2020 +0100) - * 0e9b71c - Merge pull request #177 from pact-foundation/docs/remove_handcrafted_broker (Elliott Murray, Fri Oct 9 12:01:24 2020 +0100) - * 2db7008 - docs(examples): removed manaul publish to broker (Elliott Murray, Fri Oct 9 11:54:30 2020 +0100) + +- 90b71d2 - Merge pull request #178 from pact-foundation/fix/custom_header_typo (Elliott Murray, Fri Oct 9 12:47:37 2020 +0100) +- b07ef69 - fix(verifier): headers not propogated properly (Elliott Murray, Fri Oct 9 12:24:25 2020 +0100) +- 0e9b71c - Merge pull request #177 from pact-foundation/docs/remove_handcrafted_broker (Elliott Murray, Fri Oct 9 12:01:24 2020 +0100) +- 2db7008 - docs(examples): removed manaul publish to broker (Elliott Murray, Fri Oct 9 11:54:30 2020 +0100) + ### 1.2.6 - * 1192bd6 - Merge pull request #173 from copalco/master (Elliott Murray, Thu Sep 10 15:30:07 2020 +0100) - * 5db7100 - feat(verifier): allow to use unauthenticated brokers (Piotr Kopalko, Thu Sep 10 14:12:12 2020 +0200) + +- 1192bd6 - Merge pull request #173 from copalco/master (Elliott Murray, Thu Sep 10 15:30:07 2020 +0100) +- 5db7100 - feat(verifier): allow to use unauthenticated brokers (Piotr Kopalko, Thu Sep 10 14:12:12 2020 +0200) + ### 1.2.5 - * 46372c7 - Merge pull request #171 from m-aciek/enable-pending (Elliott Murray, Wed Sep 9 10:03:02 2020 +0100) - * e840587 - fix(verifier): remove superfluous verbose mentions (Maciej Olko, Sat Sep 5 21:33:52 2020 +0200) - * c64bec1 - refactor(verifier): add enable_pending to signature of verify methods (Maciej Olko, Sat Sep 5 21:32:33 2020 +0200) - * e6c9ed0 - feat(verifier): support --enable-pending flag in CLI (Maciej Olko, Thu Sep 3 15:33:40 2020 +0200) - * 2b57446 - feat(verifier): pass enable_pending flag in Verifier's methods (Maciej Olko, Thu Sep 3 17:03:08 2020 +0200) - * d51c88d - test: bump mock to 3.0.5 (m-aciek, Thu Sep 3 23:42:00 2020 +0200) - * 39de1f3 - feat(verifier): add enable_pending argument handling in verify wrapper (Maciej Olko, Thu Sep 3 15:33:07 2020 +0200) - * fc6c365 - fix(verifier): remove superfluous option from verify CLI command (Maciej Olko, Thu Sep 3 13:30:57 2020 +0200) - * fbbd5fa - ci(pre-commit): add commitizen to pre-commit configuration (Maciej Olko, Thu Sep 3 17:19:45 2020 +0200) + +- 46372c7 - Merge pull request #171 from m-aciek/enable-pending (Elliott Murray, Wed Sep 9 10:03:02 2020 +0100) +- e840587 - fix(verifier): remove superfluous verbose mentions (Maciej Olko, Sat Sep 5 21:33:52 2020 +0200) +- c64bec1 - refactor(verifier): add enable_pending to signature of verify methods (Maciej Olko, Sat Sep 5 21:32:33 2020 +0200) +- e6c9ed0 - feat(verifier): support --enable-pending flag in CLI (Maciej Olko, Thu Sep 3 15:33:40 2020 +0200) +- 2b57446 - feat(verifier): pass enable_pending flag in Verifier's methods (Maciej Olko, Thu Sep 3 17:03:08 2020 +0200) +- d51c88d - test: bump mock to 3.0.5 (m-aciek, Thu Sep 3 23:42:00 2020 +0200) +- 39de1f3 - feat(verifier): add enable_pending argument handling in verify wrapper (Maciej Olko, Thu Sep 3 15:33:07 2020 +0200) +- fc6c365 - fix(verifier): remove superfluous option from verify CLI command (Maciej Olko, Thu Sep 3 13:30:57 2020 +0200) +- fbbd5fa - ci(pre-commit): add commitizen to pre-commit configuration (Maciej Olko, Thu Sep 3 17:19:45 2020 +0200) + ### 1.2.4 - * a594e22 - Merge pull request #170 from alecgerona/feat/consumer-version-selector (Elliott Murray, Thu Aug 27 15:21:45 2020 +0100) - * 05c5e41 - docs(cli): improve cli help grammar (Alexandre Gerona, Thu Aug 27 06:28:56 2020 +0800) - * 49d5f7c - docs: update README.md with relevant option documentation (Alexandre Gerona, Thu Aug 27 06:22:37 2020 +0800) - * 5a99528 - feat(cli): add consumer-version-selector option (Alexandre Gerona, Thu Aug 27 06:22:07 2020 +0800) + +- a594e22 - Merge pull request #170 from alecgerona/feat/consumer-version-selector (Elliott Murray, Thu Aug 27 15:21:45 2020 +0100) +- 05c5e41 - docs(cli): improve cli help grammar (Alexandre Gerona, Thu Aug 27 06:28:56 2020 +0800) +- 49d5f7c - docs: update README.md with relevant option documentation (Alexandre Gerona, Thu Aug 27 06:22:37 2020 +0800) +- 5a99528 - feat(cli): add consumer-version-selector option (Alexandre Gerona, Thu Aug 27 06:22:07 2020 +0800) + ### 1.2.3 - * 8188d88 - chore: fix release script (Elliott Murray, Wed Aug 26 12:46:10 2020 +0100) - * e0e5106 - Merge pull request #169 from pact-foundation/chore/update_pr_scripts (Elliott Murray, Wed Aug 26 10:24:47 2020 +0100) - * 81fd653 - chore: release script updates version automaitcally now (Elliott Murray, Wed Aug 26 10:16:14 2020 +0100) - * 773d3f9 - chore: script now uses gh over hub (Elliott Murray, Wed Aug 26 10:03:06 2020 +0100) - * 468e4ad - Merge pull request #168 from pact-foundation/chore/upgrade-to-pact-ruby-standalone-1-88-3 (Elliott Murray, Wed Aug 26 09:49:33 2020 +0100) - * ce944fe - feat: update standalone to 1.88.3 (Elliott Murray, Wed Aug 26 09:08:27 2020 +0100) + +- 8188d88 - chore: fix release script (Elliott Murray, Wed Aug 26 12:46:10 2020 +0100) +- e0e5106 - Merge pull request #169 from pact-foundation/chore/update_pr_scripts (Elliott Murray, Wed Aug 26 10:24:47 2020 +0100) +- 81fd653 - chore: release script updates version automaitcally now (Elliott Murray, Wed Aug 26 10:16:14 2020 +0100) +- 773d3f9 - chore: script now uses gh over hub (Elliott Murray, Wed Aug 26 10:03:06 2020 +0100) +- 468e4ad - Merge pull request #168 from pact-foundation/chore/upgrade-to-pact-ruby-standalone-1-88-3 (Elliott Murray, Wed Aug 26 09:49:33 2020 +0100) +- ce944fe - feat: update standalone to 1.88.3 (Elliott Murray, Wed Aug 26 09:08:27 2020 +0100) + ### 1.2.2 - * 2c52053 - Merge pull request #167 from pact-foundation/feat/add_env_vars_verify (Elliott Murray, Mon Aug 24 16:08:04 2020 +0100) - * ce62588 - feat: added env vars for broker verify (Elliott Murray, Mon Aug 24 16:03:44 2020 +0100) - * 880fff2 - Merge pull request #165 from pact-foundation/docs/https_fix (Elliott Murray, Thu Aug 20 12:43:12 2020 +0100) - * 1a3605e - docs: https svg (Elliott Murray, Thu Aug 20 12:37:01 2020 +0100) + +- 2c52053 - Merge pull request #167 from pact-foundation/feat/add_env_vars_verify (Elliott Murray, Mon Aug 24 16:08:04 2020 +0100) +- ce62588 - feat: added env vars for broker verify (Elliott Murray, Mon Aug 24 16:03:44 2020 +0100) +- 880fff2 - Merge pull request #165 from pact-foundation/docs/https_fix (Elliott Murray, Thu Aug 20 12:43:12 2020 +0100) +- 1a3605e - docs: https svg (Elliott Murray, Thu Aug 20 12:37:01 2020 +0100) + ### 1.2.1 - * 69a4a9a - Merge pull request #163 from elliottmurray/fix/custom_header (Elliott Murray, Sat Aug 8 10:17:20 2020 +0100) - * 88b7d9f - fix: custom headers had a typo (Elliott Murray, Sat Aug 1 11:08:54 2020 +0100) - * f501f19 - Merge pull request #161 from pact-foundation/docs/verifier_docs_examples (Elliott Murray, Fri Jul 24 12:30:35 2020 +0100) - * 9875c71 - docs: merged 2 examples (Elliott Murray, Fri Jul 24 12:00:37 2020 +0100) - * 6f0d3ac - docs: Example code verifier (Elliott Murray, Fri Jul 24 11:31:17 2020 +0100) + +- 69a4a9a - Merge pull request #163 from elliottmurray/fix/custom_header (Elliott Murray, Sat Aug 8 10:17:20 2020 +0100) +- 88b7d9f - fix: custom headers had a typo (Elliott Murray, Sat Aug 1 11:08:54 2020 +0100) +- f501f19 - Merge pull request #161 from pact-foundation/docs/verifier_docs_examples (Elliott Murray, Fri Jul 24 12:30:35 2020 +0100) +- 9875c71 - docs: merged 2 examples (Elliott Murray, Fri Jul 24 12:00:37 2020 +0100) +- 6f0d3ac - docs: Example code verifier (Elliott Murray, Fri Jul 24 11:31:17 2020 +0100) + ### 1.2.0 - * 2b844c5 - Merge pull request #159 from pact-foundation/feat/fix_provider_classs (Elliott Murray, Fri Jul 24 09:47:46 2020 +0100) - * 9c565bb - feat: fixing up tests and examples and code for provider class (Elliott Murray, Mon Jul 20 15:57:49 2020 +0100) - * d4072ed - Merge pull request #156 from pact-foundation/feat/provider_verifier (Elliott Murray, Thu Jul 16 13:31:18 2020 +0100) - * 926a611 - feat: create beta verifier class and api (Elliott Murray, Wed Jun 10 21:31:47 2020 +0100) - * 4635a07 - chore: added semantic yml for git messages (Elliott Murray, Sun Jun 28 12:43:24 2020 +0100) - * ff9894a - Merge pull request #154 from elliottmurray/style/git_message (Elliott Murray, Sat Jun 27 13:31:16 2020 +0100) - * be6697f - fix: change to head from master (Elliott Murray, Sat Jun 27 13:08:08 2020 +0100) + +- 2b844c5 - Merge pull request #159 from pact-foundation/feat/fix_provider_classs (Elliott Murray, Fri Jul 24 09:47:46 2020 +0100) +- 9c565bb - feat: fixing up tests and examples and code for provider class (Elliott Murray, Mon Jul 20 15:57:49 2020 +0100) +- d4072ed - Merge pull request #156 from pact-foundation/feat/provider_verifier (Elliott Murray, Thu Jul 16 13:31:18 2020 +0100) +- 926a611 - feat: create beta verifier class and api (Elliott Murray, Wed Jun 10 21:31:47 2020 +0100) +- 4635a07 - chore: added semantic yml for git messages (Elliott Murray, Sun Jun 28 12:43:24 2020 +0100) +- ff9894a - Merge pull request #154 from elliottmurray/style/git_message (Elliott Murray, Sat Jun 27 13:31:16 2020 +0100) +- be6697f - fix: change to head from master (Elliott Murray, Sat Jun 27 13:08:08 2020 +0100) + ### 1.1.0 - * 1079417 - test (Elliott Murray, Thu Jun 25 10:02:14 2020 +0100) - * 7fe1ef4 - Releasing version 1.1.0 (Elliott Murray, Thu Jun 25 09:41:42 2020 +0100) - * fafc3d5 - Merge pull request #147 from pact-foundation/feat/add_logging_params (Elliott Murray, Thu Jun 25 09:24:34 2020 +0100) - * 8ce7d44 - Added logging params (Elliott Murray, Wed Jun 24 11:58:25 2020 +0100) - * b6450b8 - Merge pull request #146 from pact-foundation/chore/upgrade-to-pact-ruby-standalone-1-86-0 (Elliott Murray, Wed Jun 24 10:59:29 2020 +0100) - * bf43d8a - feat: update standalone to 1.86.0 (Beth Skurrie, Wed Jun 24 09:31:18 2020 +1000) - * 529dfb7 - Merge pull request #145 from jstoebel/patch-1 (Elliott Murray, Thu Jun 11 12:00:51 2020 +0100) - * 9359d34 - Remove typo from examples/e2e requirements.txt (Jacob Stoebel, Thu Jun 11 06:47:02 2020 -0400) - * aee95ed - Merge pull request #144 from pact-foundation/chore_cleanup (Elliott Murray, Wed Jun 10 21:38:12 2020 +0100) - * 9c71ea0 - chore: removed some files and moved a few things around (Elliott Murray, Wed Jun 10 21:33:37 2020 +0100) - - ### v1.0.1 - * 8c78ff7 - Releasing version 1.0.1 (Elliott Murray, Wed Jun 3 11:01:39 2020 +0100) - * 63f0e3e - Merge pull request #142 from elliottmurray/ssl_verify (Elliott Murray, Wed Jun 3 09:50:10 2020 +0100) - * cd43bd0 - Removed coverage (Elliott Murray, Tue Jun 2 21:41:52 2020 +0100) - * 30e6f86 - Fixed flake (Elliott Murray, Tue Jun 2 21:32:01 2020 +0100) - * 1a11320 - Fix unit tests (Elliott Murray, Tue Jun 2 21:29:56 2020 +0100) - * 353d054 - travis code coverage (Elliott Murray, Tue Jun 2 21:14:37 2020 +0100) - * c08babd - Fixing unit tests command in tox and travis (Elliott Murray, Tue Jun 2 18:30:10 2020 +0100) - * 157676c - Allowed https communication to mock. Didnt fix tests (Elliott Murray, Tue Jun 2 17:47:08 2020 +0100) - * 60c9f5a - Fix deploy to pypi2 (Elliott Murray, Fri May 22 13:50:41 2020 +0100) - * e2c7e4e - Fix deploy to pypi (Elliott Murray, Fri May 22 13:41:27 2020 +0100) - - ### v1.0.0 - * 2c6e4eb - Releasing version 1.0.0 (Elliott Murray, Fri May 22 13:30:49 2020 +0100) - * c68ccb7 - Merge pull request #140 from elliottmurray/python2_deprecate (Elliott Murray, Fri May 22 13:29:38 2020 +0100) - * 8bc6d48 - Release script to make life a bit easier (Elliott Murray, Thu May 21 12:32:27 2020 +0100) - * a845f71 - Removed 2.x support (Elliott Murray, Thu May 21 12:19:16 2020 +0100) - * 562e047 - Merge pull request #138 from pyasi/pyasi_add_matcher_regexes (Elliott Murray, Fri May 15 14:10:28 2020 +0100) - * db39d87 - remove virtualenv (Peter Yasi, Fri May 15 09:02:01 2020 -0400) - * cccd30a - Add Format to the standard pact package (Peter Yasi, Fri May 15 08:55:57 2020 -0400) - * b78ac6d - Merge branch 'master' into pyasi_add_matcher_regexes (Peter Yasi, Fri May 15 08:13:30 2020 -0400) - * 35dfa0d - add enum34 a a dep for py27-install (Peter Yasi, Thu May 14 20:52:09 2020 -0400) - * 1fcc6c1 - Pydocstyle fixes, will still need fix for no enum in 2.7 (Peter Yasi, Thu May 14 19:49:23 2020 -0400) - * fe068e5 - Add examples to e2e tests (Peter Yasi, Thu May 14 19:07:31 2020 -0400) - * 5aaa82f - README documentation (Peter Yasi, Thu May 14 18:46:42 2020 -0400) - * 0d588f7 - pydocs and formatting (Peter Yasi, Thu May 14 18:19:32 2020 -0400) - * a21118c - Use raw strings to avoid deprecated escape sequence (Peter Yasi, Thu May 14 08:54:01 2020 -0400) - * 715d10f - Initial implementation with example unit tests (Peter Yasi, Thu May 14 00:00:30 2020 -0400) +- 1079417 - test (Elliott Murray, Thu Jun 25 10:02:14 2020 +0100) +- 7fe1ef4 - Releasing version 1.1.0 (Elliott Murray, Thu Jun 25 09:41:42 2020 +0100) +- fafc3d5 - Merge pull request #147 from pact-foundation/feat/add_logging_params (Elliott Murray, Thu Jun 25 09:24:34 2020 +0100) +- 8ce7d44 - Added logging params (Elliott Murray, Wed Jun 24 11:58:25 2020 +0100) +- b6450b8 - Merge pull request #146 from pact-foundation/chore/upgrade-to-pact-ruby-standalone-1-86-0 (Elliott Murray, Wed Jun 24 10:59:29 2020 +0100) +- bf43d8a - feat: update standalone to 1.86.0 (Beth Skurrie, Wed Jun 24 09:31:18 2020 +1000) +- 529dfb7 - Merge pull request #145 from jstoebel/patch-1 (Elliott Murray, Thu Jun 11 12:00:51 2020 +0100) +- 9359d34 - Remove typo from examples/e2e requirements.txt (Jacob Stoebel, Thu Jun 11 06:47:02 2020 -0400) +- aee95ed - Merge pull request #144 from pact-foundation/chore_cleanup (Elliott Murray, Wed Jun 10 21:38:12 2020 +0100) +- 9c71ea0 - chore: removed some files and moved a few things around (Elliott Murray, Wed Jun 10 21:33:37 2020 +0100) + +### v1.0.1 + +- 8c78ff7 - Releasing version 1.0.1 (Elliott Murray, Wed Jun 3 11:01:39 2020 +0100) +- 63f0e3e - Merge pull request #142 from elliottmurray/ssl_verify (Elliott Murray, Wed Jun 3 09:50:10 2020 +0100) +- cd43bd0 - Removed coverage (Elliott Murray, Tue Jun 2 21:41:52 2020 +0100) +- 30e6f86 - Fixed flake (Elliott Murray, Tue Jun 2 21:32:01 2020 +0100) +- 1a11320 - Fix unit tests (Elliott Murray, Tue Jun 2 21:29:56 2020 +0100) +- 353d054 - travis code coverage (Elliott Murray, Tue Jun 2 21:14:37 2020 +0100) +- c08babd - Fixing unit tests command in tox and travis (Elliott Murray, Tue Jun 2 18:30:10 2020 +0100) +- 157676c - Allowed https communication to mock. Didnt fix tests (Elliott Murray, Tue Jun 2 17:47:08 2020 +0100) +- 60c9f5a - Fix deploy to pypi2 (Elliott Murray, Fri May 22 13:50:41 2020 +0100) +- e2c7e4e - Fix deploy to pypi (Elliott Murray, Fri May 22 13:41:27 2020 +0100) + +### v1.0.0 + +- 2c6e4eb - Releasing version 1.0.0 (Elliott Murray, Fri May 22 13:30:49 2020 +0100) +- c68ccb7 - Merge pull request #140 from elliottmurray/python2_deprecate (Elliott Murray, Fri May 22 13:29:38 2020 +0100) +- 8bc6d48 - Release script to make life a bit easier (Elliott Murray, Thu May 21 12:32:27 2020 +0100) +- a845f71 - Removed 2.x support (Elliott Murray, Thu May 21 12:19:16 2020 +0100) +- 562e047 - Merge pull request #138 from pyasi/pyasi_add_matcher_regexes (Elliott Murray, Fri May 15 14:10:28 2020 +0100) +- db39d87 - remove virtualenv (Peter Yasi, Fri May 15 09:02:01 2020 -0400) +- cccd30a - Add Format to the standard pact package (Peter Yasi, Fri May 15 08:55:57 2020 -0400) +- b78ac6d - Merge branch 'master' into pyasi_add_matcher_regexes (Peter Yasi, Fri May 15 08:13:30 2020 -0400) +- 35dfa0d - add enum34 a a dep for py27-install (Peter Yasi, Thu May 14 20:52:09 2020 -0400) +- 1fcc6c1 - Pydocstyle fixes, will still need fix for no enum in 2.7 (Peter Yasi, Thu May 14 19:49:23 2020 -0400) +- fe068e5 - Add examples to e2e tests (Peter Yasi, Thu May 14 19:07:31 2020 -0400) +- 5aaa82f - README documentation (Peter Yasi, Thu May 14 18:46:42 2020 -0400) +- 0d588f7 - pydocs and formatting (Peter Yasi, Thu May 14 18:19:32 2020 -0400) +- a21118c - Use raw strings to avoid deprecated escape sequence (Peter Yasi, Thu May 14 08:54:01 2020 -0400) +- 715d10f - Initial implementation with example unit tests (Peter Yasi, Thu May 14 00:00:30 2020 -0400) ### 0.22.0 - * d112a4a - Merge pull request #134 from elliottmurray/multiple-custom-provider-header (Elliott Murray, Mon May 11 16:32:49 2020 +0100) - * 58f8e6b - Fix some style issues (Elliott Murray, Wed Apr 29 12:35:00 2020 +0100) - * bf9bc2d - Added multiple click options for custom headers (Elliott Murray, Tue Apr 28 18:14:02 2020 +0100) - * 254ffc5 - Merge pull request #130 from elliottmurray/examples (Elliott Murray, Sat May 9 18:02:46 2020 +0100) - * 3898aee - Created examples folder (Elliott Murray, Thu Apr 2 14:03:17 2020 +0100) - * b859443 - Merge pull request #129 from elliottmurray/docker (Elliott Murray, Sat May 9 17:54:01 2020 +0100) - * 9b83da7 - Added bash to containers (Elliott Murray, Fri Apr 10 11:52:43 2020 +0100) - * 73db8fc - Remove subprocess requirement (Elliott Murray, Fri Apr 10 11:32:38 2020 +0100) - * f3315a1 - Added 38 and created build helper script (Elliott Murray, Wed Apr 1 17:11:06 2020 +0100) - * e7743de - Some readme and python37 (Elliott Murray, Wed Apr 1 14:25:20 2020 +0100) - * 515aeb2 - Tweaked the run script (Elliott Murray, Wed Apr 1 12:57:02 2020 +0100) - * 5a6acaf - Merge pull request #131 from elliottmurray/python38 (Elliott Murray, Sat May 9 17:47:13 2020 +0100) - * bb921eb - Updated to 3.8 (Elliott Murray, Sat Apr 4 16:39:18 2020 +0100) - * 12108c4 - Merge pull request #132 from pyasi/pyasi_test_refactor (Elliott Murray, Sat May 9 17:33:59 2020 +0100) - * 48ad173 - Merge pull request #135 from m-aciek/master (Elliott Murray, Sat May 9 17:21:52 2020 +0100) - * 6948482 - Merge pull request #136 from pact-foundation/chore/upgrade-to-pact-ruby-standalone-1-84-0 (Elliott Murray, Sat May 9 15:13:07 2020 +0100) - * 14603ac - feat: update standalone to 1.84.0 (Beth Skurrie, Sat May 2 09:43:30 2020 +1000) - * 410caf1 - chore: add script to create a PR to update the pact-ruby-standalone version (Beth Skurrie, Sat May 2 09:42:55 2020 +1000) - * b5af1fc - Fix missing normalization of consumer name while publishing pact (Maciej Olko, Thu Apr 30 08:50:17 2020 +0200) - * 5785782 - Move tests to standard tests dir (Peter Yasi, Fri Apr 17 14:03:04 2020 -0400) - * 88ea23d - docs: update RELEASING.md (Beth Skurrie, Tue Feb 18 10:46:11 2020 +1100) + +- d112a4a - Merge pull request #134 from elliottmurray/multiple-custom-provider-header (Elliott Murray, Mon May 11 16:32:49 2020 +0100) +- 58f8e6b - Fix some style issues (Elliott Murray, Wed Apr 29 12:35:00 2020 +0100) +- bf9bc2d - Added multiple click options for custom headers (Elliott Murray, Tue Apr 28 18:14:02 2020 +0100) +- 254ffc5 - Merge pull request #130 from elliottmurray/examples (Elliott Murray, Sat May 9 18:02:46 2020 +0100) +- 3898aee - Created examples folder (Elliott Murray, Thu Apr 2 14:03:17 2020 +0100) +- b859443 - Merge pull request #129 from elliottmurray/docker (Elliott Murray, Sat May 9 17:54:01 2020 +0100) +- 9b83da7 - Added bash to containers (Elliott Murray, Fri Apr 10 11:52:43 2020 +0100) +- 73db8fc - Remove subprocess requirement (Elliott Murray, Fri Apr 10 11:32:38 2020 +0100) +- f3315a1 - Added 38 and created build helper script (Elliott Murray, Wed Apr 1 17:11:06 2020 +0100) +- e7743de - Some readme and python37 (Elliott Murray, Wed Apr 1 14:25:20 2020 +0100) +- 515aeb2 - Tweaked the run script (Elliott Murray, Wed Apr 1 12:57:02 2020 +0100) +- 5a6acaf - Merge pull request #131 from elliottmurray/python38 (Elliott Murray, Sat May 9 17:47:13 2020 +0100) +- bb921eb - Updated to 3.8 (Elliott Murray, Sat Apr 4 16:39:18 2020 +0100) +- 12108c4 - Merge pull request #132 from pyasi/pyasi_test_refactor (Elliott Murray, Sat May 9 17:33:59 2020 +0100) +- 48ad173 - Merge pull request #135 from m-aciek/master (Elliott Murray, Sat May 9 17:21:52 2020 +0100) +- 6948482 - Merge pull request #136 from pact-foundation/chore/upgrade-to-pact-ruby-standalone-1-84-0 (Elliott Murray, Sat May 9 15:13:07 2020 +0100) +- 14603ac - feat: update standalone to 1.84.0 (Beth Skurrie, Sat May 2 09:43:30 2020 +1000) +- 410caf1 - chore: add script to create a PR to update the pact-ruby-standalone version (Beth Skurrie, Sat May 2 09:42:55 2020 +1000) +- b5af1fc - Fix missing normalization of consumer name while publishing pact (Maciej Olko, Thu Apr 30 08:50:17 2020 +0200) +- 5785782 - Move tests to standard tests dir (Peter Yasi, Fri Apr 17 14:03:04 2020 -0400) +- 88ea23d - docs: update RELEASING.md (Beth Skurrie, Tue Feb 18 10:46:11 2020 +1100) ### 0.21.0 -* 6352dda - feat: update to pact-ruby-standalone-1.79.0 (#127) (Beth Skurrie, Tue Feb 18 10:25:59 2020 +1100) -* 758d6ea - Converting to kwargs (Elliott Murray, Sat Feb 1 16:24:49 2020 +1100) -* 1388b8f - feat: support using environment variables to set pact broker configuration (mikahjc, Wed Jan 29 17:52:33 2020 -0700) -* ec7ff99 - Make verify tests compatible with Click v7.x (mikahjc, Tue Jun 11 16:37:13 2019 -0600) -* 5dcb56c - Add broker_token parameter for authentication (mikahjc, Tue Jun 11 16:16:46 2019 -0600) -* 1bdfb42 - Integrate the Ruby pact broker client to allow for automatic publishing of pacts (mikahjc, Tue Jun 11 11:13:18 2019 -0600) + +- 6352dda - feat: update to pact-ruby-standalone-1.79.0 (#127) (Beth Skurrie, Tue Feb 18 10:25:59 2020 +1100) +- 758d6ea - Converting to kwargs (Elliott Murray, Sat Feb 1 16:24:49 2020 +1100) +- 1388b8f - feat: support using environment variables to set pact broker configuration (mikahjc, Wed Jan 29 17:52:33 2020 -0700) +- ec7ff99 - Make verify tests compatible with Click v7.x (mikahjc, Tue Jun 11 16:37:13 2019 -0600) +- 5dcb56c - Add broker_token parameter for authentication (mikahjc, Tue Jun 11 16:16:46 2019 -0600) +- 1bdfb42 - Integrate the Ruby pact broker client to allow for automatic publishing of pacts (mikahjc, Tue Jun 11 11:13:18 2019 -0600) ### 0.20.0 - * 978d9f3 - fix typo (Jingjing Duan, Wed May 24 15:48:43 2017 -0700) - * 4ede7d5 - Merge pull request #117 from dlmiddlecote/feature/expose-more-options (Matt Fellows, Fri Jan 17 10:00:56 2020 +1100) - * 73ae8d2 - Update docs (Daniel Middlecote, Tue Jan 14 22:11:40 2020 +0000) - * 2bffe5e - Simple test case (Daniel Middlecote, Tue Jan 14 22:11:25 2020 +0000) - * 3ba51b5 - Add --broker-token support (Daniel Middlecote, Tue Jan 14 22:04:39 2020 +0000) - * d3a8ba6 - Update pact-ruby-standalone (Daniel Middlecote, Tue Jan 14 21:45:01 2020 +0000) - * 0cbb9d4 - Merge pull request #115 from ejrb/patch-1 (Matthew Balvanz, Sat Dec 14 20:49:56 2019 -0600) - * 0c85502 - match platforms like 'macOS-*' to osx suffix (ejrb, Mon Dec 9 11:13:19 2019 +0000) - * 9a0eaa7 - Merge pull request #109 from pact-foundation/dependabot/pip/flask-1.0 (Matthew Balvanz, Mon Sep 30 21:35:20 2019 -0500) - * 6f70a28 - Bump flask from 0.11.1 to 1.0 (dependabot[bot], Sat Sep 28 19:20:11 2019 +0000) + +- 978d9f3 - fix typo (Jingjing Duan, Wed May 24 15:48:43 2017 -0700) +- 4ede7d5 - Merge pull request #117 from dlmiddlecote/feature/expose-more-options (Matt Fellows, Fri Jan 17 10:00:56 2020 +1100) +- 73ae8d2 - Update docs (Daniel Middlecote, Tue Jan 14 22:11:40 2020 +0000) +- 2bffe5e - Simple test case (Daniel Middlecote, Tue Jan 14 22:11:25 2020 +0000) +- 3ba51b5 - Add --broker-token support (Daniel Middlecote, Tue Jan 14 22:04:39 2020 +0000) +- d3a8ba6 - Update pact-ruby-standalone (Daniel Middlecote, Tue Jan 14 21:45:01 2020 +0000) +- 0cbb9d4 - Merge pull request #115 from ejrb/patch-1 (Matthew Balvanz, Sat Dec 14 20:49:56 2019 -0600) +- 0c85502 - match platforms like 'macOS-\*' to osx suffix (ejrb, Mon Dec 9 11:13:19 2019 +0000) +- 9a0eaa7 - Merge pull request #109 from pact-foundation/dependabot/pip/flask-1.0 (Matthew Balvanz, Mon Sep 30 21:35:20 2019 -0500) +- 6f70a28 - Bump flask from 0.11.1 to 1.0 (dependabot[bot], Sat Sep 28 19:20:11 2019 +0000) ### 0.19.0 - * fed5fba - Start testing in Python 3.7 (Matthew Balvanz, Sat Sep 28 15:18:17 2019 -0500) - * 19aa689 - Adjust tests to support click 2.0.0 to 7.0.0 (Matthew Balvanz, Sat Sep 28 15:04:53 2019 -0500) - * 9d4d6f3 - Merge pull request #94 from yangineer/optional_given (Matthew Balvanz, Sat Sep 28 14:52:49 2019 -0500) - * b286b30 - Merge branch 'master' into optional_given (Matthew Balvanz, Sat Sep 28 14:50:02 2019 -0500) - * 68e792a - Merge pull request #93 from francoiscampbell/pass_file_write_mode (Matthew Balvanz, Sat Sep 28 14:18:59 2019 -0500) - * 8927df6 - Updated the tests for Click v7 (Yang Wang, Sat Oct 20 00:34:37 2018 -0400) - * 125a1de - Changed given to be optional (Yang Wang, Sat Oct 20 00:27:07 2018 -0400) - * 68527e0 - max out click at 6.7 because 7.0 fails tests (Francois Campbell, Thu Oct 4 10:57:13 2018 -0400) - * 2452f42 - update tests (Francois Campbell, Thu Oct 4 10:56:42 2018 -0400) - * 1116601 - Add param docs (Francois Campbell, Thu Oct 4 10:04:50 2018 -0400) - * 48a7591 - Pass file_write_mode from Consumer to Pact (Francois Campbell, Thu Oct 4 10:00:37 2018 -0400) - * 6d39609 - Merge pull request #91 from szekar1/small_updates_to_docs (Matthew Balvanz, Fri Aug 24 13:49:05 2018 -0500) - * a5c8146 - Update README.md (bvccaneer, Fri Aug 24 19:23:26 2018 +0200) - * 4d40485 - adding documentation around #52 and fixing dead link for Matching docs (szekar1, Fri Aug 24 19:19:10 2018 +0200) + +- fed5fba - Start testing in Python 3.7 (Matthew Balvanz, Sat Sep 28 15:18:17 2019 -0500) +- 19aa689 - Adjust tests to support click 2.0.0 to 7.0.0 (Matthew Balvanz, Sat Sep 28 15:04:53 2019 -0500) +- 9d4d6f3 - Merge pull request #94 from yangineer/optional_given (Matthew Balvanz, Sat Sep 28 14:52:49 2019 -0500) +- b286b30 - Merge branch 'master' into optional_given (Matthew Balvanz, Sat Sep 28 14:50:02 2019 -0500) +- 68e792a - Merge pull request #93 from francoiscampbell/pass_file_write_mode (Matthew Balvanz, Sat Sep 28 14:18:59 2019 -0500) +- 8927df6 - Updated the tests for Click v7 (Yang Wang, Sat Oct 20 00:34:37 2018 -0400) +- 125a1de - Changed given to be optional (Yang Wang, Sat Oct 20 00:27:07 2018 -0400) +- 68527e0 - max out click at 6.7 because 7.0 fails tests (Francois Campbell, Thu Oct 4 10:57:13 2018 -0400) +- 2452f42 - update tests (Francois Campbell, Thu Oct 4 10:56:42 2018 -0400) +- 1116601 - Add param docs (Francois Campbell, Thu Oct 4 10:04:50 2018 -0400) +- 48a7591 - Pass file_write_mode from Consumer to Pact (Francois Campbell, Thu Oct 4 10:00:37 2018 -0400) +- 6d39609 - Merge pull request #91 from szekar1/small_updates_to_docs (Matthew Balvanz, Fri Aug 24 13:49:05 2018 -0500) +- a5c8146 - Update README.md (bvccaneer, Fri Aug 24 19:23:26 2018 +0200) +- 4d40485 - adding documentation around #52 and fixing dead link for Matching docs (szekar1, Fri Aug 24 19:19:10 2018 +0200) ### 0.18.0 - * 4e8bb85 - Upgrade pact-ruby-standalone (Matthew Balvanz, Tue Aug 21 08:56:53 2018 -0500) - * 8a44feb - chore(docs): update contact information (Matt Fellows, Thu Aug 2 17:18:43 2018 +1000) + +- 4e8bb85 - Upgrade pact-ruby-standalone (Matthew Balvanz, Tue Aug 21 08:56:53 2018 -0500) +- 8a44feb - chore(docs): update contact information (Matt Fellows, Thu Aug 2 17:18:43 2018 +1000) ### 0.17.0 - * cf5d5bc - Merge pull request #87 from acabelloj/custom-provider-header-support (Matthew Balvanz, Fri Jul 20 22:27:33 2018 -0500) - * cc61427 - Fixes #83 The verifier always returns exit code 0 (Matthew Balvanz, Fri Jul 20 22:08:26 2018 -0500) - * 239da1c - Remove Python 3.3 from Travis builds (Matthew Balvanz, Wed Jul 4 10:39:12 2018 -0500) - * 273b3fd - Remove Python 3.3 testing (Matthew Balvanz, Wed Jul 4 10:36:01 2018 -0500) - * 01c6763 - Add support to custom provider header (Alejandro Cabello Jiménez, Fri Jun 1 11:40:32 2018 +0200) + +- cf5d5bc - Merge pull request #87 from acabelloj/custom-provider-header-support (Matthew Balvanz, Fri Jul 20 22:27:33 2018 -0500) +- cc61427 - Fixes #83 The verifier always returns exit code 0 (Matthew Balvanz, Fri Jul 20 22:08:26 2018 -0500) +- 239da1c - Remove Python 3.3 from Travis builds (Matthew Balvanz, Wed Jul 4 10:39:12 2018 -0500) +- 273b3fd - Remove Python 3.3 testing (Matthew Balvanz, Wed Jul 4 10:36:01 2018 -0500) +- 01c6763 - Add support to custom provider header (Alejandro Cabello Jiménez, Fri Jun 1 11:40:32 2018 +0200) ### 0.16.1 - * eecbb60 - Merge pull request #79 from shahha/fix-stopping-mock-service-on-windows (Matthew Balvanz, Fri Mar 16 08:45:19 2018 -0500) - * 4115264 - Added windows specific code to check if mock service is stopped. (Hardik Shah, Wed Mar 7 10:44:33 2018 +1100) + +- eecbb60 - Merge pull request #79 from shahha/fix-stopping-mock-service-on-windows (Matthew Balvanz, Fri Mar 16 08:45:19 2018 -0500) +- 4115264 - Added windows specific code to check if mock service is stopped. (Hardik Shah, Wed Mar 7 10:44:33 2018 +1100) ### 0.16.0 - * 30af240 - Merge pull request #78 from pact-foundation/standalone-1-29-2 (Matthew Balvanz☃, Fri Mar 2 22:05:12 2018 -0600) - * d428951 - Update to pact-ruby-standalone 1.29.2 (Matthew Balvanz, Fri Mar 2 21:59:08 2018 -0600) + +- 30af240 - Merge pull request #78 from pact-foundation/standalone-1-29-2 (Matthew Balvanz☃, Fri Mar 2 22:05:12 2018 -0600) +- d428951 - Update to pact-ruby-standalone 1.29.2 (Matthew Balvanz, Fri Mar 2 21:59:08 2018 -0600) ### 0.15.0 - * eb925c3 - Merge pull request #77 from pact-foundation/standalone-1-9-1 (Matthew Balvanz☃, Fri Mar 2 21:22:35 2018 -0600) - * 2a2dcd1 - Upgrade to pact-ruby-standalone 1.9.1 (Matthew Balvanz, Fri Mar 2 21:18:25 2018 -0600) - * 53545be - Merge pull request #72 from fabianbuechler/reduce-server-start-timeout (Matthew Balvanz☃, Fri Mar 2 21:04:03 2018 -0600) - * b782e43 - Merge pull request #76 from pact-foundation/hide-ruby-stacks (Matthew Balvanz☃, Fri Mar 2 21:03:14 2018 -0600) - * 589224a - Hide Ruby stack traces by default (Matthew Balvanz, Fri Mar 2 20:56:59 2018 -0600) - * e952b37 - Reduce timeout in _wait_for_server_start to 25s (Fabian Büchler, Fri Feb 9 11:04:01 2018 +0100) + +- eb925c3 - Merge pull request #77 from pact-foundation/standalone-1-9-1 (Matthew Balvanz☃, Fri Mar 2 21:22:35 2018 -0600) +- 2a2dcd1 - Upgrade to pact-ruby-standalone 1.9.1 (Matthew Balvanz, Fri Mar 2 21:18:25 2018 -0600) +- 53545be - Merge pull request #72 from fabianbuechler/reduce-server-start-timeout (Matthew Balvanz☃, Fri Mar 2 21:04:03 2018 -0600) +- b782e43 - Merge pull request #76 from pact-foundation/hide-ruby-stacks (Matthew Balvanz☃, Fri Mar 2 21:03:14 2018 -0600) +- 589224a - Hide Ruby stack traces by default (Matthew Balvanz, Fri Mar 2 20:56:59 2018 -0600) +- e952b37 - Reduce timeout in \_wait_for_server_start to 25s (Fabian Büchler, Fri Feb 9 11:04:01 2018 +0100) ### 0.14.0 - * 3070638 - Merge pull request #71 from pact-foundation/update-standalone-1-9-0 (Matthew Balvanz, Sat Feb 3 23:25:37 2018 -0600) - * 475703c - Resolves #58: Update to pact-ruby-standalone 1.9.0 (Matthew Balvanz, Sat Feb 3 23:12:22 2018 -0600) + +- 3070638 - Merge pull request #71 from pact-foundation/update-standalone-1-9-0 (Matthew Balvanz, Sat Feb 3 23:25:37 2018 -0600) +- 475703c - Resolves #58: Update to pact-ruby-standalone 1.9.0 (Matthew Balvanz, Sat Feb 3 23:12:22 2018 -0600) ### 0.13.0 - * 3316743 - Merge pull request #69 from jawu/#52-helper-function-for-assertion-with-matchers (Matthew Balvanz, Sat Jan 20 16:43:56 2018 -0600) - * ae7f333 - Merge pull request #70 from bethesque/issues/pact-provider-verifier-19 (Matthew Balvanz, Sat Jan 20 16:40:31 2018 -0600) - * 81597d9 - docs: remove reference to v3 pact in provider-states-setup-url (Beth Skurrie, Tue Jan 9 12:27:18 2018 +1100) - * 8bedfd4 - removed local files (Janneck Wullschleger, Wed Dec 20 05:12:08 2017 +0100) - * 5ab2648 - solves #52 added get_generated_values to resolve Mathers to their generated value for assertion (Janneck Wullschleger, Wed Dec 20 05:06:33 2017 +0100) + +- 3316743 - Merge pull request #69 from jawu/#52-helper-function-for-assertion-with-matchers (Matthew Balvanz, Sat Jan 20 16:43:56 2018 -0600) +- ae7f333 - Merge pull request #70 from bethesque/issues/pact-provider-verifier-19 (Matthew Balvanz, Sat Jan 20 16:40:31 2018 -0600) +- 81597d9 - docs: remove reference to v3 pact in provider-states-setup-url (Beth Skurrie, Tue Jan 9 12:27:18 2018 +1100) +- 8bedfd4 - removed local files (Janneck Wullschleger, Wed Dec 20 05:12:08 2017 +0100) +- 5ab2648 - solves #52 added get_generated_values to resolve Mathers to their generated value for assertion (Janneck Wullschleger, Wed Dec 20 05:06:33 2017 +0100) ### 0.12.0 - * 149dfc7 - Merge pull request #67 from jawu/enable-possibility-to-use-mathers-in-path (Matthew Balvanz, Sun Dec 17 10:32:34 2017 -0600) - * fb97d2f - fixed doc string of Request (Janneck Wullschleger, Sat Dec 16 20:44:11 2017 +0100) - * c2c24cc - adjusted doc string of Request calss to allow str and Matcher as path parameter (Janneck Wullschleger, Sat Dec 16 20:40:35 2017 +0100) - * 697a6a2 - fixed port parameter in e2e test for python 2.7 (Janneck Wullschleger, Thu Dec 14 15:08:05 2017 +0100) - * ca2eb92 - added from_term call in Request constructor to process path property for json transport (Janneck Wullschleger, Thu Dec 14 14:45:11 2017 +0100) + +- 149dfc7 - Merge pull request #67 from jawu/enable-possibility-to-use-mathers-in-path (Matthew Balvanz, Sun Dec 17 10:32:34 2017 -0600) +- fb97d2f - fixed doc string of Request (Janneck Wullschleger, Sat Dec 16 20:44:11 2017 +0100) +- c2c24cc - adjusted doc string of Request calss to allow str and Matcher as path parameter (Janneck Wullschleger, Sat Dec 16 20:40:35 2017 +0100) +- 697a6a2 - fixed port parameter in e2e test for python 2.7 (Janneck Wullschleger, Thu Dec 14 15:08:05 2017 +0100) +- ca2eb92 - added from_term call in Request constructor to process path property for json transport (Janneck Wullschleger, Thu Dec 14 14:45:11 2017 +0100) ### 0.11.0 - * ad69039 - Merge pull request #63 from pact-foundation/run-specific-interactions (Matthew Balvanz, Sun Dec 17 09:53:35 2017 -0600) - * eb63864 - Output a rerun command when a verification fails (Matthew Balvanz, Sun Nov 19 20:44:06 2017 -0600) - * 7c7bc7d - Merge pull request #62 from dhoomakethu/master (Matthew Balvanz, Sun Nov 19 19:53:48 2017 -0600) - * c27a7a9 - #62 Fix flake8 issues 2 (sanjay, Sun Nov 19 11:18:15 2017 +0530) - * 382c46c - #62 fix flake issues (sanjay, Sun Nov 19 11:13:58 2017 +0530) - * cdcc85d - Add support to publish verification result to pact broker (sanjay, Tue Oct 31 12:41:52 2017 +0530) - * c1a5402 - Merge pull request #2 from pact-foundation/master (dhoomakethu, Tue Oct 31 12:15:53 2017 +0530) - * b91f6c3 - Merge pull request #1 from pact-foundation/master (dhoomakethu, Mon Aug 21 12:36:15 2017 +0530) + +- ad69039 - Merge pull request #63 from pact-foundation/run-specific-interactions (Matthew Balvanz, Sun Dec 17 09:53:35 2017 -0600) +- eb63864 - Output a rerun command when a verification fails (Matthew Balvanz, Sun Nov 19 20:44:06 2017 -0600) +- 7c7bc7d - Merge pull request #62 from dhoomakethu/master (Matthew Balvanz, Sun Nov 19 19:53:48 2017 -0600) +- c27a7a9 - #62 Fix flake8 issues 2 (sanjay, Sun Nov 19 11:18:15 2017 +0530) +- 382c46c - #62 fix flake issues (sanjay, Sun Nov 19 11:13:58 2017 +0530) +- cdcc85d - Add support to publish verification result to pact broker (sanjay, Tue Oct 31 12:41:52 2017 +0530) +- c1a5402 - Merge pull request #2 from pact-foundation/master (dhoomakethu, Tue Oct 31 12:15:53 2017 +0530) +- b91f6c3 - Merge pull request #1 from pact-foundation/master (dhoomakethu, Mon Aug 21 12:36:15 2017 +0530) ### 0.10.0 - * 821671e - Merge pull request #53 from pact-foundation/verify-directories (Matthew Balvanz, Sat Nov 18 23:26:05 2017 -0600) - * 8291bb7 - Resolve #22: --pact-url accepts directories (Matthew Balvanz, Sat Oct 7 11:35:37 2017 -0500) + +- 821671e - Merge pull request #53 from pact-foundation/verify-directories (Matthew Balvanz, Sat Nov 18 23:26:05 2017 -0600) +- 8291bb7 - Resolve #22: --pact-url accepts directories (Matthew Balvanz, Sat Oct 7 11:35:37 2017 -0500) ### 0.9.0 - * 735aa87 - Set new project minimum requirements (Matthew Balvanz, Sun Oct 22 16:30:12 2017 -0500) - * 295f17c - Merge pull request #60 from ftobia/requirements (Matthew Balvanz, Sun Oct 22 16:09:59 2017 -0500) - * 1dc72da - Merge pull request #48 from bassdread/allow-later-versions-of-requests (Matthew Balvanz, Sun Oct 22 16:09:39 2017 -0500) - * 3265b45 - add suggestion (Chris Hannam, Fri Oct 20 09:33:05 2017 +0100) - * 33504a6 - Resolve #51 verify outputs text instead of bytes (Matthew Balvanz, Thu Oct 19 21:28:39 2017 -0500) - * 51dcda3 - Merge pull request #57 from jceplaras/fix-e2e-test-incorrect-number-of-arg (Matthew Balvanz, Thu Oct 19 20:57:49 2017 -0500) - * 1a4d136 - Relax version requirements in setup.py (vs requirements.txt) (ftobia, Fri Oct 13 19:42:46 2017 -0400) - * 8ece1d6 - Fix incorrect indent on test_incorrect_number_of_arguments on test_e2e (James Plaras, Fri Oct 13 12:54:56 2017 +0800) - * 5f8257b - Resolve #50: Note which version of the Pact specification is supported (Matthew Balvanz, Sat Oct 7 14:05:26 2017 -0500) - * e728301 - Resolve #45: Document request query parameter (Matthew Balvanz, Sat Oct 7 13:58:07 2017 -0500) - * 5de7200 - Merge pull request #49 from pact-foundation/rename-somethinglike (Matt Fellows, Wed Oct 4 22:36:21 2017 +1100) - * d73aa1c - Resolve #43: Rename SomethingLike to Like (Matthew Balvanz, Mon Sep 4 15:49:13 2017 -0500) - * a07c8b6 - Merge pull request #46 from bassdread/fix-setup-url-name (Matthew Balvanz, Mon Sep 4 15:44:45 2017 -0500) - * b5e1f95 - allow later versions of requests (Chris Hannam, Tue Aug 29 13:38:42 2017 +0100) - * 08fe123 - make setup-url name format match above reference (Chris Hannam, Fri Aug 25 11:03:35 2017 +0100) + +- 735aa87 - Set new project minimum requirements (Matthew Balvanz, Sun Oct 22 16:30:12 2017 -0500) +- 295f17c - Merge pull request #60 from ftobia/requirements (Matthew Balvanz, Sun Oct 22 16:09:59 2017 -0500) +- 1dc72da - Merge pull request #48 from bassdread/allow-later-versions-of-requests (Matthew Balvanz, Sun Oct 22 16:09:39 2017 -0500) +- 3265b45 - add suggestion (Chris Hannam, Fri Oct 20 09:33:05 2017 +0100) +- 33504a6 - Resolve #51 verify outputs text instead of bytes (Matthew Balvanz, Thu Oct 19 21:28:39 2017 -0500) +- 51dcda3 - Merge pull request #57 from jceplaras/fix-e2e-test-incorrect-number-of-arg (Matthew Balvanz, Thu Oct 19 20:57:49 2017 -0500) +- 1a4d136 - Relax version requirements in setup.py (vs requirements.txt) (ftobia, Fri Oct 13 19:42:46 2017 -0400) +- 8ece1d6 - Fix incorrect indent on test_incorrect_number_of_arguments on test_e2e (James Plaras, Fri Oct 13 12:54:56 2017 +0800) +- 5f8257b - Resolve #50: Note which version of the Pact specification is supported (Matthew Balvanz, Sat Oct 7 14:05:26 2017 -0500) +- e728301 - Resolve #45: Document request query parameter (Matthew Balvanz, Sat Oct 7 13:58:07 2017 -0500) +- 5de7200 - Merge pull request #49 from pact-foundation/rename-somethinglike (Matt Fellows, Wed Oct 4 22:36:21 2017 +1100) +- d73aa1c - Resolve #43: Rename SomethingLike to Like (Matthew Balvanz, Mon Sep 4 15:49:13 2017 -0500) +- a07c8b6 - Merge pull request #46 from bassdread/fix-setup-url-name (Matthew Balvanz, Mon Sep 4 15:44:45 2017 -0500) +- b5e1f95 - allow later versions of requests (Chris Hannam, Tue Aug 29 13:38:42 2017 +0100) +- 08fe123 - make setup-url name format match above reference (Chris Hannam, Fri Aug 25 11:03:35 2017 +0100) ### 0.8.0 - * edb6c72 - Merge pull request #41 from pact-foundation/fix-running-on-windows (Matthew Balvanz, Thu Aug 10 21:39:27 2017 -0500) - * 244fff1 - Merge pull request #42 from pact-foundation/deprecate-provider-states-url (Matthew Balvanz, Thu Aug 10 21:38:44 2017 -0500) - * 447b8bb - Resolve #17: Deprecate --provider-states-url (Matthew Balvanz, Sat Jul 29 11:53:05 2017 -0500) - * 4661406 - Move to using the `service` command with pact-mock-service (Matthew Balvanz, Sat Jul 29 10:00:47 2017 -0500) - * 04107db - Remove the PyPi server declaration to use the defaults (Matthew Balvanz, Sun Jul 16 09:05:30 2017 -0500) + +- edb6c72 - Merge pull request #41 from pact-foundation/fix-running-on-windows (Matthew Balvanz, Thu Aug 10 21:39:27 2017 -0500) +- 244fff1 - Merge pull request #42 from pact-foundation/deprecate-provider-states-url (Matthew Balvanz, Thu Aug 10 21:38:44 2017 -0500) +- 447b8bb - Resolve #17: Deprecate --provider-states-url (Matthew Balvanz, Sat Jul 29 11:53:05 2017 -0500) +- 4661406 - Move to using the `service` command with pact-mock-service (Matthew Balvanz, Sat Jul 29 10:00:47 2017 -0500) +- 04107db - Remove the PyPi server declaration to use the defaults (Matthew Balvanz, Sun Jul 16 09:05:30 2017 -0500) ### v0.7.0 - * 223ea76 - Merge pull request #32 from SimKev2/pacturls (Matthew Balvanz, Sun Jul 16 08:41:14 2017 -0500) - * e382eb4 - Add tests for #36 SomethingLike not supporting Terms (Matthew Balvanz, Sun Jul 16 08:36:58 2017 -0500) - * 05b4d70 - Merge pull request #37 from jeanbaptistepriez/fix-somethinglike (Matthew Balvanz, Sun Jul 16 08:30:28 2017 -0500) - * 29a2518 - Fix json generation of SomethingLike (https://github.com/pact-foundation/pact-python/issues/36) (jean-baptiste.priez, Wed Jul 12 20:01:58 2017 +0200) - * b6e1a8b - Issue: Cannot supply multiple files to pact-verifier - PR: Added deprecation warning instead of making api-breaking change (simkev2, Sat Jun 24 20:05:05 2017 -0500) - * 17aa15b - Issue: Cannot supply multiple files to pact-verifier - Updated '--pact-urls' to be a single comma separated string argument - Added '--pact-url' which can be specified multiple times (simkev2, Sat Jun 24 12:57:51 2017 -0500) - * 65b493d - Merge pull request #33 from bethesque/reamde (Matthew Balvanz, Tue Jun 27 08:58:08 2017 -0500) - * f5a5958 - Update README.md (Beth Skurrie, Sun Jun 25 10:37:03 2017 +1000) + +- 223ea76 - Merge pull request #32 from SimKev2/pacturls (Matthew Balvanz, Sun Jul 16 08:41:14 2017 -0500) +- e382eb4 - Add tests for #36 SomethingLike not supporting Terms (Matthew Balvanz, Sun Jul 16 08:36:58 2017 -0500) +- 05b4d70 - Merge pull request #37 from jeanbaptistepriez/fix-somethinglike (Matthew Balvanz, Sun Jul 16 08:30:28 2017 -0500) +- 29a2518 - Fix json generation of SomethingLike (https://github.com/pact-foundation/pact-python/issues/36) (jean-baptiste.priez, Wed Jul 12 20:01:58 2017 +0200) +- b6e1a8b - Issue: Cannot supply multiple files to pact-verifier - PR: Added deprecation warning instead of making api-breaking change (simkev2, Sat Jun 24 20:05:05 2017 -0500) +- 17aa15b - Issue: Cannot supply multiple files to pact-verifier - Updated '--pact-urls' to be a single comma separated string argument - Added '--pact-url' which can be specified multiple times (simkev2, Sat Jun 24 12:57:51 2017 -0500) +- 65b493d - Merge pull request #33 from bethesque/reamde (Matthew Balvanz, Tue Jun 27 08:58:08 2017 -0500) +- f5a5958 - Update README.md (Beth Skurrie, Sun Jun 25 10:37:03 2017 +1000) ### v0.6.2 -* 69caa40 - Merge pull request #35 from pact-foundation/fix-broker-credentials (Matt Fellows, Tue Jun 27 20:49:35 2017 +1000) -* d60f37f - Fix the use of broker credentials (Matthew Balvanz, Mon Jun 26 21:14:53 2017 -0500) + +- 69caa40 - Merge pull request #35 from pact-foundation/fix-broker-credentials (Matt Fellows, Tue Jun 27 20:49:35 2017 +1000) +- d60f37f - Fix the use of broker credentials (Matthew Balvanz, Mon Jun 26 21:14:53 2017 -0500) ### v0.6.1 -* 14968ea - Merge pull request #34 from hartror/rh_version_fix (Matthew Balvanz, Mon Jun 26 20:23:29 2017 -0500) -* aca520f - pydocstyle is fussy, should have run it before pushing (Rory Hart, Sun Jun 25 20:11:26 2017 +1000) -* b70103c - Added docstring for __version__.py (Rory Hart, Sun Jun 25 20:08:50 2017 +1000) -* 2076e34 - Disabled flake8 F401 for __version__ import (Rory Hart, Sun Jun 25 20:05:24 2017 +1000) -* 2912e07 - Version in setup.py reading __version__.py directly (Rory Hart, Sun Jun 25 19:40:08 2017 +1000) -* d137a21 - Split tox environments into test & install to replicate installation issue #31 (Rory Hart, Sun Jun 25 19:16:57 2017 +1000) -* f549ddf - Merge pull request #30 from bethesque/contributing (Matthew Balvanz, Sat Jun 24 12:43:30 2017 -0500) -* 1f19a0e - Update CONTRIBUTING.md (Beth Skurrie, Thu Jun 22 08:51:35 2017 +1000) -* 3198817 - Update CONTRIBUTING.md (Beth Skurrie, Thu Jun 22 08:36:57 2017 +1000) -* 7a08bb2 - Update CONTRIBUTING.md (Beth Skurrie, Thu Jun 22 08:35:27 2017 +1000) + +- 14968ea - Merge pull request #34 from hartror/rh_version_fix (Matthew Balvanz, Mon Jun 26 20:23:29 2017 -0500) +- aca520f - pydocstyle is fussy, should have run it before pushing (Rory Hart, Sun Jun 25 20:11:26 2017 +1000) +- b70103c - Added docstring for **version**.py (Rory Hart, Sun Jun 25 20:08:50 2017 +1000) +- 2076e34 - Disabled flake8 F401 for **version** import (Rory Hart, Sun Jun 25 20:05:24 2017 +1000) +- 2912e07 - Version in setup.py reading **version**.py directly (Rory Hart, Sun Jun 25 19:40:08 2017 +1000) +- d137a21 - Split tox environments into test & install to replicate installation issue #31 (Rory Hart, Sun Jun 25 19:16:57 2017 +1000) +- f549ddf - Merge pull request #30 from bethesque/contributing (Matthew Balvanz, Sat Jun 24 12:43:30 2017 -0500) +- 1f19a0e - Update CONTRIBUTING.md (Beth Skurrie, Thu Jun 22 08:51:35 2017 +1000) +- 3198817 - Update CONTRIBUTING.md (Beth Skurrie, Thu Jun 22 08:36:57 2017 +1000) +- 7a08bb2 - Update CONTRIBUTING.md (Beth Skurrie, Thu Jun 22 08:35:27 2017 +1000) ### v0.6.0 -* 10aaaf6 - Merge pull request #27 from pact-foundation/download-pre-package-mock-service-and-verifier (Matthew Balvanz, Tue Jun 20 21:51:40 2017 -0500) -* a9b991b - Update to pact-ruby-standalone 1.0.0 (Matthew Balvanz, Mon Jun 19 10:17:09 2017 -0500) -* ab43c8b - Switch to installing the packages from pact-ruby-standalone (Matthew Balvanz, Wed May 31 21:00:51 2017 -0500) -* db3e7c3 - Use the compiled Ruby applications from pact-mock-service and pact-provider-verifier (Matthew Balvanz, Mon May 29 22:18:47 2017 -0500) + +- 10aaaf6 - Merge pull request #27 from pact-foundation/download-pre-package-mock-service-and-verifier (Matthew Balvanz, Tue Jun 20 21:51:40 2017 -0500) +- a9b991b - Update to pact-ruby-standalone 1.0.0 (Matthew Balvanz, Mon Jun 19 10:17:09 2017 -0500) +- ab43c8b - Switch to installing the packages from pact-ruby-standalone (Matthew Balvanz, Wed May 31 21:00:51 2017 -0500) +- db3e7c3 - Use the compiled Ruby applications from pact-mock-service and pact-provider-verifier (Matthew Balvanz, Mon May 29 22:18:47 2017 -0500) ### v0.5.0 -* c085a01 - Merge pull request #26 from AnObfuscator/stub-multiple-requests (Matthew Balvanz, Mon Jun 19 09:14:51 2017 -0500) -* 22c0272 - Add support for stubbing multiple requests at the same time (AnObfuscator, Fri Jun 16 23:18:01 2017 -0500) + +- c085a01 - Merge pull request #26 from AnObfuscator/stub-multiple-requests (Matthew Balvanz, Mon Jun 19 09:14:51 2017 -0500) +- 22c0272 - Add support for stubbing multiple requests at the same time (AnObfuscator, Fri Jun 16 23:18:01 2017 -0500) ### v0.4.1 -* 66cf151 - Add RELEASING.md closes #18 (Matthew Balvanz, Tue May 30 22:41:06 2017 -0500) -* 3f61c91 - Add support for request bodies that are False in Python (Matthew Balvanz, Tue May 30 21:57:46 2017 -0500) -* a39c62f - Merge pull request #19 from ftobia/patch-1 (Matthew Balvanz, Tue May 30 21:42:41 2017 -0500) -* 95aa93a - Allow falsy responses (e.g. 0 not as a string). (Frank Tobia, Mon May 29 19:22:13 2017 -0400) -* dd3c703 - Merge pull request #16 from jduan/master (Jose Salvatierra, Thu May 25 09:20:10 2017 +0100) -* 978d9f3 - fix typo (Jingjing Duan, Wed May 24 15:48:43 2017 -0700) + +- 66cf151 - Add RELEASING.md closes #18 (Matthew Balvanz, Tue May 30 22:41:06 2017 -0500) +- 3f61c91 - Add support for request bodies that are False in Python (Matthew Balvanz, Tue May 30 21:57:46 2017 -0500) +- a39c62f - Merge pull request #19 from ftobia/patch-1 (Matthew Balvanz, Tue May 30 21:42:41 2017 -0500) +- 95aa93a - Allow falsy responses (e.g. 0 not as a string). (Frank Tobia, Mon May 29 19:22:13 2017 -0400) +- dd3c703 - Merge pull request #16 from jduan/master (Jose Salvatierra, Thu May 25 09:20:10 2017 +0100) +- 978d9f3 - fix typo (Jingjing Duan, Wed May 24 15:48:43 2017 -0700) ### v0.4.0 -* 8bec271 - Setup Travis CI to publish to PyPi (Matthew Balvanz, Wed May 24 16:51:05 2017 -0500) -* d67a015 - Merge pull request #14 from pact-foundation/verify-pacts (Matthew Balvanz, Wed May 24 16:46:49 2017 -0500) -* 78bd029 - Add CONTRIBUTING.md file resolves #4 (Matthew Balvanz, Mon May 22 20:41:09 2017 -0500) -* d7c32c4 - Repository badges (Matthew Balvanz, Mon May 22 20:22:14 2017 -0500) -* 97122f1 - Merge pull request #13 from pact-foundation/update-developer-documentation (Matthew Balvanz, Sat May 20 20:55:06 2017 -0500) -* ea015eb - Increment project to v0.4.0 (Matthew Balvanz, Fri May 19 23:46:00 2017 -0500) -* 51eb338 - Command line application for verifying pacts (Matthew Balvanz, Fri May 19 22:24:06 2017 -0500) -* 4b0bbd7 - Update the developer instructions (Matthew Balvanz, Fri May 19 22:05:54 2017 -0500) + +- 8bec271 - Setup Travis CI to publish to PyPi (Matthew Balvanz, Wed May 24 16:51:05 2017 -0500) +- d67a015 - Merge pull request #14 from pact-foundation/verify-pacts (Matthew Balvanz, Wed May 24 16:46:49 2017 -0500) +- 78bd029 - Add CONTRIBUTING.md file resolves #4 (Matthew Balvanz, Mon May 22 20:41:09 2017 -0500) +- d7c32c4 - Repository badges (Matthew Balvanz, Mon May 22 20:22:14 2017 -0500) +- 97122f1 - Merge pull request #13 from pact-foundation/update-developer-documentation (Matthew Balvanz, Sat May 20 20:55:06 2017 -0500) +- ea015eb - Increment project to v0.4.0 (Matthew Balvanz, Fri May 19 23:46:00 2017 -0500) +- 51eb338 - Command line application for verifying pacts (Matthew Balvanz, Fri May 19 22:24:06 2017 -0500) +- 4b0bbd7 - Update the developer instructions (Matthew Balvanz, Fri May 19 22:05:54 2017 -0500) ### v0.3.0 -* 3130f9a - Merge pull request #11 from pact-foundation/update-mock-service (Matthew Balvanz, Sun May 14 09:03:43 2017 -0500) -* 9b20d36 - Updated Versions of Pact Ruby applications (Matthew Balvanz, Sat May 13 09:43:44 2017 -0500) + +- 3130f9a - Merge pull request #11 from pact-foundation/update-mock-service (Matthew Balvanz, Sun May 14 09:03:43 2017 -0500) +- 9b20d36 - Updated Versions of Pact Ruby applications (Matthew Balvanz, Sat May 13 09:43:44 2017 -0500) ### v0.2.0 -* 140f583 - Merge pull request #8 from pact-foundation/manage-mock-service (Matthew Balvanz, Sat May 13 09:18:40 2017 -0500) -* 5994c3a - pact-python manages the mock service for the user (Matthew Balvanz, Tue May 9 21:58:08 2017 -0500) -* 4bf7b8b - pact-python manages the mock service for the user (Matthew Balvanz, Mon May 1 20:12:53 2017 -0500) -* 0a278af - pact-python manages the mock service for the user (Matthew Balvanz, Tue Apr 18 21:23:18 2017 -0500) -* fd68b41 - Merge pull request #2 from pact-foundation/package-ruby-apps (Matthew Balvanz, Sat Apr 22 10:55:48 2017 -0500) -* 75a96dc - Package the Ruby Mock Service and Verifier (Matthew Balvanz, Tue Apr 4 23:14:11 2017 -0500) + +- 140f583 - Merge pull request #8 from pact-foundation/manage-mock-service (Matthew Balvanz, Sat May 13 09:18:40 2017 -0500) +- 5994c3a - pact-python manages the mock service for the user (Matthew Balvanz, Tue May 9 21:58:08 2017 -0500) +- 4bf7b8b - pact-python manages the mock service for the user (Matthew Balvanz, Mon May 1 20:12:53 2017 -0500) +- 0a278af - pact-python manages the mock service for the user (Matthew Balvanz, Tue Apr 18 21:23:18 2017 -0500) +- fd68b41 - Merge pull request #2 from pact-foundation/package-ruby-apps (Matthew Balvanz, Sat Apr 22 10:55:48 2017 -0500) +- 75a96dc - Package the Ruby Mock Service and Verifier (Matthew Balvanz, Tue Apr 4 23:14:11 2017 -0500) ### v0.1.0 -* 189c647 - Merge pull request #3 from pact-foundation/travis-ci (Jose Salvatierra, Fri Apr 7 21:40:02 2017 +0100) -* 559efb8 - Add Travis CI build (Matthew Balvanz, Fri Apr 7 11:12:01 2017 -0500) -* 8f074a0 - Merge pull request #1 from pact-foundation/initial-framework (Matthew Balvanz, Fri Apr 7 09:55:34 2017 -0500) -* f5caf9c - Initial pact-python implementation (Matthew Balvanz, Mon Apr 3 09:16:59 2017 -0500) -* bfb8380 - Initial pact-python implementation (Matthew Balvanz, Thu Mar 30 20:41:05 2017 -0500) + +- 189c647 - Merge pull request #3 from pact-foundation/travis-ci (Jose Salvatierra, Fri Apr 7 21:40:02 2017 +0100) +- 559efb8 - Add Travis CI build (Matthew Balvanz, Fri Apr 7 11:12:01 2017 -0500) +- 8f074a0 - Merge pull request #1 from pact-foundation/initial-framework (Matthew Balvanz, Fri Apr 7 09:55:34 2017 -0500) +- f5caf9c - Initial pact-python implementation (Matthew Balvanz, Mon Apr 3 09:16:59 2017 -0500) +- bfb8380 - Initial pact-python implementation (Matthew Balvanz, Thu Mar 30 20:41:05 2017 -0500) diff --git a/pact/__version__.py b/pact/__version__.py index a9c04f731..9872c1a15 100644 --- a/pact/__version__.py +++ b/pact/__version__.py @@ -1,3 +1,3 @@ """Pact version info.""" -__version__ = '2.0.1' +__version__ = "2.1.0" From 9fe9694654e2611aa7e08a90bcb8dedda0da1409 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Sep 2023 16:38:56 +1000 Subject: [PATCH 0047/1376] chore: add future deprecation warnings As work starts on the next major version of Pact for Python, we need to start warning users that the current version will be deprecated in the future. This commit adds a warning to the top of all modules and standalone functions that will be deprecated in the next major version. While the class names themselves might remain, the way they are used may change to accommodate the changes in the underlying workings, and to accommodate versions 3 and 4 of the Pact specification. Once the work on the Pact Python v3 is complete, a thorough migration guide will be written. Signed-off-by: JP-Ellis --- pact/broker.py | 7 +++++++ pact/consumer.py | 7 +++++++ pact/http_proxy.py | 7 +++++++ pact/matchers.py | 37 +++++++++++++++++++++++++++++++++ pact/message_consumer.py | 7 +++++++ pact/message_pact.py | 7 +++++++ pact/message_provider.py | 7 +++++++ pact/pact.py | 7 +++++++ pact/provider.py | 9 ++++++++ pact/verifier.py | 7 +++++++ pact/verify_wrapper.py | 45 ++++++++++++++++++++++++++++++++++++++++ 11 files changed, 147 insertions(+) diff --git a/pact/broker.py b/pact/broker.py index 644453361..da25f958a 100644 --- a/pact/broker.py +++ b/pact/broker.py @@ -4,6 +4,7 @@ import fnmatch import os from subprocess import Popen +import warnings from .constants import BROKER_CLIENT_PATH @@ -36,6 +37,12 @@ def __init__(self, broker_base_url=None, broker_username=None, broker_password=N the PACT_BROKER_TOKEN environment variable instead. Defaults to None. """ + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) self.broker_base_url = broker_base_url self.broker_username = broker_username self.broker_password = broker_password diff --git a/pact/consumer.py b/pact/consumer.py index e2357a024..57281234d 100644 --- a/pact/consumer.py +++ b/pact/consumer.py @@ -1,4 +1,5 @@ """Classes and methods to describe contract Consumers.""" +import warnings from .pact import Pact from .provider import Provider @@ -47,6 +48,12 @@ def __init__(self, name, service_cls=Pact, tags=None, Defaults to False. :type auto_detect_version_properties: bool """ + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) self.name = name self.service_cls = service_cls self.tags = tags diff --git a/pact/http_proxy.py b/pact/http_proxy.py index 23c9ebda9..e97dec77a 100644 --- a/pact/http_proxy.py +++ b/pact/http_proxy.py @@ -1,4 +1,5 @@ """Http Proxy to be used as provider url in verifier.""" +import warnings from fastapi import FastAPI, status, Request, HTTPException import uvicorn as uvicorn import logging @@ -55,4 +56,10 @@ async def setup(request: Request): def run_proxy(): """Rub HTTP Proxy.""" + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) uvicorn.run("pact.http_proxy:app", port=PROXY_PORT, log_level=UVICORN_LOGGING_LEVEL) diff --git a/pact/matchers.py b/pact/matchers.py index fd929f6b6..6df81f2ff 100644 --- a/pact/matchers.py +++ b/pact/matchers.py @@ -1,4 +1,5 @@ """Classes for defining request and response data that is variable.""" +import warnings import six import datetime @@ -50,6 +51,12 @@ def __init__(self, matcher, minimum=1): Must be greater than or equal to 1. :type minimum: int """ + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) self.matcher = matcher assert minimum >= 1, 'Minimum must be greater than or equal to 1' self.minimum = minimum @@ -100,6 +107,12 @@ def __init__(self, matcher): ignored. :type matcher: None, list, dict, int, float, str, unicode, Matcher """ + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) valid_types = ( type(None), list, dict, int, float, six.string_types, Matcher) @@ -158,6 +171,12 @@ def __init__(self, matcher, generate): generating the response to the consumer. :type generate: basestring """ + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) self.matcher = matcher self._generate = generate @@ -188,6 +207,12 @@ def from_term(term): :return: The JSON representation for this term. :rtype: dict, list, str """ + warnings.warn( + "This function will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) if term is None: return term elif isinstance(term, (six.string_types, six.binary_type, int, float)): @@ -211,6 +236,12 @@ def get_generated_values(input): :return: The input resolved to its generated value(s) :rtype: None, list, dict, int, float, bool, str, unicode, Matcher """ + warnings.warn( + "This function will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) if input is None: return input if isinstance(input, (six.string_types, int, float, bool)): @@ -254,6 +285,12 @@ class Format: def __init__(self): """Create a new Formatter.""" + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) self.identifier = self.integer_or_identifier() self.integer = self.integer_or_identifier() self.decimal = self.decimal() diff --git a/pact/message_consumer.py b/pact/message_consumer.py index 97f1718b3..9230e7d51 100644 --- a/pact/message_consumer.py +++ b/pact/message_consumer.py @@ -1,4 +1,5 @@ """Classes and methods to describe contract Consumers.""" +import warnings from .message_pact import MessagePact from .provider import Provider @@ -56,6 +57,12 @@ def __init__( Defaults to False. :type auto_detect_version_properties: bool """ + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) self.name = name self.service_cls = service_cls self.tags = tags diff --git a/pact/message_pact.py b/pact/message_pact.py index 409d9de36..fba01d97c 100644 --- a/pact/message_pact.py +++ b/pact/message_pact.py @@ -4,6 +4,7 @@ import json import os from subprocess import Popen +import warnings from .broker import Broker from .constants import MESSAGE_PATH @@ -84,6 +85,12 @@ def __init__( `merge`. :type file_write_mode: str """ + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) super().__init__( broker_base_url, broker_username, broker_password, broker_token ) diff --git a/pact/message_provider.py b/pact/message_provider.py index 4774fc84a..6e49466ee 100644 --- a/pact/message_provider.py +++ b/pact/message_provider.py @@ -1,6 +1,7 @@ """Contract Message Provider.""" import os import time +import warnings import requests from requests.adapters import HTTPAdapter @@ -38,6 +39,12 @@ def __init__( proxy_port='1234' ): """Create a Message Provider instance.""" + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) self.message_providers = message_providers self.provider = provider self.consumer = consumer diff --git a/pact/pact.py b/pact/pact.py index 28126bfc6..c03e02ae2 100644 --- a/pact/pact.py +++ b/pact/pact.py @@ -4,6 +4,7 @@ import os import platform from subprocess import Popen +import warnings import psutil import requests @@ -124,6 +125,12 @@ def __init__( `overwrite`. :type file_write_mode: str """ + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) super().__init__( broker_base_url, broker_username, broker_password, broker_token ) diff --git a/pact/provider.py b/pact/provider.py index 543f0152d..4ee48cbee 100644 --- a/pact/provider.py +++ b/pact/provider.py @@ -1,6 +1,9 @@ """Classes and methods to describe contract Providers.""" +import warnings + + class Provider(object): """A Pact provider.""" @@ -12,4 +15,10 @@ def __init__(self, name): when it is published. :type name: str """ + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) self.name = name diff --git a/pact/verifier.py b/pact/verifier.py index 4388e1095..6ad8bb534 100644 --- a/pact/verifier.py +++ b/pact/verifier.py @@ -1,5 +1,6 @@ """Classes and methods to verify Contracts.""" import json +import warnings from pact.verify_wrapper import VerifyWrapper, path_exists, expand_directories @@ -15,6 +16,12 @@ def __init__(self, provider, provider_base_url, **kwargs): provider_base_url ([String]): provider url """ + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) self.provider = provider self.provider_base_url = provider_base_url diff --git a/pact/verify_wrapper.py b/pact/verify_wrapper.py index 0789b6aef..71da1d02e 100644 --- a/pact/verify_wrapper.py +++ b/pact/verify_wrapper.py @@ -1,5 +1,6 @@ """Wrapper to verify previously created pacts.""" +import warnings from pact.constants import VERIFIER_PATH import sys import os @@ -12,6 +13,12 @@ def capture_logs(process, verbose): """Capture logs from ruby process.""" + warnings.warn( + "This function will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) result = '' for line in process.stdout: result = result + line + '\n' @@ -31,6 +38,12 @@ def path_exists(path): :return: True if the path exists and is a file, otherwise False. :rtype: bool """ + warnings.warn( + "This function will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) if path.startswith('http://') or path.startswith('https://'): return True @@ -46,6 +59,12 @@ def sanitize_logs(process, verbose): :type verbose: bool :rtype: None """ + warnings.warn( + "This function will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) for line in process.stdout: if (not verbose and line.lstrip().startswith('#') and ('vendor/ruby' in line or 'pact-provider-verifier.rb' in line)): @@ -63,6 +82,12 @@ def expand_directories(paths): JSON files in those directories. :rtype: list """ + warnings.warn( + "This function will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) paths_ = [] for path in paths: if path.startswith('http://') or path.startswith('https://'): @@ -83,6 +108,12 @@ def rerun_command(): :rtype: str """ + warnings.warn( + "This function will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) is_windows = 'windows' in platform.platform().lower() command = '' if is_windows: @@ -119,12 +150,26 @@ class PactException(Exception): def __init__(self, *args, **kwargs): """Create wrapper.""" + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) super().__init__(*args, **kwargs) self.message = args[0] class VerifyWrapper(object): """A Pact Verifier Wrapper.""" + def __init__(self): + warnings.warn( + "This class will be deprecated Pact Python v3 " + "(see pact-foundation/pact-python#396)", + PendingDeprecationWarning, + stacklevel=2, + ) + def _broker_present(self, **kwargs): if kwargs.get('broker_url') is None: return False From fbd1fbe5c4a814b9dac2c4355d57221160181e59 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 4 Oct 2023 11:47:53 +1100 Subject: [PATCH 0048/1376] fix(ci): add missing environment When migrating the CI publish script, I unfortunately missed the environment which contains the PyPI credentials. Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6a0e9213..ba252ffdd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -155,6 +155,7 @@ jobs: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') needs: [check] runs-on: ubuntu-latest + environment: Upload Python Package steps: - uses: actions/download-artifact@v3 From dab6dbac1c4e697a757a6af2b77c7c642fd58b9c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 11 Oct 2023 12:26:22 +1100 Subject: [PATCH 0049/1376] style: fix pre-commit lints This commit fixes pre-commit linting and formatting issues. To avoid refactoring all of the existing Python code (which will is going to be deprecated), the pre-commit configuration has been updated to ignore these Python files. All other changes in this commit are purely stylistic. Signed-off-by: JP-Ellis --- .github/semantic.yml | 2 - .../workflows/trigger_pact_docs_update.yml | 2 +- .pre-commit-config.yaml | 6 + Dockerfile.ubuntu | 2 +- README.md | 245 +++++++----------- RELEASING.md | 70 +++-- docker/README.md | 24 +- examples/README.md | 77 +----- run-docker.sh | 2 +- script/commit_message.py | 21 +- script/release_prep.sh | 2 - 11 files changed, 173 insertions(+), 280 deletions(-) diff --git a/.github/semantic.yml b/.github/semantic.yml index af6bf3c91..918f44ded 100644 --- a/.github/semantic.yml +++ b/.github/semantic.yml @@ -1,4 +1,2 @@ titleAndCommits: true allowMergeCommits: true - - diff --git a/.github/workflows/trigger_pact_docs_update.yml b/.github/workflows/trigger_pact_docs_update.yml index 109521d95..634d7a804 100644 --- a/.github/workflows/trigger_pact_docs_update.yml +++ b/.github/workflows/trigger_pact_docs_update.yml @@ -5,7 +5,7 @@ on: branches: - master paths: - - '**.md' + - "**.md" jobs: run: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68f0845b0..88eb34759 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,6 +41,9 @@ repos: rev: v0.0.289 hooks: - id: ruff + # Exclude python files in pact/** and tests/**, except for the + # files in pact/v3/** and tests/v3/**. + exclude: ^(pact|tests)/(?!v3/).*\.py$ args: [--fix, --exit-non-zero-on-fix] stages: [pre-push] @@ -48,6 +51,9 @@ repos: rev: 23.9.1 hooks: - id: black + # Exclude python files in pact/** and tests/**, except for the + # files in pact/v3/** and tests/v3/**. + exclude: ^(pact|tests)/(?!v3/).*\.py$ stages: [pre-push] - repo: https://github.com/commitizen-tools/commitizen diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 74efa1c69..c841bc1cc 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -3,7 +3,7 @@ ENV DEBIAN_FRONTEND=noninteractive ARG PYTHON_VERSION 3.9 #Set of all dependencies needed for pyenv to work on Ubuntu -RUN apt-get update \ +RUN apt-get update \ && apt-get install -y --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget ca-certificates curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev mecab-ipadic-utf8 git # Set-up necessary Env vars for PyEnv diff --git a/README.md b/README.md index 230d6fd9d..6f046fe56 100644 --- a/README.md +++ b/README.md @@ -4,25 +4,24 @@ [![License](https://img.shields.io/github/license/pact-foundation/pact-python.svg?maxAge=2592000)](https://github.com/pact-foundation/pact-python/blob/master/LICENSE) [![Build and Test](https://github.com/pact-foundation/pact-python/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/pact-foundation/pact-python/actions/workflows/build_and_test.yml) -Python version of Pact. Enables consumer driven contract testing, -providing a mock service and DSL for the consumer project, and -interaction playback and verification for the service provider project. -Currently supports version 2 of the [Pact specification]. +Python version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project. Currently supports version 2 of the [Pact specification]. -For more information about what Pact is, and how it can help you -test your code more efficiently, check out the [Pact documentation]. +For more information about what Pact is, and how it can help you test your code more efficiently, check out the [Pact documentation]. Note: As of Version 1.0 deprecates support for python 2.7 to allow us to incorporate python 3.x features more readily. If you want to still use Python 2.7 use the 0.x.y versions. Only bug fixes will now be added to that release. # How to use pact-python ## Installation + ``` pip install pact-python ``` ## Getting started + + A guide follows but if you go to the [examples](https://github.com/pact-foundation/pact-python/tree/master/examples). This has a consumer, provider and pact-broker set of tests for both FastAPI and Flask. ## Writing a Pact @@ -34,16 +33,13 @@ Creating a complete contract is a two step process: ## Writing the Consumer Test -If we have a method that communicates with one of our external services, which we'll call -`Provider`, and our product, `Consumer` is hitting an endpoint on `Provider` at -`/users/` to get information about a particular user. +If we have a method that communicates with one of our external services, which we'll call `Provider`, and our product, `Consumer` is hitting an endpoint on `Provider` at `/users/` to get information about a particular user. If the code to fetch a user looked like this: ```python import requests - def user(user_name): """Fetch a user object by user_name from the server.""" uri = 'http://localhost:1234/users/' + user_name @@ -87,16 +83,12 @@ class GetUserInfoContract(unittest.TestCase): This does a few important things: - - Defines the Consumer and Provider objects that describe our product and our service under test - - Uses `given` to define the setup criteria for the Provider `UserA exists and is not an administrator` - - Defines what the request that is expected to be made by the consumer will contain - - Defines how the server is expected to respond +- Defines the Consumer and Provider objects that describe our product and our service under test +- Uses `given` to define the setup criteria for the Provider `UserA exists and is not an administrator` +- Defines what the request that is expected to be made by the consumer will contain +- Defines how the server is expected to respond -Using the Pact object as a [context manager], we call our method under test -which will then communicate with the Pact mock service. The mock service will respond with -the items we defined, allowing us to assert that the method processed the response and -returned the expected value. If you want more control over when the mock service is -configured and the interactions verified, use the `setup` and `verify` methods, respectively: +Using the Pact object as a [context manager], we call our method under test which will then communicate with the Pact mock service. The mock service will respond with the items we defined, allowing us to assert that the method processed the response and returned the expected value. If you want more control over when the mock service is configured and the interactions verified, use the `setup` and `verify` methods, respectively: ```python (pact @@ -114,8 +106,7 @@ configured and the interactions verified, use the `setup` and `verify` methods, ### Requests -When defining the expected HTTP request that your code is expected to make you -can specify the method, path, body, headers, and query: +When defining the expected HTTP request that your code is expected to make you can specify the method, path, body, headers, and query: ```python pact.with_request( @@ -125,8 +116,7 @@ pact.with_request( ) ``` -`query` is used to specify URL query parameters, so the above example expects -a request made to `/api/v1/my-resources/?search=example`. +`query` is used to specify URL query parameters, so the above example expects a request made to `/api/v1/my-resources/?search=example`. ```python pact.with_request( @@ -137,12 +127,9 @@ pact.with_request( ) ``` -You can define exact values for your expected request like the examples above, -or you can use the matchers defined later to assist in handling values that are -variable. +You can define exact values for your expected request like the examples above, or you can use the matchers defined later to assist in handling values that are variable. -The default hostname and port for the Pact mock service will be -`localhost:1234` but you can adjust this during Pact creation: +The default hostname and port for the Pact mock service will be `localhost:1234` but you can adjust this during Pact creation: ```python from pact import Consumer, Provider @@ -150,28 +137,22 @@ pact = Consumer('Consumer').has_pact_with( Provider('Provider'), host_name='mockservice', port=8080) ``` -This can be useful if you need to run to create more than one Pact for your test -because your code interacts with two different services. It is important to note -that the code you are testing with this contract _must_ contact the mock service. -So in this example, the `user` method could accept an argument to specify the -location of the server, or retrieve it from an environment variable so you can -change its URI during the test. +This can be useful if you need to run to create more than one Pact for your test because your code interacts with two different services. It is important to note that the code you are testing with this contract _must_ contact the mock service. So in this example, the `user` method could accept an argument to specify the location of the server, or retrieve it from an environment variable so you can change its URI during the test. The mock service offers you several important features when building your contracts: -- It provides a real HTTP server that your code can contact during the test and provides the responses you defined. -- You provide it with the expectations for the request your code will make and it will assert the contents of the actual requests made based on your expectations. -- If a request is made that does not match one you defined or if a request from your code is missing it will return an error with details. -- Finally, it will record your contracts as a JSON file that you can store in your repository or publish to a Pact broker. + +- It provides a real HTTP server that your code can contact during the test and provides the responses you defined. +- You provide it with the expectations for the request your code will make and it will assert the contents of the actual requests made based on your expectations. +- If a request is made that does not match one you defined or if a request from your code is missing it will return an error with details. +- Finally, it will record your contracts as a JSON file that you can store in your repository or publish to a Pact broker. ## Expecting Variable Content -The above test works great if that user information is always static, but what happens if -the user has a last updated field that is set to the current time every time the object is -modified? To handle variable data and make your tests more robust, there are 3 helpful matchers: + +The above test works great if that user information is always static, but what happens if the user has a last updated field that is set to the current time every time the object is modified? To handle variable data and make your tests more robust, there are 3 helpful matchers: ### Term(matcher, generate) -Asserts the value should match the given regular expression. You could use this -to expect a timestamp with a particular format in the request or response where -you know you need a particular format, but are unconcerned about the exact date: + +Asserts the value should match the given regular expression. You could use this to expect a timestamp with a particular format in the request or response where you know you need a particular format, but are unconcerned about the exact date: ```python from pact import Term @@ -188,12 +169,10 @@ body = { .will_respond_with(200, body=body)) ``` -When you run the tests for the consumer, the mock service will return the value you provided -as `generate`, in this case `2016-12-15T20:16:01`. When the contract is verified on the -provider, the regex will be used to search the response from the real provider service -and the test will be considered successful if the regex finds a match in the response. +When you run the tests for the consumer, the mock service will return the value you provided as `generate`, in this case `2016-12-15T20:16:01`. When the contract is verified on the provider, the regex will be used to search the response from the real provider service and the test will be considered successful if the regex finds a match in the response. ### Like(matcher) + Asserts the element's type matches the matcher. For example: ```python @@ -202,6 +181,7 @@ Like(123) # Matches if the value is an integer Like('hello world') # Matches if the value is a string Like(3.14) # Matches if the value is a float ``` + The argument supplied to `Like` will be what the mock service responds with. When a dictionary is used as an argument for Like, all the child objects (and their child objects etc.) will be matched according to their types, unless you use a more specific matcher like a Term. @@ -220,8 +200,8 @@ Like({ ``` ### EachLike(matcher, minimum=1) -Asserts the value is an array type that consists of elements -like the one passed in. It can be used to assert simple arrays: + +Asserts the value is an array type that consists of elements like the one passed in. It can be used to assert simple arrays: ```python from pact import EachLike @@ -240,11 +220,9 @@ EachLike({ }) ``` -> Note, you do not need to specify everything that will be returned from the Provider in a -> JSON response, any extra data that is received will be ignored and the tests will still pass. +> Note, you do not need to specify everything that will be returned from the Provider in a JSON response, any extra data that is received will be ignored and the tests will still pass. -> Note, to get the generated values from an object that can contain matchers like Term, Like, EachLike, etc. -> for assertion in self.assertEqual(result, expected) you may need to use get_generated_values() helper function: +> Note, to get the generated values from an object that can contain matchers like Term, Like, EachLike, etc. for assertion in self.assertEqual(result, expected) you may need to use get_generated_values() helper function: ```python from pact.matchers import get_generated_values @@ -252,6 +230,7 @@ self.assertEqual(result, get_generated_values(expected)) ``` ### Match common formats + Often times, you find yourself having to re-write regular expressions for common formats. ```python @@ -262,20 +241,20 @@ Format().ip_address # Matches if the value is an ip address We've created a number of them for you to save you the time: -| matcher | description | -|-------------------|-------------------------------------------------------------------------------------------------------------------------| -| `identifier` | Match an ID (e.g. 42) | -| `integer` | Match all numbers that are integers (both ints and longs) | -| `decimal` | Match all real numbers (floating point and decimal) | -| `hexadecimal` | Match all hexadecimal encoded strings | -| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) | -| `timestamp` | Match a string containing an RFC3339 formatted timestamp (e.g. Mon, 31 Oct 2016 15:21:41 -0400) | -| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) | -| `iso_datetime` | Match string containing ISO 8601 formatted dates (e.g. 2015-08-06T16:53:10+01:00) | -| `iso_datetime_ms` | Match string containing ISO 8601 formatted dates, enforcing millisecond precision (e.g. 2015-08-06T16:53:10.123+01:00) | -| `ip_address` | Match string containing IP4 formatted address | -| `ipv6_address` | Match string containing IP6 formatted address | -| `uuid` | Match strings containing UUIDs | +| matcher | description | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `identifier` | Match an ID (e.g. 42) | +| `integer` | Match all numbers that are integers (both ints and longs) | +| `decimal` | Match all real numbers (floating point and decimal) | +| `hexadecimal` | Match all hexadecimal encoded strings | +| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) | +| `timestamp` | Match a string containing an RFC3339 formatted timestamp (e.g. Mon, 31 Oct 2016 15:21:41 -0400) | +| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) | +| `iso_datetime` | Match string containing ISO 8601 formatted dates (e.g. 2015-08-06T16:53:10+01:00) | +| `iso_datetime_ms` | Match string containing ISO 8601 formatted dates, enforcing millisecond precision (e.g. 2015-08-06T16:53:10.123+01:00) | +| `ip_address` | Match string containing IP4 formatted address | +| `ipv6_address` | Match string containing IP6 formatted address | +| `uuid` | Match strings containing UUIDs | These can be used to replace other matchers @@ -329,42 +308,36 @@ output, logs = verifier.verify_pacts('./userserviceclient-userservice.json') ``` The parameters for this differ slightly in naming from their CLI equivalents: -| CLI | native Python | +| CLI | native Python | |-----------------------------------|-----------------------------------| -| `--branch` | `branch` | -| `--build-url` | `build_url` | -| `--auto-detect-version-properties`| `auto_detect_version_properties` | -| `--tag=TAG` | `consumer_tags` | -| `--tag-with-git-branch` | `tag_with_git_branch` | -| `PACT_DIRS_OR_FILES` | `pact_dir` | -| `--consumer-app-version` | `version` | -| `n/a` | `consumer_name` | +| `--branch` | `branch` | +| `--build-url` | `build_url` | +| `--auto-detect-version-properties`| `auto_detect_version_properties` | +| `--tag=TAG` | `consumer_tags` | +| `--tag-with-git-branch` | `tag_with_git_branch` | +| `PACT_DIRS_OR_FILES` | `pact_dir` | +| `--consumer-app-version` | `version` | +| `n/a` | `consumer_name` | ## Verifying Pacts Against a Service -In addition to writing Pacts for Python consumers, you can also verify those Pacts -against a provider of any language. There are two ways to do this. +In addition to writing Pacts for Python consumers, you can also verify those Pacts against a provider of any language. There are two ways to do this. ### CLI -After installing pact-python a `pact-verifier` -application should be available. To get details about its use you can call it with the -help argument: +After installing pact-python a `pact-verifier` application should be available. To get details about its use you can call it with the help argument: ```bash pact-verifier --help ``` -The simplest example is verifying a server with locally stored Pact files and no provider -states: +The simplest example is verifying a server with locally stored Pact files and no provider states: ```bash pact-verifier --provider-base-url=http://localhost:8080 --pact-url=./pacts/consumer-provider.json ``` -Which will immediately invoke the Pact verifier, making HTTP requests to the server located -at `http://localhost:8080` based on the Pacts in `./pacts/consumer-provider.json` and -reporting the results. +Which will immediately invoke the Pact verifier, making HTTP requests to the server located at `http://localhost:8080` based on the Pacts in `./pacts/consumer-provider.json` and reporting the results. There are several options for configuring how the Pacts are verified: @@ -374,9 +347,7 @@ Required. Defines the URL of the server to make requests to when verifying the P ###### --pact-url -Required if --pact-urls not specified. The location of a Pact file you want -to verify. This can be a URL to a [Pact Broker] or a local path, to provide -multiple files, specify multiple arguments. +Required if --pact-urls not specified. The location of a Pact file you want to verify. This can be a URL to a [Pact Broker] or a local path, to provide multiple files, specify multiple arguments. ``` pact-verifier --provider-base-url=http://localhost:8080 --pact-url=./pacts/one.json --pact-url=./pacts/two.json @@ -384,14 +355,11 @@ pact-verifier --provider-base-url=http://localhost:8080 --pact-url=./pacts/one.j ###### --pact-urls -Required if --pact-url not specified. The location of the Pact files you want -to verify. This can be a URL to a [Pact Broker] or one or more local paths, separated by a comma. +Required if --pact-url not specified. The location of the Pact files you want to verify. This can be a URL to a [Pact Broker] or one or more local paths, separated by a comma. ###### --provider-states-url -_DEPRECATED AFTER v 0.6.0._ The URL where your provider application will produce the list of available provider states. -The verifier calls this URL to ensure the Pacts specify valid states before making the HTTP -requests. +_DEPRECATED AFTER v 0.6.0._ The URL where your provider application will produce the list of available provider states. The verifier calls this URL to ensure the Pacts specify valid states before making the HTTP requests. ###### --provider-states-setup-url @@ -399,33 +367,27 @@ The URL which should be called to setup a specific provider state before a Pact ###### --pact-broker-url -Base URl for the Pact Broker instance to publish pacts to. Can also be specified via the environment variable -`PACT_BROKER_BASE_URL`. +Base URl for the Pact Broker instance to publish pacts to. Can also be specified via the environment variable `PACT_BROKER_BASE_URL`. ###### --pact-broker-username -The username to use when contacting the Pact Broker. Can also be specified via the environment variable -`PACT_BROKER_USERNAME`. +The username to use when contacting the Pact Broker. Can also be specified via the environment variable `PACT_BROKER_USERNAME`. ###### --pact-broker-password -The password to use when contacting the Pact Broker. You can also specify this value -as the environment variable `PACT_BROKER_PASSWORD`. +The password to use when contacting the Pact Broker. You can also specify this value as the environment variable `PACT_BROKER_PASSWORD`. ###### --pact-broker-token -The bearer token to use when contacting the Pact Broker. You can also specify this value -as the environment variable `PACT_BROKER_TOKEN`. +The bearer token to use when contacting the Pact Broker. You can also specify this value as the environment variable `PACT_BROKER_TOKEN`. ###### --consumer-version-tag -Retrieve the latest pacts with this consumer version tag. Used in conjunction with `--provider`. -May be specified multiple times. +Retrieve the latest pacts with this consumer version tag. Used in conjunction with `--provider`. May be specified multiple times. ###### --consumer-version-selector -You can also retrieve pacts with consumer version selector, a more flexible approach in specifying which pacts you need. -May be specified multiple times. Read more about selectors [here](https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/). +You can also retrieve pacts with consumer version selector, a more flexible approach in specifying which pacts you need. May be specified multiple times. Read more about selectors [here](https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/). ###### --provider-version-tag @@ -453,6 +415,7 @@ The provider application version. Required for publishing verification results. Publish verification results to the broker. ### Python API + You can use the Verifier class. This allows you to write native python code and the test framework of your choice. ```python @@ -481,51 +444,46 @@ success, logs = verifier.verify_with_broker( enable_pending=True, ) assert success == 0 - ``` The parameters for this differ slightly in naming from their CLI equivalents: -| CLI | native Python | -|-----------------------------------|------------------------------- | -| `--log-dir` | `log_dir` | -| `--log-level` | `log_level` | -| `--provider-app-version` | `provider_app_version` | -| `--headers` | `custom_provider_headers` | -| `--consumer-version-tag` | `consumer_tags` | -| `--provider-version-tag` | `provider_tags` | -| `--provider-states-setup-url` | `provider_states_setup_url` | -| `--verbose` | `verbose` | -| `--consumer-version-selector` | `consumer_selectors` | -| `--publish-verification-results` | `publish_verification_results` | -| `--provider-version-branch` | `provider_version_branch` | +| CLI | native Python | +| -------------------------------- | ------------------------------ | +| `--log-dir` | `log_dir` | +| `--log-level` | `log_level` | +| `--provider-app-version` | `provider_app_version` | +| `--headers` | `custom_provider_headers` | +| `--consumer-version-tag` | `consumer_tags` | +| `--provider-version-tag` | `provider_tags` | +| `--provider-states-setup-url` | `provider_states_setup_url` | +| `--verbose` | `verbose` | +| `--consumer-version-selector` | `consumer_selectors` | +| `--publish-verification-results` | `publish_verification_results` | +| `--provider-version-branch` | `provider_version_branch` | You can see more details in the examples -- [Message Provider Verifier Test](https://github.com/pact-foundation/pact-python/tree/master/examples/message/tests/provider/test_message_provider.py) -- [Flask Provider Verifier Test](https://github.com/pact-foundation/pact-python/tree/master/examples/flask_provider/tests/provider/test_provider.py) -- [FastAPI Provider Verifier Test](https://github.com/pact-foundation/pact-python/tree/master/examples/fastapi_provider/tests/provider/test_provider.py) +- [Message Provider Verifier Test](https://github.com/pact-foundation/pact-python/tree/master/examples/message/tests/provider/test_message_provider.py) +- [Flask Provider Verifier Test](https://github.com/pact-foundation/pact-python/tree/master/examples/flask_provider/tests/provider/test_provider.py) +- [FastAPI Provider Verifier Test](https://github.com/pact-foundation/pact-python/tree/master/examples/fastapi_provider/tests/provider/test_provider.py) ### Provider States -In many cases, your contracts will need very specific data to exist on the provider -to pass successfully. If you are fetching a user profile, that user needs to exist, -if querying a list of records, one or more records needs to exist. To support -decoupling the testing of the consumer and provider, Pact offers the idea of provider -states to communicate from the consumer what data should exist on the provider. -When setting up the testing of a provider you will also need to setup the management of -these provider states. The Pact verifier does this by making additional HTTP requests to -the `--provider-states-setup-url` you provide. This URL could be -on the provider application or a separate one. Some strategies for managing state include: +In many cases, your contracts will need very specific data to exist on the provider to pass successfully. If you are fetching a user profile, that user needs to exist, if querying a list of records, one or more records needs to exist. To support decoupling the testing of the consumer and provider, Pact offers the idea of provider states to communicate from the consumer what data should exist on the provider. + +When setting up the testing of a provider you will also need to setup the management of these provider states. The Pact verifier does this by making additional HTTP requests to the `--provider-states-setup-url` you provide. This URL could be on the provider application or a separate one. Some strategies for managing state include: -- Having endpoints in your application that are not active in production that create and delete your datastore state -- A separate application that has access to the same datastore to create and delete, like a separate App Engine module or Docker container pointing to the same datastore -- A standalone application that can start and stop the other server with different datastore states +- Having endpoints in your application that are not active in production that create and delete your datastore state +- A separate application that has access to the same datastore to create and delete, like a separate App Engine module or Docker container pointing to the same datastore +- A standalone application that can start and stop the other server with different datastore states For more information about provider states, refer to the [Pact documentation] on [Provider States]. # Development + + Please read [CONTRIBUTING.md](https://github.com/pact-foundation/pact-python/blob/master/CONTRIBUTING.md) To setup a development environment: @@ -533,21 +491,18 @@ To setup a development environment: 1. If you want to run tests for all Python versions, install 2.7, 3.3, 3.4, 3.5, and 3.6 from source or using a tool like [pyenv] 2. Its recommended to create a Python [virtualenv] for the project -To setup the environment, run tests, and package the application, run: -`make release` +To setup the environment, run tests, and package the application, run: `make release` -If you are just interested in packaging pact-python so you can install it using pip: +If you are just interested in packaging pact-python so you can install it using pip: `make package` -`make package` - -This creates a `dist/pact-python-N.N.N.tar.gz` file, where the Ns are the current version. -From there you can use pip to install it: +This creates a `dist/pact-python-N.N.N.tar.gz` file, where the Ns are the current version. From there you can use pip to install it: `pip install ./dist/pact-python-N.N.N.tar.gz` ## Offline Installation of Standalone Packages Although all Ruby standalone applications are predownloaded into the wheel artifact, it may be useful, for development, purposes to install custom Ruby binaries. In which case, use the `bin-path` flag. + ``` pip install pact-python --bin-path=/absolute/path/to/folder/containing/pact/binaries/for/your/os ``` @@ -568,8 +523,8 @@ Join us in slack: [![slack](https://slack.pact.io/badge.svg)](https://slack.pact or -- Twitter: [@pact_up](https://twitter.com/pact_up) -- Stack Overflow: [stackoverflow.com/questions/tagged/pact](https://stackoverflow.com/questions/tagged/pact) +- Twitter: [@pact_up](https://twitter.com/pact_up) +- Stack Overflow: [stackoverflow.com/questions/tagged/pact](https://stackoverflow.com/questions/tagged/pact) [bundler]: http://bundler.io/ [context manager]: https://en.wikibooks.org/wiki/Python_Programming/Context_Managers diff --git a/RELEASING.md b/RELEASING.md index f51cb722e..43b72f594 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,59 +2,53 @@ ## Preparing the release -The easiest way is to just run the following command from the root folder with -the HEAD commit on trunk and the appropriate version. We follow -`..` versioning. +The easiest way is to just run the following command from the root folder with the HEAD commit on trunk and the appropriate version. We follow `..` versioning. - ```shell - $ script/release_prep.sh X.Y.Z - ``` +```shell +$ script/release_prep.sh X.Y.Z +``` This script effectively runs the following: -1. Increment the version according to semantic versioning rules in `pact/__version__.py` +1. Increment the version according to semantic versioning rules in `pact/__version__.py` -2. Update the `CHANGELOG.md` using: - ```shell - $ git log --pretty=format:' * %h - %s (%an, %ad)' vX.Y.Z..HEAD - ``` +2. Update the `CHANGELOG.md` using: -3. Add files to git - ```shell - $ git add CHANGELOG.md pact/__version__.py - ``` + ```shell + $ git log --pretty=format:' * %h - %s (%an, %ad)' vX.Y.Z..HEAD + ``` -4. Commit - ```shell - $ git commit -m "Releasing version X.Y.Z" - ``` +3. Add files to git -5. Tag - ```shell - $ git tag -a vX.Y.Z -m "Releasing version X.Y.Z" - $ git push origin master --tags - ``` + ```shell + $ git add CHANGELOG.md pact/__version__.py + ``` + +4. Commit + + ```shell + $ git commit -m "Releasing version X.Y.Z" + ``` + +5. Tag + + ```shell + $ git tag -a vX.Y.Z -m "Releasing version X.Y.Z" + $ git push origin master --tags + ``` ## Updating Pact Ruby -To upgrade the versions of `pact-mock_service` and `pact-provider-verifier`, change the -`PACT_STANDALONE_VERSION` in `setup.py` to match the latest version available from the -[pact-ruby-standalone](https://github.com/pact-foundation/pact-ruby-standalone/releases) -repository. Do this before preparing the release. +To upgrade the versions of `pact-mock_service` and `pact-provider-verifier`, change the `PACT_STANDALONE_VERSION` in `setup.py` to match the latest version available from the [pact-ruby-standalone](https://github.com/pact-foundation/pact-ruby-standalone/releases) repository. Do this before preparing the release. ## Publishing to pypi -1. Wait until GitHub Actions have run and the new tag is available at - https://github.com/pact-foundation/pact-python/releases/tag/vX.Y.Z +1. Wait until GitHub Actions have run and the new tag is available at https://github.com/pact-foundation/pact-python/releases/tag/vX.Y.Z -2. Set the title to `pact-python-X.Y.Z` +2. Set the title to `pact-python-X.Y.Z` -3. Save +3. Save -4. Go to GitHub Actions for Pact Python and you should see an 'Upload Python - Package' action blocked for your version. +4. Go to GitHub Actions for Pact Python and you should see an 'Upload Python Package' action blocked for your version. -5. Click this and then 'Review deployments'. Select 'Upload Python Package' - and Approve deploy. If you can't do this you may need an administrator to - give you permissions or do it for you. You should see in Slack #pact-python - that the release has happened. Verify in [pypi](https://pypi.org/project/pact-python/) +5. Click this and then 'Review deployments'. Select 'Upload Python Package' and Approve deploy. If you can't do this you may need an administrator to give you permissions or do it for you. You should see in Slack #pact-python that the release has happened. Verify in [pypi](https://pypi.org/project/pact-python/) diff --git a/docker/README.md b/docker/README.md index 40c8ca230..50ddfe74c 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,27 +1,20 @@ # Introduction -This is for contributors who want to make changes and test for all different -versions of python currently supported. If you don't want to set up and install -all the different python versions locally (and there are some difficulties with -that) you can just run them in docker using containers. +This is for contributors who want to make changes and test for all different versions of python currently supported. If you don't want to set up and install all the different python versions locally (and there are some difficulties with that) you can just run them in docker using containers. # Setup -To build a container say for Python 3.11, change to the root directory of the -project and run: +To build a container say for Python 3.11, change to the root directory of the project and run: ```bash (export PY=3.11 && docker build --build-arg PY="$PY" --build-arg TOXPY="$(sed 's/\.//' <<< "$PY")" -t pactfoundation:python${PY} -f docker/Dockerfile .) ``` -This uses an Alpine based image (currently 3.17), which is available as of -2023-04 for Python versions 3.7 - 3.11. +This uses an Alpine based image (currently 3.17), which is available as of 2023-04 for Python versions 3.7 - 3.11. -Note: To run tox, the Python version without the '.' is required, i.e. '311' -instead of '3.11', so some manipulation with `sed` is used to remove the '.' +Note: To run tox, the Python version without the '.' is required, i.e. '311' instead of '3.11', so some manipulation with `sed` is used to remove the '.' -To build for Python versions which require a different Alpine image, such as if -trying to build against Python 3.6, an extra `ALPINE` arg can be provided: +To build for Python versions which require a different Alpine image, such as if trying to build against Python 3.6, an extra `ALPINE` arg can be provided: ```bash (export PY=3.6 && docker build --build-arg PY="$PY" --build-arg TOXPY="$(sed 's/\.//' <<< "$PY")" --build-arg ALPINE=3.15 -t pactfoundation:python${PY} -f docker/Dockerfile .) @@ -39,16 +32,13 @@ If you need to debug you can change the command to: docker run -it --rm -v "$(pwd)":/home pactfoundation:python3.11 sh ``` -This will open a container with a prompt. From the `/home` location in the -container you can run the same tests manually: +This will open a container with a prompt. From the `/home` location in the container you can run the same tests manually: ```bash tox -e py311-{test,install} ``` -In all the above if you need to run a different version change -`py311`/`python3.11` where appropriate. Or you can run the convenience script -to build: +In all the above if you need to run a different version change `py311`/`python3.11` where appropriate. Or you can run the convenience script to build: ```bash docker/build.sh 3.11 diff --git a/examples/README.md b/examples/README.md index 4520e8fb7..4a91ca5f5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,30 +1,20 @@ # Examples -This directory contains an end-to-end example of using Pact in Python. While -this document and the documentation within the examples themselves are intended -to be mostly self-contained, it is highly recommended that you read the [Pact -Documentation](https://docs.pact.io/) as well. +This directory contains an end-to-end example of using Pact in Python. While this document and the documentation within the examples themselves are intended to be mostly self-contained, it is highly recommended that you read the [Pact Documentation](https://docs.pact.io/) as well. -Assuming you have [hatch](https://hatch.pypa.io/latest/) installed, the example -suite can be executed with: +Assuming you have [hatch](https://hatch.pypa.io/latest/) installed, the example suite can be executed with: ```sh hatch run example ``` -The code within the examples is intended to be well documented and you are -encouraged to look through the code as well (or submit a PR if anything is -unclear!). +The code within the examples is intended to be well documented and you are encouraged to look through the code as well (or submit a PR if anything is unclear!). ## Overview -Pact is a contract testing tool. Contract testing is a way to ensure that -services (such as an API provider and a client) can communicate with each other. -This example focuses on HTTP interactions, but Pact can be used to test more -general interactions as well such as through message queues. +Pact is a contract testing tool. Contract testing is a way to ensure that services (such as an API provider and a client) can communicate with each other. This example focuses on HTTP interactions, but Pact can be used to test more general interactions as well such as through message queues. -An interaction between a HTTP client (the _consumer_) and a server (the -_provider_) would typically look like this: +An interaction between a HTTP client (the _consumer_) and a server (the _provider_) would typically look like this:
@@ -40,11 +30,7 @@ sequenceDiagram
-To test this interaction naively would require both the consumer and provider to -be running at the same time. While this is straightforward in the above example, -this quickly becomes impractical as the number of interactions grows between -many microservices. Pact solves this by allowing the consumer and provider to be -tested independently. +To test this interaction naively would require both the consumer and provider to be running at the same time. While this is straightforward in the above example, this quickly becomes impractical as the number of interactions grows between many microservices. Pact solves this by allowing the consumer and provider to be tested independently. Pact achieves this be mocking the other side of the interaction: @@ -75,34 +61,20 @@ sequenceDiagram -In the first stage, the consumer defines a number of interactions in the form -below. Pact sets up a mock server that will respond to the requests as defined -by the consumer. All these interactions, containing both the request and -expected response, are all sent to the Pact Broker. +In the first stage, the consumer defines a number of interactions in the form below. Pact sets up a mock server that will respond to the requests as defined by the consumer. All these interactions, containing both the request and expected response, are all sent to the Pact Broker. > Given {provider state} \ > Upon receiving {description} \ > With {request} \ > Will respond with {response} -In the second stage, the provider retrieves the interactions from the Pact -Broker. It then sets up a mock client that will make the requests as defined by -the consumer. Pact then verifies that the responses from the provider match the -expected responses defined by the consumer. +In the second stage, the provider retrieves the interactions from the Pact Broker. It then sets up a mock client that will make the requests as defined by the consumer. Pact then verifies that the responses from the provider match the expected responses defined by the consumer. -In this way, Pact is consumer driven and can ensure that the provider is -compatible with the consumer. While this example showcases both sides in Python, -this is absolutely not required. The provider could be written in any language, -and satisfy contracts from a number of consumers all written in different -languages. +In this way, Pact is consumer driven and can ensure that the provider is compatible with the consumer. While this example showcases both sides in Python, this is absolutely not required. The provider could be written in any language, and satisfy contracts from a number of consumers all written in different languages. ### Consumer -The consumer in this example is a simple Python script that makes a HTTP GET -request to a server. It is defined in [`src/consumer.py`](src/consumer.py). The -tests for the consumer are defined in -[`tests/test_00_consumer.py`](tests/test_00_consumer.py). Each interaction is -defined using the format mentioned above. Programmatically, this looks like: +The consumer in this example is a simple Python script that makes a HTTP GET request to a server. It is defined in [`src/consumer.py`](src/consumer.py). The tests for the consumer are defined in [`tests/test_00_consumer.py`](tests/test_00_consumer.py). Each interaction is defined using the format mentioned above. Programmatically, this looks like: ```py expected: dict[str, Any] = { @@ -121,17 +93,9 @@ expected: dict[str, Any] = { ### Provider -This example showcases to different providers, one written in Flask and one -written in FastAPI. Both are simple Python web servers that respond to a HTTP -GET request. The Flask provider is defined in [`src/flask.py`](src/flask.py) and -the FastAPI provider is defined in [`src/fastapi.py`](src/fastapi.py). The -tests for the providers are defined in -[`tests/test_01_provider_flask.py`](tests/test_01_provider_flask.py) and -[`tests/test_01_provider_fastapi.py`](tests/test_01_provider_fastapi.py). +This example showcases to different providers, one written in Flask and one written in FastAPI. Both are simple Python web servers that respond to a HTTP GET request. The Flask provider is defined in [`src/flask.py`](src/flask.py) and the FastAPI provider is defined in [`src/fastapi.py`](src/fastapi.py). The tests for the providers are defined in [`tests/test_01_provider_flask.py`](tests/test_01_provider_flask.py) and [`tests/test_01_provider_fastapi.py`](tests/test_01_provider_fastapi.py). -Unlike the consumer side, the provider side is responsible to responding to the -interactions defined by the consumers. In this regard, the provider testing -is rather simple: +Unlike the consumer side, the provider side is responsible to responding to the interactions defined by the consumers. In this regard, the provider testing is rather simple: ```py code, _ = verifier.verify_with_broker( @@ -142,21 +106,8 @@ code, _ = verifier.verify_with_broker( assert code == 0 ``` -The complication comes from the fact that the provider needs to know what state -to be in before responding to the request. In order to achieve this, a testing -endpoint is defined that sets the state of the provider as defined in the -`provider_states_setup_url` above. For example, the consumer requests has _Given -user 123 exists_ as the provider state, and the provider will need to ensure -that this state is satisfied. This would typically entail setting up a database -with the correct data, but it is advisable to achieve the equivalent state by -mocking the appropriate calls. This has been showcased in both provider -examples. +The complication comes from the fact that the provider needs to know what state to be in before responding to the request. In order to achieve this, a testing endpoint is defined that sets the state of the provider as defined in the `provider_states_setup_url` above. For example, the consumer requests has _Given user 123 exists_ as the provider state, and the provider will need to ensure that this state is satisfied. This would typically entail setting up a database with the correct data, but it is advisable to achieve the equivalent state by mocking the appropriate calls. This has been showcased in both provider examples. ### Broker -The broker acts as the intermediary between these test suites. It stores the -interactions defined by the consumer and makes them available to the provider. -Once the provider has verified that it satisfies all interactions, the broker -also stores the verification results. The example here runs the open source -broker within a Docker container. An alternative is to use the hosted [Pactflow -service](https://pactflow.io). +The broker acts as the intermediary between these test suites. It stores the interactions defined by the consumer and makes them available to the provider. Once the provider has verified that it satisfies all interactions, the broker also stores the verification results. The example here runs the open source broker within a Docker container. An alternative is to use the hosted [Pactflow service](https://pactflow.io). diff --git a/run-docker.sh b/run-docker.sh index 9bbf49a05..004f725b1 100755 --- a/run-docker.sh +++ b/run-docker.sh @@ -1,7 +1,7 @@ #!/bin/bash for arch in arm64 amd64; do - # for version in 3.6; do + # for version in 3.6; do for version in 3.7 3.8 3.9 3.10 3.11; do docker build -t python-$arch-$version --build-arg PYTHON_VERSION=$version --platform=linux/$arch . docker run -it --rm python-$arch-$version diff --git a/script/commit_message.py b/script/commit_message.py index dec30d1ee..840443536 100755 --- a/script/commit_message.py +++ b/script/commit_message.py @@ -1,7 +1,8 @@ #!/usr/bin/env python +# ruff: noqa import re -import sys import subprocess +import sys examples = """+ 61c8ca9 fix: navbar not responsive on mobile + 479c48b test: prepared test cases for user authentication @@ -13,21 +14,21 @@ def main(): - cmd_tag = "git describe --abbrev=0" - tag = subprocess.check_output(cmd_tag, - shell=True).decode("utf-8").split('\n')[0] + tag = subprocess.check_output(cmd_tag, shell=True).decode("utf-8").split("\n")[0] - cmd = "git log --pretty=format:'%s' {}..HEAD".format(tag) + cmd = f"git log --pretty=format:'%s' {tag}..HEAD" commits = subprocess.check_output(cmd, shell=True) - commits = commits.decode("utf-8").split('\n') + commits = commits.decode("utf-8").split("\n") for commit in commits: - - pattern = r'((build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert)(\([\w\-]+\))?:\s.*)|((Merge|Fixed)(\([\w\-]+\))?\s.*)' # noqa + pattern = r"((build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert)(\([\w\-]+\))?:\s.*)|((Merge|Fixed)(\([\w\-]+\))?\s.*)" m = re.match(pattern, commit) if m is None: - print("\nError with git message '{}' style".format(commit)) - print("\nPlease change commit message to the conventional format and try to commit again. Examples:") # noqa + print(f"\nError with git message '{commit}' style") + print( + "\nPlease change commit message to the conventional format and try to" + " commit again. Examples:", + ) print("\n" + examples) sys.exit(1) diff --git a/script/release_prep.sh b/script/release_prep.sh index b6bf29ca2..d664b125b 100755 --- a/script/release_prep.sh +++ b/script/release_prep.sh @@ -28,5 +28,3 @@ git add CHANGELOG.md pact/__version__.py git commit -m "chore: Releasing version $VERSION" git tag -a "$TAG_NAME" -m "Releasing version $VERSION" && git push origin master --tags - - From 297feef1cf712d303edabffba6d3e3bb712f27a3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 9 Oct 2023 08:45:34 +1100 Subject: [PATCH 0050/1376] chore(ci): disable on draft pull requests As draft PRs are explicitly in a state which is not ready to be merged, there is no need to run the full suite of CI workflows every time there is an update. In particular, this commit disables: - Cirrus workflows, as Cirrus has a minute quota - Wheel builds - Long-running tests The test matrix is _still_ being run. Signed-off-by: JP-Ellis --- .cirrus.yml | 1 + .github/workflows/build.yml | 15 ++++++++++++--- .github/workflows/test.yml | 2 ++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 9c08833dc..d889977b0 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,4 +1,5 @@ TEST_TEMPLATE: &TEST_TEMPLATE + skip: $CIRRUS_PR_DRAFT == "true" arch_check_script: - uname -am test_script: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba252ffdd..7ed4a15a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,8 @@ env: jobs: build-x86_64: name: Build wheels on ${{ matrix.os }} (x86, 64-bit) + + if: github.event_name == 'push' || ! github.event.pull_request.draft runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -58,6 +60,8 @@ jobs: build-x86: name: Build wheels on ${{ matrix.os }} (x86, 32-bit) + + if: github.event_name == 'push' || ! github.event.pull_request.draft runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -86,11 +90,12 @@ jobs: build-arm64: name: Build wheels on ${{ matrix.os }} (arm64) - runs-on: ${{ matrix.os }} + # As this requires emulation, it's not worth running on PRs if: >- github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: @@ -129,11 +134,13 @@ jobs: check: name: Check wheels + + runs-on: ubuntu-latest + needs: - build-x86_64 - build-x86 - build-arm64 - runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -152,11 +159,13 @@ jobs: publish: name: Publish wheels + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') - needs: [check] runs-on: ubuntu-latest environment: Upload Python Package + needs: [check] + steps: - uses: actions/download-artifact@v3 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index def2b9d3a..13ecd07be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,6 +61,8 @@ jobs: name: Example runs-on: ubuntu-latest + if: github.event_name == 'push' || ! github.event.pull_request.draft + services: broker: image: pactfoundation/pact-broker:latest From 21fdec085ac5988287e9dad7bed292020854ca06 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 9 Oct 2023 08:48:26 +1100 Subject: [PATCH 0051/1376] chore(ci): separate concurrency groups for builds The previous configuration for concurrency group used `head_ref` incorrectly resulting in separate runs on master being cancelled. This solution was taken from: https://stackoverflow.com/a/75403978/1573761 Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ed4a15a0..ec9a76b46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: - master concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} + group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }} cancel-in-progress: true env: From 6509894c686941ad2792ce819cc10ce65c76f9af Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 6 Oct 2023 16:27:12 +1100 Subject: [PATCH 0052/1376] chore: fix hatch test scripts Hatch scripts allow for additional arguments to be specified through the `{args:}` placeholder. Unfortunately, this was not automatically enabled. In doing this, I have also simplified the scripts by moving some common pytest options into pytest's own `addopts` option. Signed-off-by: JP-Ellis --- pyproject.toml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e7f2d3825..2ba9cce58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,10 +103,10 @@ 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/" -example = "pytest examples/ {args}" -all = ["lint", "test", "example"] +lint = ["black --check --diff {args:.}", "ruff {args:.}", "mypy {args:.}"] +test = "pytest {args:tests/}" +example = "pytest {args:examples/}" +all = ["lint", "test", "example"] # Test environment for running unit tests. This automatically tests against all # supported Python versions. @@ -117,16 +117,20 @@ features = ["test"] 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/" -example = "pytest examples/ {args}" -all = ["test", "example"] +test = "pytest {args:tests/}" +example = "pytest {args:examples/}" +all = ["test", "example"] ################################################################################ ## PyTest Configuration ################################################################################ [tool.pytest.ini_options] -addopts = ["--import-mode=importlib"] +addopts = [ + "--import-mode=importlib", + "--cov-config=pyproject.toml", + "--cov=pact", +] ################################################################################ ## Coverage From 2d3914338453620472d181ab8bc9348b251592de Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 6 Oct 2023 16:29:41 +1100 Subject: [PATCH 0053/1376] chore(test): add pytest options in root To help ensure there is a unified experience when running any/all tests, we need to define this in the root directory. The rest of the example-specific `conftest.py` can remain in `examples/conftest.py`. Running `pytest` in the root directory will now correctly execute _all_ tests, including the examples. Signed-off-by: JP-Ellis --- conftest.py | 20 ++++++++++++++++++++ examples/conftest.py | 12 ------------ 2 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000..b6bf257ad --- /dev/null +++ b/conftest.py @@ -0,0 +1,20 @@ +""" +Global Pytest configuration. + +This file is used to define global Pytest configuration. In this case, we use it +to define additional command line options to customise the examples. +""" + +import pytest + + +def pytest_addoption(parser: pytest.Parser) -> None: + """Define additional command lines to customise the examples.""" + parser.addoption( + "--broker-url", + help=( + "The URL of the broker to use. If this option has been given, the container" + " will _not_ be started." + ), + type=str, + ) diff --git a/examples/conftest.py b/examples/conftest.py index aa1de61b1..eba8ffc73 100644 --- a/examples/conftest.py +++ b/examples/conftest.py @@ -21,18 +21,6 @@ EXAMPLE_DIR = Path(__file__).parent.resolve() -def pytest_addoption(parser: pytest.Parser) -> None: - """Define additional command lines to customise the examples.""" - parser.addoption( - "--broker-url", - help=( - "The URL of the broker to use. If this option has been given, the container" - " will _not_ be started." - ), - type=str, - ) - - @pytest.fixture(scope="session") def broker(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: """ From aa69a201f4a4cf080cbdd06c100e186e1ef7239f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 6 Oct 2023 16:32:27 +1100 Subject: [PATCH 0054/1376] fix(test): ignore internal deprecation warnings Signed-off-by: JP-Ellis --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2ba9cce58..2fff1c825 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,6 +131,12 @@ addopts = [ "--cov-config=pyproject.toml", "--cov=pact", ] +filterwarnings = [ + "ignore::DeprecationWarning:pact", + "ignore::DeprecationWarning:tests", + "ignore::PendingDeprecationWarning:pact", + "ignore::PendingDeprecationWarning:tests", +] ################################################################################ ## Coverage From ab300a4837d5eb868ce77b8dd0301f3bfff0c576 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 25 Sep 2023 14:59:24 +1000 Subject: [PATCH 0055/1376] chore(build): update packaging to build ffi As we will transition to using the Rust Pact library, we need to download it as part of the build process. This commit adds a step to the build process to download the library and extract it to the correct location and then build the Python bindings. As the Rust library is available for more platforms than the Ruby executables, failing to find the Ruby executables is no longer a fatal error and will instead be raised as a warning during the build process. So small adjustments to the build script were made to accommodate this change. Signed-off-by: JP-Ellis --- hatch_build.py | 398 +++++++++++++++++++++++++++++++++++++++++-------- pyproject.toml | 12 +- 2 files changed, 340 insertions(+), 70 deletions(-) diff --git a/hatch_build.py b/hatch_build.py index 630fab65e..de4409ea6 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -1,34 +1,57 @@ -"""Hatchling build hook for Pact binary download.""" +""" +Hatchling build hook for binary downloads. + +Pact Python is built on top of the Ruby Pact binaries and the Rust Pact library. +This build script downloads the binaries and library for the current platform +and installs them in the `pact` directory under `/bin` and `/lib`. + +The version of the binaries and library can be controlled with the +`PACT_BIN_VERSION` and `PACT_LIB_VERSION` environment variables. If these are +not set, a pinned version will be used instead. +""" from __future__ import annotations +import gzip import os import shutil -import typing +import tarfile +import tempfile +import warnings +import zipfile from pathlib import Path from typing import Any, Dict +import cffi +import requests from hatchling.builders.hooks.plugin.interface import BuildHookInterface from packaging.tags import sys_tags ROOT_DIR = Path(__file__).parent.resolve() -PACT_VERSION = "2.0.7" -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): + +PACT_BIN_VERSION = os.getenv("PACT_BIN_VERSION", "2.0.7") +PACT_BIN_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" + +PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.9") +PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{machine}.{ext}" + + +class PactBuildHook(BuildHookInterface[Any]): """Custom hook to download Pact binaries.""" PLUGIN_NAME = "custom" + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 + """ + Initialize the build hook. + + For this hook, we additionally define the lib extension based on the + current platform. + """ + super().__init__(*args, **kwargs) + self.tmpdir = Path(tempfile.TemporaryDirectory().name) + self.tmpdir.mkdir(parents=True, exist_ok=True) + def clean(self, versions: list[str]) -> None: # noqa: ARG002 """Clean up any files created by the build hook.""" for subdir in ["bin", "lib", "data"]: @@ -43,10 +66,10 @@ def initialize( build_data["infer_tag"] = True build_data["pure_python"] = False - pact_version = os.getenv("PACT_VERSION", PACT_VERSION) - self.install_pact_binaries(pact_version) + self.pact_bin_install(PACT_BIN_VERSION) + self.pact_lib_install(PACT_LIB_VERSION) - def install_pact_binaries(self, version: str) -> None: # noqa: PLR0912 + def pact_bin_install(self, version: str) -> None: """ Install the Pact standalone binaries. @@ -54,10 +77,30 @@ def install_pact_binaries(self, version: str) -> None: # noqa: PLR0912 the current operating system is determined automatically. Args: - version: The Pact version to install. Defaults to the value in - `PACT_VERSION`. + version: The Pact version to install. + """ + url = self._pact_bin_url(version) + if url: + artifact = self._download(url) + self._pact_bin_extract(artifact) + + def _pact_bin_url(self, version: str) -> str | None: # noqa: PLR0911 """ - platform = typing.cast(str, next(sys_tags()).platform) + Generate the download URL for the Pact binaries. + + Generate the download URL for the Pact binaries based on the current + platform and specified version. This function mainly contains a lot of + matching logic to determine the correct URL to use, due to the + inconsistencies in naming conventions between ecosystems. + + Args: + version: The upstream Pact version. + + Returns: + The URL to download the Pact binaries from, or None if the current + platform is not supported. + """ + platform = next(sys_tags()).platform if platform.startswith("macosx"): os = "osx" @@ -67,81 +110,314 @@ def install_pact_binaries(self, version: str) -> None: # noqa: PLR0912 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") + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return None + return PACT_BIN_URL.format( + version=version, + os=os, + machine=machine, + ext="tar.gz", + ) - elif platform.startswith("win"): + if platform.startswith("win"): os = "windows" if platform.endswith("amd64"): machine = "x86_64" elif platform.endswith(("x86", "win32")): machine = "x86" + else: + msg = f"Unknown Windows machine {platform}" + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return None + return PACT_BIN_URL.format( + version=version, + os=os, + machine=machine, + ext="zip", + ) + + if "linux" in platform and "musl" not 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}" + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return None + return PACT_BIN_URL.format( + version=version, + os=os, + machine=machine, + ext="tar.gz", + ) + + msg = f"Unknown platform {platform}" + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return None + + def _pact_bin_extract(self, artifact: Path) -> None: + """ + Extract the Pact binaries. + + The upstream distributables contain a lot of files which are not needed + for this library. This function ensures that only the files in + `pact/bin` are extracted to avoid unnecessary bloat. + + Args: + artifact: The path to the downloaded artifact. + """ + (ROOT_DIR / "pact" / "bin").mkdir(parents=True, exist_ok=True) + + if str(artifact).endswith(".zip"): + with zipfile.ZipFile(artifact) as f: + for member in f.namelist(): + if member.startswith("pact/bin"): + f.extract(member, ROOT_DIR) + + if str(artifact).endswith(".tar.gz"): + with tarfile.open(artifact) as f: + for member in f.getmembers(): + if member.name.startswith("pact/bin"): + f.extract(member, ROOT_DIR) + + def pact_lib_install(self, version: str) -> None: + """ + Install the Pact library binary. + + The library is installed in `pact/lib`, and the relevant version for + the current operating system is determined automatically. + + Args: + version: The Pact version to install. + """ + url = self._pact_lib_url(version) + artifact = self._download(url) + self._pact_lib_extract(artifact) + includes = self._pact_lib_header(url) + self._pact_lib_cffi(includes) + + def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 + """ + Generate the download URL for the Pact library. + + Generate the download URL for the Pact library based on the current + platform and specified version. This function mainly contains a lot of + matching logic to determine the correct URL to use, due to the + inconsistencies in naming conventions between ecosystems. + + Args: + version: The upstream Pact version. + + Returns: + The URL to download the Pact library from. + + Raises: + ValueError: + If the current platform is not supported. + """ + platform = next(sys_tags()).platform + + if platform.startswith("macosx"): + os = "osx" + if platform.endswith("arm64"): + machine = "aarch64-apple-darwin" + elif platform.endswith("x86_64"): + machine = "x86_64" + else: + msg = f"Unknown macOS machine {platform}" + raise ValueError(msg) + return PACT_LIB_URL.format( + prefix="lib", + version=version, + os=os, + machine=machine, + ext="a.gz", + ) + + if platform.startswith("win"): + os = "windows" + + if platform.endswith("amd64"): + machine = "x86_64" else: msg = f"Unknown Windows machine {platform}" raise ValueError(msg) + return PACT_LIB_URL.format( + prefix="", + version=version, + os=os, + machine=machine, + ext="lib.gz", + ) - url = PACT_URL.format(version=version, os=os, machine=machine, ext="zip") + if "linux" in platform and "musl" in platform: + os = "linux" + if platform.endswith("x86_64"): + machine = "x86_64-musl" + else: + msg = f"Unknown MUSL Linux machine {platform}" + raise ValueError(msg) + return PACT_LIB_URL.format( + prefix="lib", + version=version, + os=os, + machine=machine, + ext="a.gz", + ) - elif "linux" in platform: + if "linux" in platform: os = "linux" if platform.endswith("x86_64"): machine = "x86_64" elif platform.endswith("aarch64"): - machine = "arm64" + machine = "aarch64" else: msg = f"Unknown Linux machine {platform}" raise ValueError(msg) - url = PACT_URL.format(version=version, os=os, machine=machine, ext="tar.gz") + return PACT_LIB_URL.format( + prefix="lib", + version=version, + os=os, + machine=machine, + ext="a.gz", + ) + + msg = f"Unknown platform {platform}" + raise ValueError(msg) - else: - msg = f"Unknown platform {platform}" + def _pact_lib_extract(self, artifact: Path) -> None: + """ + Extract the Pact library. + + Extract the Pact library from the downloaded artifact and place it in + `pact/lib`. + + Args: + artifact: The URL to download the Pact binaries from. + """ + if not str(artifact).endswith(".gz"): + msg = f"Unknown artifact type {artifact}" raise ValueError(msg) - self.download_and_extract_pact(url) + with gzip.open(artifact, "rb") as f_in, ( + self.tmpdir / (artifact.name.split("-")[0] + artifact.suffixes[0]) + ).open("wb") as f_out: + shutil.copyfileobj(f_in, f_out) + + def _pact_lib_header(self, url: str) -> list[str]: + """ + Download the Pact library header. + + Download the Pact library header from GitHub and place it in + `pact/include`. This uses the same URL as for the artifact, replacing + the final segment with `pact.h`. + + This also processes the header to strip out elements which are not + supported by CFFI (i.e., any line starting with `#`). The list of + `#include` statements is returned for use in the CFFI bindings. + + Args: + url: The URL pointing to the Pact library artifact. + """ + url = url.rsplit("/", 1)[0] + "/pact.h" + artifact = self._download(url) + includes: list[str] = [] + with artifact.open("r", encoding="utf-8") as f_in, ( + self.tmpdir / "pact.h" + ).open("w", encoding="utf-8") as f_out: + for line in f_in: + sline = line.strip() + if sline.startswith("#include"): + includes.append(sline) + continue + if sline.startswith("#"): + continue + + f_out.write(line) + return includes + + def _pact_lib_cffi(self, includes: list[str]) -> None: + """ + Build the CFFI bindings for the Pact library. + + This will build the CFFI bindings for the Pact library and place them in + `pact/lib`. + + A list of additional `#include` statements can be passed to this + function, which will be included in the generated bindings. - def download_and_extract_pact(self, url: str) -> None: + Args: + includes: + A list of additional `#include` statements to include in the + generated bindings. """ - Download and extract the Pact binaries. + if os.name == "nt": + extra_libs = [ + "advapi32", + "bcrypt", + "crypt32", + "iphlpapi", + "ncrypt", + "netapi32", + "ntdll", + "ole32", + "oleaut32", + "pdh", + "powrprof", + "psapi", + "secur32", + "shell32", + "user32", + "userenv", + "ws2_32", + ] + else: + extra_libs = [] - If the download artifact is already present, it will be used instead of - downloading it again. + ffibuilder = cffi.FFI() + with (self.tmpdir / "pact.h").open( + "r", + encoding="utf-8", + ) as f: + ffibuilder.cdef(f.read()) + ffibuilder.set_source( + "_ffi", + "\n".join([*includes, '#include "pact.h"']), + libraries=["pact_ffi", *extra_libs], + library_dirs=[str(self.tmpdir)], + ) + output = ffibuilder.compile(verbose=True, tmpdir=str(self.tmpdir)) + shutil.copy(output, ROOT_DIR / "pact" / "v3") + + def _download(self, url: str) -> Path: + """ + Download the target URL. + + This will download the target URL to the `pact/data` directory. If the + download artifact is already present, its path will be returned. Args: - url: The URL to download the Pact binaries from. + url: The URL to download + + Return: + The path to the downloaded artifact. """ 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() + try: + response.raise_for_status() + except requests.HTTPError as e: + msg = f"Failed to download from {url}." + raise RuntimeError(msg) from e 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", - ) + return artifact diff --git a/pyproject.toml b/pyproject.toml index 2fff1c825..c059fc2d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ dev = [ ################################################################################ [build-system] -requires = ["hatchling", "packaging", "requests"] +requires = ["hatchling", "packaging", "requests", "cffi"] build-backend = "hatchling.build" [tool.hatch.version] @@ -78,17 +78,11 @@ 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/*"] +artifacts = ["pact/bin/*", "pact/lib/*"] [tool.hatch.build.targets.wheel.hooks.custom] @@ -100,7 +94,7 @@ exclude = ["pact/data/*"] # workflow. [tool.hatch.envs.default] features = ["dev"] -extra-dependencies = ["hatchling", "packaging", "requests"] +extra-dependencies = ["hatchling", "packaging", "requests", "cffi"] [tool.hatch.envs.default.scripts] lint = ["black --check --diff {args:.}", "ruff {args:.}", "mypy {args:.}"] From a8ab4592ffca3607f6b90570ab1a2f3cb0722fdd Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 25 Sep 2023 15:26:20 +1000 Subject: [PATCH 0056/1376] style!: refactor constants With the possibility of building wheels against systems for which no Ruby executables exist, the constants module has been refactored to allow for the use of system installed Pact executables. This also introduces the `PACT_USE_SYSTEM_BINS` environment variable which can be used to force the use of system installed Pact executables. In doing these changes, the module has been refactored to avoid redundancies, and avoids the complexities of Windows executables extensions by using the `shutil.which` function. The test was refactored accordingly to test the constants instead of the functions. BREAKING CHANGE: The public functions within the constants module have been removed. If you previously used them, please make use of the constants. For example, instead of `pact.constants.broker_client_exe()` use `pact.constants.BROKER_CLIENT_PATH` instead. BREAKING CHANGE: It is possible to use the system installed Pact executables by setting `PACT_USE_SYSTEM_BINS` to `True` or `Yes` (case insensitive). Signed-off-by: JP-Ellis --- pact/constants.py | 92 +++++++++++++++++++++++++---------------- tests/test_constants.py | 92 +++++++++++++++++------------------------ 2 files changed, 95 insertions(+), 89 deletions(-) diff --git a/pact/constants.py b/pact/constants.py index 225d0a528..f29805940 100644 --- a/pact/constants.py +++ b/pact/constants.py @@ -1,38 +1,60 @@ -"""Constant values for the pact-python package.""" +""" +Constant values for the pact-python package. + +This will default to the bundled Pact binaries bundled with the package, but +should these be unavailable or the environment variable `PACT_USE_SYSTEM_BINS` is +set to `TRUE` or `YES`, the system Pact binaries will be used instead. +""" import os +import shutil +import warnings from pathlib import Path - -def broker_client_exe() -> str: - """Get the appropriate executable name for this platform.""" - if os.name == "nt": - return "pact-broker.bat" - return "pact-broker" - - -def message_exe() -> str: - """Get the appropriate executable name for this platform.""" - if os.name == "nt": - return "pact-message.bat" - return "pact-message" - - -def mock_service_exe() -> str: - """Get the appropriate executable name for this platform.""" - if os.name == "nt": - return "pact-mock-service.bat" - return "pact-mock-service" - - -def provider_verifier_exe() -> str: - """Get the appropriate provider executable name for this platform.""" - 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() +__all__ = [ + "BROKER_CLIENT_PATH", + "MESSAGE_PATH", + "MOCK_SERVICE_PATH", + "VERIFIER_PATH", +] + + +_USE_SYSTEM_BINS = os.getenv("PACT_USE_SYSTEM_BINS", "").upper() in ("TRUE", "YES") +_BIN_DIR = Path(__file__).parent.resolve() / "bin" + + +def _find_executable(executable: str) -> str: + """ + Find the path to an executable. + + This inspects the environment variable `PACT_USE_SYSTEM_BINS` to determine + whether to use the bundled Pact binaries or the system ones. Note that if + the local executables are not found, this will fall back to the system + executables (if found). + + Args: + executable: + The name of the executable to find without the extension. Python + will automatically append the correct extension for the current + platform. + + Returns: + The absolute path to the executable. + + Warns: + RuntimeWarning: + If the executable cannot be found in the system path. + """ + if _USE_SYSTEM_BINS: + bin_path = shutil.which(executable) + else: + bin_path = shutil.which(executable, path=_BIN_DIR) or shutil.which(executable) + if bin_path is None: + msg = f"Unable to find {executable} binary executable." + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return bin_path or "" + + +BROKER_CLIENT_PATH = _find_executable("pact-broker") +MESSAGE_PATH = _find_executable("pact-message") +MOCK_SERVICE_PATH = _find_executable("pact-mock-service") +VERIFIER_PATH = _find_executable("pact-provider-verifier") diff --git a/tests/test_constants.py b/tests/test_constants.py index 38bc48b90..384ae2dc6 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -1,67 +1,51 @@ -from unittest import TestCase +"""Test the values in pact.constants.""" -from mock import patch +import os -from pact import constants as constants +def test_broker_client() -> None: + """Test the value of BROKER_CLIENT_PATH on POSIX.""" + import pact.constants -class BrokerClientExeTestCase(TestCase): - def setUp(self): - super(BrokerClientExeTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_os = patch.object(constants, 'os', autospec=True).start() + if os.name == "nt": + # As the Windows filesystem is case insensitive, we must normalize it. + assert pact.constants.BROKER_CLIENT_PATH.lower().endswith("pact-broker.bat") + else: + assert pact.constants.BROKER_CLIENT_PATH.endswith("pact-broker") - def test_other(self): - self.mock_os.name = 'posix' - self.assertEqual(constants.broker_client_exe(), 'pact-broker') - def test_windows(self): - self.mock_os.name = 'nt' - self.assertEqual(constants.broker_client_exe(), 'pact-broker.bat') +def test_message() -> None: + """Test the value of MESSAGE_PATH on POSIX.""" + import pact.constants + if os.name == "nt": + # As the Windows filesystem is case insensitive, we must normalize it. + assert pact.constants.MESSAGE_PATH.lower().endswith("pact-message.bat") + else: + assert pact.constants.MESSAGE_PATH.endswith("pact-message") -class MockServiceExeTestCase(TestCase): - def setUp(self): - super(MockServiceExeTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_os = patch.object(constants, 'os', autospec=True).start() - def test_other(self): - self.mock_os.name = 'posix' - self.assertEqual(constants.mock_service_exe(), 'pact-mock-service') +def test_mock_service() -> None: + """Test the value of MOCK_SERVICE_PATH on POSIX.""" + import pact.constants - def test_windows(self): - self.mock_os.name = 'nt' - self.assertEqual(constants.mock_service_exe(), 'pact-mock-service.bat') + if os.name == "nt": + # As the Windows filesystem is case insensitive, we must normalize it. + assert pact.constants.MOCK_SERVICE_PATH.lower().endswith( + "pact-mock-service.bat", + ) + else: + assert pact.constants.MOCK_SERVICE_PATH.endswith("pact-mock-service") -class MessageExeTestCase(TestCase): - def setUp(self): - super(MessageExeTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_os = patch.object(constants, 'os', autospec=True).start() +def test_verifier() -> None: + """Test the value of VERIFIER_PATH on POSIX.""" + import pact.constants - def test_other(self): - self.mock_os.name = 'posix' - self.assertEqual(constants.message_exe(), 'pact-message') - - def test_windows(self): - self.mock_os.name = 'nt' - self.assertEqual(constants.message_exe(), 'pact-message.bat') - - -class ProviderVerifierExeTestCase(TestCase): - def setUp(self): - super(ProviderVerifierExeTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_os = patch.object(constants, 'os', autospec=True).start() - - def test_other(self): - self.mock_os.name = 'posix' - self.assertEqual( - constants.provider_verifier_exe(), 'pact-provider-verifier') - - def test_windows(self): - self.mock_os.name = 'nt' - self.assertEqual( - constants.provider_verifier_exe(), 'pact-provider-verifier.bat') + if os.name == "nt": + # As the Windows filesystem is case insensitive, we must normalize it. + assert pact.constants.VERIFIER_PATH.lower().endswith( + "pact-provider-verifier.bat", + ) + else: + assert pact.constants.VERIFIER_PATH.endswith("pact-provider-verifier") From 57d015c8906b1fac1b41900ea682cb1b89ea19b6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 25 Sep 2023 15:50:42 +1000 Subject: [PATCH 0057/1376] chore(tests): add ruff.toml for tests directory Adjust the lint rules that apply to tests to be more permissive. Specifically to allow the use of `assert` statements. Signed-off-by: JP-Ellis --- tests/ruff.toml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tests/ruff.toml diff --git a/tests/ruff.toml b/tests/ruff.toml new file mode 100644 index 000000000..7d4b21b00 --- /dev/null +++ b/tests/ruff.toml @@ -0,0 +1,6 @@ +extend = "../pyproject.toml" +ignore = [ + "D103", # Require docstrings on public functions + "S101", # Disable assert + "PLR2004", # Forbid magic numbers +] From 0d26395d9cac1cc3ca82dfd1b125f376d92706d5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 26 Sep 2023 11:08:45 +1000 Subject: [PATCH 0058/1376] feat(v3): add v3.ffi module This module provides a Python interface to the Pact library written in Rust. For this first commit, only the `pactffi_version()` function is implemented and tested. In the transition to v3, the new codebase will be located within the `v3` submodule. This will allow the v2 code to remain in place for backwards compatibility, and will allow the v3 code to be tested independently of the v2 code. Once the v3 code is complete, the existing v2 code will be scoped to a new `v2` submodule, and the v3 code will be moved to the root of the repository. Signed-off-by: JP-Ellis --- pact/v3/__init__.py | 21 +++++++++++++++++++++ pact/v3/ffi.py | 18 ++++++++++++++++++ tests/test_ffi.py | 15 +++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 pact/v3/__init__.py create mode 100644 pact/v3/ffi.py create mode 100644 tests/test_ffi.py diff --git a/pact/v3/__init__.py b/pact/v3/__init__.py new file mode 100644 index 000000000..443bf9164 --- /dev/null +++ b/pact/v3/__init__.py @@ -0,0 +1,21 @@ +""" +Pact Python V3. + +The next major release of Pact Python will make use of the Pact reference +library written in Rust. This will allow us to support all of the features of +Pact, and bring the Python library in line with the other Pact libraries. + +The migration will happen in stages, and this module will be used to provide +access to the new functionality without breaking existing code. The stages will +be as follows: + +- **Stage 1**: The new library is exposed within `pact.v3` and can be used + alongside the existing library. During this stage, no guarantees are made + about the stability of the `pact.v3` module. +- **Stage 2**: The library within `pact.v3` is considered stable, and we begin + the process of deprecating the existing library by raising deprecation + warnings when it is used. A detailed migration guide will be provided. +- **Stage 3**: The `pact.v3` module is renamed to `pact`, and the existing + library is moved to the `pact.v2` scope. The `pact.v2` module will be + considered deprecated, and will be removed in a future release. +""" diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py new file mode 100644 index 000000000..3ff7bbfa7 --- /dev/null +++ b/pact/v3/ffi.py @@ -0,0 +1,18 @@ +""" +Python bindings for the Pact FFI. + +This module provides a Python interface to the Pact FFI. It is a thin wrapper +around the C API, and is intended to be used by the Pact Python client library +to provide a Pythonic interface to Pact. + +This module is not intended to be used directly by Pact users. Pact users +should use the Pact Python client library instead. No guarantees are made +about the stability of this module's API. +""" + +from ._ffi import ffi, lib + + +def version() -> str: + """Return the version of the Pact FFI library.""" + return ffi.string(lib.pactffi_version()).decode("utf-8") diff --git a/tests/test_ffi.py b/tests/test_ffi.py new file mode 100644 index 000000000..be3aff537 --- /dev/null +++ b/tests/test_ffi.py @@ -0,0 +1,15 @@ +""" +Tests of the FFI module. + +These tests are intended to ensure that the FFI module is working correctly. +They are not intended to test the Pact API itself, as that is handled by the +client library. +""" + +from pact.v3 import ffi + + +def test_version() -> None: + assert isinstance(ffi.version(), str) + assert len(ffi.version()) > 0 + assert ffi.version().count(".") == 2 From 11cc67698d0cf5d7b8c3dc11023dc818d04d6934 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 26 Sep 2023 12:30:45 +1000 Subject: [PATCH 0059/1376] chore(ci): update build targets As the upstream Pact reference library has a different set of targets, the build targets for this library have been updated to match. The most significant change is the dropping is 32-bit architectures altogether. This also adds a `musllinux` target (which was previously not supported). Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ec9a76b46..fe1c559cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,36 +58,6 @@ jobs: path: ./wheelhouse/*.whl if-no-files-found: error - build-x86: - name: Build wheels on ${{ matrix.os }} (x86, 32-bit) - - if: github.event_name == 'push' || ! github.event.pull_request.draft - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: windows-latest - archs: x86 - - steps: - - uses: actions/checkout@v4 - with: - # Fetch all tags - fetch-depth: 0 - - - name: Create wheels - uses: pypa/cibuildwheel@v2.15.0 - env: - CIBW_ARCHS: ${{ matrix.archs }} - - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: ./wheelhouse/*.whl - if-no-files-found: error - build-arm64: name: Build wheels on ${{ matrix.os }} (arm64) @@ -102,10 +72,8 @@ jobs: include: - os: ubuntu-latest archs: aarch64 - build: "*manylinux*" - os: macos-latest archs: arm64 - build: "*" steps: - uses: actions/checkout@v4 @@ -123,7 +91,6 @@ jobs: uses: pypa/cibuildwheel@v2.15.0 env: CIBW_ARCHS: ${{ matrix.archs }} - CIBW_BUILD: ${{ matrix.build }} - name: Upload wheels uses: actions/upload-artifact@v3 @@ -139,7 +106,6 @@ jobs: needs: - build-x86_64 - - build-x86 - build-arm64 steps: From 5688e3c1e92cbe57c274ab004bfb9519a54eeb6b Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 6 Oct 2023 14:53:13 +1100 Subject: [PATCH 0060/1376] fix(build): include omitted `lib` dir When packaging the Ruby Pact binaries, I initially removed the `lib` dir naively believing that the Pact binaries were static. This is in fact incorrect, and I adjusted the extraction to extract _all_ of the content (and only remove the README.md). The unit tests all passed which affirmed my initial belief. Unfortunately (as I have now discovered), the unit tests mock out the call to the binaries, and therefore the test suite did not actuall test the execution of the binaries. Signed-off-by: JP-Ellis --- hatch_build.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/hatch_build.py b/hatch_build.py index de4409ea6..01b9d122e 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -169,19 +169,16 @@ def _pact_bin_extract(self, artifact: Path) -> None: Args: artifact: The path to the downloaded artifact. """ - (ROOT_DIR / "pact" / "bin").mkdir(parents=True, exist_ok=True) - if str(artifact).endswith(".zip"): with zipfile.ZipFile(artifact) as f: - for member in f.namelist(): - if member.startswith("pact/bin"): - f.extract(member, ROOT_DIR) + f.extractall(ROOT_DIR) if str(artifact).endswith(".tar.gz"): with tarfile.open(artifact) as f: - for member in f.getmembers(): - if member.name.startswith("pact/bin"): - f.extract(member, ROOT_DIR) + f.extractall(ROOT_DIR) + + # Cleanup the extract `README.md` + (ROOT_DIR / "pact" / "README.md").unlink() def pact_lib_install(self, version: str) -> None: """ From 257e3217156cee442bbb785f5115ff60dbcbd821 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 2 Oct 2023 15:31:17 +1100 Subject: [PATCH 0061/1376] chore(v3): create ffi.py Import close to 400 functions exposed by the Rust library into the `ffi.py` file. All of them (save `version`) solely raise a `NotImplementedError`. The documentation of the functions has been automatically imported from the exposed docstring by the `pact.h` header. Some more minor changes includes: - Adding the `py.typed` marker file (see [PEP 561](https://peps.python.org/pep-0561/)) - Move all tests relating to `v3` into `tests/v3` - Minor fixes toe the `ruff.toml` lint rules for tests - Update type dependencies Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 6091 ++++++++++++++++++++++++- tests/__init__.py => pact/v3/py.typed | 0 pyproject.toml | 6 +- tests/ruff.toml | 3 +- tests/{ => v3}/test_ffi.py | 0 5 files changed, 6093 insertions(+), 7 deletions(-) rename tests/__init__.py => pact/v3/py.typed (100%) rename tests/{ => v3}/test_ffi.py (100%) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 3ff7bbfa7..b2f3b03b8 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -5,14 +5,6095 @@ around the C API, and is intended to be used by the Pact Python client library to provide a Pythonic interface to Pact. -This module is not intended to be used directly by Pact users. Pact users -should use the Pact Python client library instead. No guarantees are made -about the stability of this module's API. +This module is not intended to be used directly by Pact users. Pact users should +use the Pact Python client library instead. No guarantees are made about the +stability of this module's API. + +## Developer Notes + +This modules should provide the following only: + +- Basic Enum classes +- Simple wrappers around functions, including the casting of input and output + values between the high level Python types and the low level C types. + +These low-level functions may then be combined into higher level classes and +modules. + +During initial implementation, a lot of these functions will simply raise a +`NotImplementedError`. + +For those unfamiliar with CFFI, please make sure to read the [CFFI +documentation](https://cffi.readthedocs.io/en/latest/using.html). """ +# ruff: noqa: ARG001 (unused-function-argument) +# ruff: noqa: A002 (builtin-argument-shadowing) +# ruff: noqa: D101 (undocumented-public-class) + +import warnings +from enum import Enum +from typing import List + +from ._ffi import ffi, lib # type: ignore[import] + +# The follow types are classes defined in the Rust code. Ultimately, a Python +# alternative should be implemented, but for now, the follow lines only serve +# to inform the type checker of the existence of these types. + + +class AsynchronousMessage: + ... + + +class Consumer: + ... + + +class Generator: + ... + + +class GeneratorCategoryIterator: + ... + + +class GeneratorKeyValuePair: + ... + + +class HttpRequest: + ... + + +class HttpResponse: + ... + + +class InteractionHandle: + ... + + +class MatchingRule: + ... + + +class MatchingRuleCategoryIterator: + ... + + +class MatchingRuleDefinitionResult: + ... + + +class MatchingRuleIterator: + ... + + +class MatchingRuleKeyValuePair: + ... + + +class MatchingRuleResult: + ... + + +class Message: + ... + + +class MessageContents: + ... + + +class MessageHandle: + ... + + +class MessageMetadataIterator: + ... + + +class MessageMetadataPair: + ... + + +class MessagePact: + ... + + +class MessagePactHandle: + ... + + +class MessagePactMessageIterator: + ... + + +class MessagePactMetadataIterator: + ... + + +class MessagePactMetadataTriple: + ... + + +class Mismatch: + ... + + +class Mismatches: + ... + + +class MismatchesIterator: + ... + -from ._ffi import ffi, lib +class Pact: + ... + + +class PactHandle: + ... + + +class PactInteraction: + ... + + +class PactInteractionIterator: + ... + + +class PactMessageIterator: + ... + + +class PactSyncHttpIterator: + ... + + +class PactSyncMessageIterator: + ... + + +class Provider: + ... + + +class ProviderState: + ... + + +class ProviderStateIterator: + ... + + +class ProviderStateParamIterator: + ... + + +class ProviderStateParamPair: + ... + + +class SynchronousHttp: + ... + + +class SynchronousMessage: + ... + + +class VerifierHandle: + ... + + +class ExpressionValueType(Enum): + """ + Expression Value Type. + + [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/models/expressions/enum.ExpressionValueType.html) + """ + + UNKNOWN = lib.ExpressionValueType_Unknown + STRING = lib.ExpressionValueType_String + NUMBER = lib.ExpressionValueType_Number + INTEGER = lib.ExpressionValueType_Integer + DECIMAL = lib.ExpressionValueType_Decimal + BOOLEAN = lib.ExpressionValueType_Boolean + + +class GeneratorCategory(Enum): + """ + Generator Category. + + [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/models/generators/enum.GeneratorCategory.html) + """ + + METHOD = lib.GeneratorCategory_METHOD + PATH = lib.GeneratorCategory_PATH + HEADER = lib.GeneratorCategory_HEADER + QUERY = lib.GeneratorCategory_QUERY + BODY = lib.GeneratorCategory_BODY + STATUS = lib.GeneratorCategory_STATUS + METADATA = lib.GeneratorCategory_METADATA + + +class InteractionPart(Enum): + """ + Interaction Part. + + [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/mock_server/handles/enum.InteractionPart.html) + """ + + REQUEST = lib.InteractionPart_Request + RESPONSE = lib.InteractionPart_Response + + +class LevelFilter(Enum): + """Level Filter.""" + + OFF = lib.LevelFilter_Off + ERROR = lib.LevelFilter_Error + WARN = lib.LevelFilter_Warn + INFO = lib.LevelFilter_Info + DEBUG = lib.LevelFilter_Debug + TRACE = lib.LevelFilter_Trace + + +class MatchingRuleCategory(Enum): + """ + Matching Rule Category. + + [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) + """ + + METHOD = lib.MatchingRuleCategory_METHOD + PATH = lib.MatchingRuleCategory_PATH + HEADER = lib.MatchingRuleCategory_HEADER + QUERY = lib.MatchingRuleCategory_QUERY + BODY = lib.MatchingRuleCategory_BODY + STATUS = lib.MatchingRuleCategory_STATUS + CONTENST = lib.MatchingRuleCategory_CONTENTS + METADATA = lib.MatchingRuleCategory_METADATA + + +class PactSpecification(Enum): + """ + Pact Specification. + + [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/models/pact_specification/enum.PactSpecification.html) + """ + + UNKNOWN = lib.PactSpecification_Unknown + V1 = lib.PactSpecification_V1 + V1_1 = lib.PactSpecification_V1_1 + V2 = lib.PactSpecification_V2 + V3 = lib.PactSpecification_V3 + V4 = lib.PactSpecification_V4 + + +class StringResult(Enum): + """ + String Result. + + [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/mock_server/enum.StringResult.html) + """ + + FAILED = lib.StringResult_Failed + Ok = lib.StringResult_Ok def version() -> str: - """Return the version of the Pact FFI library.""" + """ + Wraps a Pact model struct. + + [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_version) + """ return ffi.string(lib.pactffi_version()).decode("utf-8") + + +def init(log_env_var: str) -> None: + """ + Initialise the mock server library. + + This can provide an environment variable name to use to set the log levels. + This function should only be called once, as it tries to install a global + tracing subscriber. + + [Rust + `pactffi_init`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_init) + + # Safety + + log_env_var must be a valid NULL terminated UTF-8 string. + """ + raise NotImplementedError + + +def init_with_log_level(level: str) -> None: + """ + Initialises logging, and sets the log level explicitly. + + This function should only be called once, as it tries to install a global + tracing subscriber. + + [Rust + `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_init_with_log_level) + + # Safety + + Exported functions are inherently unsafe. + """ + raise NotImplementedError + + +def enable_ansi_support() -> None: + """ + Enable ANSI coloured output on Windows. + + On non-Windows platforms, this function is a no-op. + + [Rust + `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_enable_ansi_support) + + # Safety + + This function is safe. + """ + raise NotImplementedError + + +def log_message(source: str, log_level: str, message: str) -> None: + """ + Log using the shared core logging facility. + + [Rust + `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_message) + + This is useful for callers to have a single set of logs. + + - `source`: String. The source of the log, such as the class or caller + framework to disambiguate log lines from the rust logging (e.g. pact_go) + - `log_level`: String. One of TRACE, DEBUG, INFO, WARN, ERROR + - `message`: Message to log + + # Safety + + This function will fail if any of the pointers passed to it are invalid. + """ + raise NotImplementedError + + +def match_message(msg_1: Message, msg_2: Message) -> Mismatches: + """ + Match a pair of messages, producing a collection of mismatches. + + If the messages match, the returned collection will be empty. + + [Rust + `pactffi_match_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_match_message) + """ + raise NotImplementedError + + +def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: + """ + Get an iterator over mismatches. + + [Rust + `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatches_get_iter) + """ + raise NotImplementedError + + +def mismatches_delete(mismatches: Mismatches) -> None: + """ + Delete mismatches. + + [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatches_delete) + """ + raise NotImplementedError + + +def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: + """ + Get the next mismatch from a mismatches iterator. + + [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatches_iter_next) + + Returns a null pointer if no mismatches remain. + """ + raise NotImplementedError + + +def mismatches_iter_delete(iter: MismatchesIterator) -> None: + """ + Delete a mismatches iterator when you're done with it. + + [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatches_iter_delete) + """ + raise NotImplementedError + + +def mismatch_to_json(mismatch: Mismatch) -> str: + """ + Get a JSON representation of the mismatch. + + [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatch_to_json) + """ + raise NotImplementedError + + +def mismatch_type(mismatch: Mismatch) -> str: + """ + Get the type of a mismatch. + + [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatch_type) + """ + raise NotImplementedError + + +def mismatch_summary(mismatch: Mismatch) -> str: + """ + Get a summary of a mismatch. + + [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatch_summary) + """ + raise NotImplementedError + + +def mismatch_description(mismatch: Mismatch) -> str: + """ + Get a description of a mismatch. + + [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatch_description) + """ + raise NotImplementedError + + +def mismatch_ansi_description(mismatch: Mismatch) -> str: + """ + Get an ANSI-compatible description of a mismatch. + + [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatch_ansi_description) + """ + raise NotImplementedError + + +def get_error_message(buffer: str, length: int) -> int: + """ + Provide the error message from `LAST_ERROR` to the calling C code. + + [Rust + `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_get_error_message) + + This function should be called after any other function in the pact_matching + FFI indicates a failure with its own error message, if the caller wants to + get more context on why the error happened. + + Do note that this error-reporting mechanism only reports the top-level error + message, not any source information embedded in the original Rust error + type. If you want more detailed information for debugging purposes, use the + logging interface. + + # Params + + - `buffer`: a pointer to an array of `char` of sufficient length to hold the + error message. + - `length`: an int providing the length of the `buffer`. + + # Return Codes + + - The number of bytes written to the provided buffer, which may be zero if + there is no last error. + - `-1` if the provided buffer is a null pointer. + - `-2` if the provided buffer length is too small for the error message. + - `-3` if the write failed for some other reason. + - `-4` if the error message had an interior NULL + + # Notes + + Note that this function zeroes out any excess in the provided buffer. + + # Error Handling + + The return code must be checked for one of the negative number error codes + before the buffer is used. If an error code is present, the buffer may not + be in a usable state. + + If the buffer is longer than needed for the error message, the excess space + will be zeroed as a safety mechanism. This is slightly less efficient than + leaving the contents of the buffer alone, but the difference is expected to + be negligible in practice. + """ + raise NotImplementedError + + +def log_to_stdout(level_filter: LevelFilter) -> int: + """ + Convenience function to direct all logging to stdout. + + [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_to_stdout) + """ + raise NotImplementedError + + +def log_to_stderr(level_filter: LevelFilter) -> int: + """ + Convenience function to direct all logging to stderr. + + [Rust `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_to_stderr) + """ + raise NotImplementedError + + +def log_to_file(file_name: str, level_filter: LevelFilter) -> int: + """ + Convenience function to direct all logging to a file. + + [Rust + `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_to_file) + + # Safety + + This function will fail if the file_name pointer is invalid or does not + point to a NULL terminated string. + """ + raise NotImplementedError + + +def log_to_buffer(level_filter: LevelFilter) -> int: + """ + Convenience function to direct all logging to a task local memory buffer. + + [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_to_buffer) + """ + raise NotImplementedError + + +def logger_init() -> None: + """ + Initialize the FFI logger with no sinks. + + [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_logger_init) + + This initialized logger does nothing until `pactffi_logger_apply` has been called. + + # Usage + + ```c + pactffi_logger_init(); + ``` + + # Safety + + This function is always safe to call. + """ + raise NotImplementedError + + +def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: + """ + Attach an additional sink to the thread-local logger. + + [Rust + `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_logger_attach_sink) + + This logger does nothing until `pactffi_logger_apply` has been called. + + Types of sinks can be specified: + + - stdout (`pactffi_logger_attach_sink("stdout", LevelFilter_Info)`) + - stderr (`pactffi_logger_attach_sink("stderr", LevelFilter_Debug)`) + - file w/ file path (`pactffi_logger_attach_sink("file /some/file/path", + LevelFilter_Trace)`) + - buffer (`pactffi_logger_attach_sink("buffer", LevelFilter_Debug)`) + + # Usage + + ```c + int result = pactffi_logger_attach_sink("file /some/file/path", LogLevel_Filter); + ``` + + # Error Handling + + The return error codes are as follows: + + - `-1`: Can't set logger (applying the logger failed, perhaps because one is + applied already). + - `-2`: No logger has been initialized (call `pactffi_logger_init` before + any other log function). + - `-3`: The sink specifier was not UTF-8 encoded. + - `-4`: The sink type specified is not a known type (known types: "stdout", + "stderr", or "file /some/path"). + - `-5`: No file path was specified in a file-type sink specification. + - `-6`: Opening a sink to the specified file path failed (check + permissions). + + # Safety + + This function checks the validity of the passed-in sink specifier, and + errors out if the specifier isn't valid UTF-8. Passing in an invalid or NULL + pointer will result in undefined behaviour. + """ + raise NotImplementedError + + +def logger_apply() -> int: + """ + Apply the previously configured sinks and levels to the program. + + [Rust + `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_logger_apply) + + If no sinks have been setup, will set the log level to info and the target + to standard out. + + This function will install a global tracing subscriber. Any attempts to + modify the logger after the call to `logger_apply` will fail. + """ + raise NotImplementedError + + +def fetch_log_buffer(log_id: str) -> str: + """ + Fetch the in-memory logger buffer contents. + + [Rust + `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_fetch_log_buffer) + + This will only have any contents if the `buffer` sink has been configured to + log to. The contents will be allocated on the heap and will need to be freed + with `pactffi_string_delete`. + + Fetches the logs associated with the provided identifier, or uses the + "global" one if the identifier is not specified (i.e. NULL). + + Returns a NULL pointer if the buffer can't be fetched. This can occur is + there is not sufficient memory to make a copy of the contents or the buffer + contains non-UTF-8 characters. + + # Safety + + This function will fail if the log_id pointer is invalid or does not point + to a NULL terminated string. + """ + raise NotImplementedError + + +def parse_pact_json(json: str) -> Pact: + """ + Parses the provided JSON into a Pact model. + + [Rust + `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_parse_pact_json) + + The returned Pact model must be freed with the `pactffi_pact_model_delete` + function when no longer needed. + + # Error Handling + + This function will return a NULL pointer if passed a NULL pointer or if an + error occurs. + """ + raise NotImplementedError + + +def pact_model_delete(pact: Pact) -> None: + """ + Frees the memory used by the Pact model. + + [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_model_delete) + """ + raise NotImplementedError + + +def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: + """ + Returns an iterator over all the interactions in the Pact. + + [Rust + `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_model_interaction_iterator) + + The iterator will have to be deleted using the + `pactffi_pact_interaction_iter_delete` function. The iterator will contain a + copy of the interactions, so it will not be affected but mutations to the + Pact model and will still function if the Pact model is deleted. + + # Safety This function is safe as long as the Pact pointer is a valid + pointer. + + # Errors On any error, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def pact_spec_version(pact: Pact) -> PactSpecification: + """ + Returns the Pact specification enum that the Pact is for. + + [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_spec_version) + """ + raise NotImplementedError + + +def pact_interaction_delete(interaction: PactInteraction) -> None: + """ + Frees the memory used by the Pact interaction model. + + [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_delete) + """ + raise NotImplementedError + + +def async_message_new() -> AsynchronousMessage: + """ + Get a mutable pointer to a newly-created default message on the heap. + + [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_new) + + # Safety + + This function is safe. + + # Error Handling + + Returns NULL on error. + """ + raise NotImplementedError + + +def async_message_delete(message: AsynchronousMessage) -> None: + """ + Destroy the `AsynchronousMessage` being pointed to. + + [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_delete) + """ + raise NotImplementedError + + +def async_message_get_contents(message: AsynchronousMessage) -> MessageContents: + """ + Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. + + [Rust + `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_contents) + + # Safety + + The data pointed to by the pointer this function returns will be deleted + when the message is deleted. Trying to use if after the message is deleted + will result in undefined behaviour. + + # Error Handling + + If the message is NULL, returns NULL. + """ + raise NotImplementedError + + +def async_message_get_contents_str(message: AsynchronousMessage) -> str: + """ + Get the message contents of an `AsynchronousMessage` in string form. + + [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_contents_str) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + The returned string can outlive the message. + + # Error Handling + + If the message is NULL, returns NULL. If the body of the message + is missing, then this function also returns NULL. This means there's + no mechanism to differentiate with this function call alone between + a NULL message and a missing message body. + """ + raise NotImplementedError + + +def async_message_set_contents_str( + message: AsynchronousMessage, + contents: str, + content_type: str, +) -> None: + """ + Sets the contents of the message as a string. + + [Rust + `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_set_contents_str) + + - `message` - the message to set the contents for + - `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The message contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def async_message_get_contents_length(message: AsynchronousMessage) -> int: + """ + Get the length of the contents of a `AsynchronousMessage`. + + [Rust + `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_contents_length) + + # Safety + + This function is safe. + + # Error Handling + + If the message is NULL, returns 0. If the body of the request is missing, + then this function also returns 0. + """ + raise NotImplementedError + + +def async_message_get_contents_bin(message: AsynchronousMessage) -> str: + """ + Get the contents of an `AsynchronousMessage` as bytes. + + [Rust + `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_contents_bin) + + # Safety + + The number of bytes in the buffer will be returned by + `pactffi_async_message_get_contents_length`. It is safe to use the pointer + while the message is not deleted or changed. Using the pointer after the + message is mutated or deleted may lead to undefined behaviour. + + # Error Handling + + If the message is NULL, returns NULL. If the body of the message is missing, + then this function also returns NULL. + """ + raise NotImplementedError + + +def async_message_set_contents_bin( + message: AsynchronousMessage, + contents: str, + len: int, + content_type: str, +) -> None: + """ + Sets the contents of the message as an array of bytes. + + [Rust + `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_set_contents_bin) + + * `message` - the message to set the contents for + * `contents` - pointer to contents to copy from + * `len` - number of bytes to copy from the contents pointer + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def async_message_get_description(message: AsynchronousMessage) -> str: + r""" + Get a copy of the description. + + [Rust + `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_description) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + Since it is a copy, the returned string may safely outlive the + `AsynchronousMessage`. + + # Errors + + On failure, this function will return a NULL pointer. + + This function may fail if the Rust string contains embedded null ('\0') + bytes. + """ + raise NotImplementedError + + +def async_message_set_description( + message: AsynchronousMessage, + description: str, +) -> int: + """ + Write the `description` field on the `AsynchronousMessage`. + + [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_set_description) + + # Safety + + `description` must contain valid UTF-8. Invalid UTF-8 + will be replaced with U+FFFD REPLACEMENT CHARACTER. + + This function will only reallocate if the new string + does not fit in the existing buffer. + + # Error Handling + + Errors will be reported with a non-zero return value. + """ + raise NotImplementedError + + +def async_message_get_provider_state( + message: AsynchronousMessage, + index: int, +) -> ProviderState: + r""" + Get a copy of the provider state at the given index from this message. + + [Rust + `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_provider_state) + + # Safety + + The returned structure must be deleted with `provider_state_delete`. + + Since it is a copy, the returned structure may safely outlive the + `AsynchronousMessage`. + + # Error Handling + + On failure, this function will return a variant other than Success. + + This function may fail if the index requested is out of bounds, or if any of + the Rust strings contain embedded null ('\0') bytes. + """ + raise NotImplementedError + + +def async_message_get_provider_state_iter( + message: AsynchronousMessage, +) -> ProviderStateIterator: + """ + Get an iterator over provider states. + + [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) + + # Safety + + The underlying data must not change during iteration. + + # Error Handling + + Returns NULL if an error occurs. + """ + raise NotImplementedError + + +def consumer_get_name(consumer: Consumer) -> str: + r""" + Get a copy of this consumer's name. + + [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_consumer_get_name) + + The copy must be deleted with `pactffi_string_delete`. + + # Usage + + ```c + // Assuming `file_name` and `json_str` are already defined. + + MessagePact *message_pact = pactffi_message_pact_new_from_json(file_name, json_str); + if (message_pact == NULLPTR) { + // handle error. + } + + Consumer *consumer = pactffi_message_pact_get_consumer(message_pact); + if (consumer == NULLPTR) { + // handle error. + } + + char *name = pactffi_consumer_get_name(consumer); + if (name == NULL) { + // handle error. + } + + printf("%s\n", name); + + pactffi_string_delete(name); + ``` + + # Errors + + This function will fail if it is passed a NULL pointer, + or the Rust string contains an embedded NULL byte. + In the case of error, a NULL pointer will be returned. + """ + raise NotImplementedError + + +def pact_get_consumer(pact: Pact) -> Consumer: + """ + Get the consumer from a Pact. + + This returns a copy of the consumer model, and needs to be cleaned up with + `pactffi_pact_consumer_delete` when no longer required. + + [Rust + `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_get_consumer) + + # Errors + + This function will fail if it is passed a NULL pointer. In the case of + error, a NULL pointer will be returned. + """ + raise NotImplementedError + + +def pact_consumer_delete(consumer: Consumer) -> None: + """ + Frees the memory used by the Pact consumer. + + [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_consumer_delete) + """ + raise NotImplementedError + + +def message_contents_get_contents_str(contents: MessageContents) -> str: + """ + Get the message contents in string form. + + [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_get_contents_str) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + The returned string can outlive the message. + + # Error Handling + + If the message contents is NULL, returns NULL. If the body of the message + is missing, then this function also returns NULL. This means there's + no mechanism to differentiate with this function call alone between + a NULL message and a missing message body. + """ + raise NotImplementedError + + +def message_contents_set_contents_str( + contents: MessageContents, + contents_str: str, + content_type: str, +) -> None: + """ + Sets the contents of the message as a string. + + [Rust + `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_set_contents_str) + + * `contents` - the message contents to set the contents for + * `contents_str` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The message contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents string is a NULL pointer, it will set the message contents + as null. If the content type is a null pointer, or can't be parsed, it will + set the content type as unknown. + """ + raise NotImplementedError + + +def message_contents_get_contents_length(contents: MessageContents) -> int: + """ + Get the length of the message contents. + + [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_get_contents_length) + + # Safety + + This function is safe. + + # Error Handling + + If the message is NULL, returns 0. If the body of the message + is missing, then this function also returns 0. + """ + raise NotImplementedError + + +def message_contents_get_contents_bin(contents: MessageContents) -> str: + """ + Get the contents of a message as a pointer to an array of bytes. + + [Rust + `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_get_contents_bin) + + # Safety + + The number of bytes in the buffer will be returned by + `pactffi_message_contents_get_contents_length`. It is safe to use the + pointer while the message is not deleted or changed. Using the pointer after + the message is mutated or deleted may lead to undefined behaviour. + + # Error Handling + + If the message is NULL, returns NULL. If the body of the message is missing, + then this function also returns NULL. + """ + raise NotImplementedError + + +def message_contents_set_contents_bin( + contents: MessageContents, + contents_bin: str, + len: int, + content_type: str, +) -> None: + """ + Sets the contents of the message as an array of bytes. + + [Rust + `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_set_contents_bin) + + * `message` - the message contents to set the contents for + * `contents_bin` - pointer to contents to copy from + * `len` - number of bytes to copy from the contents pointer + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def message_contents_get_metadata_iter( + contents: MessageContents, +) -> MessageMetadataIterator: + r""" + Get an iterator over the metadata of a message. + + [Rust + `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) + + The returned pointer must be deleted with + `pactffi_message_metadata_iter_delete` when done with it. + + # Safety + + This iterator carries a pointer to the message contents, and must not + outlive the message. + + The message metadata also must not be modified during iteration. If it is, + the old iterator must be deleted and a new iterator created. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. + """ + raise NotImplementedError + + +def message_contents_get_matching_rule_iter( + contents: MessageContents, + category: MatchingRuleCategory, +) -> MatchingRuleCategoryIterator: + r""" + Get an iterator over the matching rules for a category of a message. + + [Rust + `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) + + The returned pointer must be deleted with + `pactffi_matching_rules_iter_delete` when done with it. + + Note that there could be multiple matching rules for the same key, so this + iterator will sequentially return each rule with the same key. + + For sample, given the following rules: + + ``` + "$.a" => Type, + "$.b" => Regex("\\d+"), Number + ``` + + This iterator will return a sequence of 3 values: + + - `("$.a", Type)` + - `("$.b", Regex("\d+"))` + - `("$.b", Number)` + + # Safety + + The iterator contains a copy of the data, so is safe to use when the message + or message contents has been deleted. + + # Error Handling + + On failure, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def request_contents_get_matching_rule_iter( + request: HttpRequest, + category: MatchingRuleCategory, +) -> MatchingRuleCategoryIterator: + r""" + Get an iterator over the matching rules for a category of an HTTP request. + + [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) + + The returned pointer must be deleted with + `pactffi_matching_rules_iter_delete` when done with it. + + For sample, given the following rules: + + ``` + "$.a" => Type, + "$.b" => Regex("\d+"), Number + ``` + + This iterator will return a sequence of 3 values: + + - `("$.a", Type)` + - `("$.b", Regex("\d+"))` + - `("$.b", Number)` + + # Safety + + The iterator contains a copy of the data, so is safe to use when the + interaction or request contents has been deleted. + + # Error Handling + + On failure, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def response_contents_get_matching_rule_iter( + response: HttpResponse, + category: MatchingRuleCategory, +) -> MatchingRuleCategoryIterator: + r""" + Get an iterator over the matching rules for a category of an HTTP response. + + [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) + + The returned pointer must be deleted with + `pactffi_matching_rules_iter_delete` when done with it. + + For sample, given the following rules: + + ``` + "$.a" => Type, + "$.b" => Regex("\d+"), Number + ``` + + This iterator will return a sequence of 3 values: + + - `("$.a", Type)` + - `("$.b", Regex("\d+"))` + - `("$.b", Number)` + + # Safety + + The iterator contains a copy of the data, so is safe to use when the + interaction or response contents has been deleted. + + # Error Handling + + On failure, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def message_contents_get_generators_iter( + contents: MessageContents, + category: GeneratorCategory, +) -> GeneratorCategoryIterator: + """ + Get an iterator over the generators for a category of a message. + + [Rust + `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_get_generators_iter) + + The returned pointer must be deleted with `pactffi_generators_iter_delete` + when done with it. + + # Safety + + The iterator contains a copy of the data, so is safe to use when the message + or message contents has been deleted. + + # Error Handling + + On failure, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def request_contents_get_generators_iter( + request: HttpRequest, + category: GeneratorCategory, +) -> GeneratorCategoryIterator: + """ + Get an iterator over the generators for a category of an HTTP request. + + [Rust + `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_request_contents_get_generators_iter) + + The returned pointer must be deleted with `pactffi_generators_iter_delete` + when done with it. + + # Safety + + The iterator contains a copy of the data, so is safe to use when the + interaction or request contents has been deleted. + + # Error Handling + + On failure, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def response_contents_get_generators_iter( + response: HttpResponse, + category: GeneratorCategory, +) -> GeneratorCategoryIterator: + """ + Get an iterator over the generators for a category of an HTTP response. + + [Rust + `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_response_contents_get_generators_iter) + + The returned pointer must be deleted with `pactffi_generators_iter_delete` + when done with it. + + # Safety + + The iterator contains a copy of the data, so is safe to use when the + interaction or response contents has been deleted. + + # Error Handling + + On failure, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: + """ + Parse a matcher definition string into a MatchingRuleDefinition. + + The MatchingRuleDefition contains the example value, and matching rules and + any generator. + + [Rust + `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_parse_matcher_definition) + + The following are examples of matching rule definitions: + + * `matching(type,'Name')` - type matcher with string value 'Name' + * `matching(number,100)` - number matcher + * `matching(datetime, 'yyyy-MM-dd','2000-01-01')` - datetime matcher with + format string + + See [Matching Rule definition + expressions](https://docs.rs/pact_models/latest/pact_models/matchingrules/expressions/index.html). + + The returned value needs to be freed up with the + `pactffi_matcher_definition_delete` function. + + # Errors If the expression is invalid, the MatchingRuleDefinition error will + be set. You can check for this value with the + `pactffi_matcher_definition_error` function. + + # Safety + + This function is safe if the expression is a valid NULL terminated string + pointer. + """ + raise NotImplementedError + + +def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: + """ + Returns any error message from parsing a matching definition expression. + + If there is no error, it will return a NULL pointer, otherwise returns the + error message as a NULL-terminated string. The returned string must be freed + using the `pactffi_string_delete` function once done with it. + + [Rust + `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matcher_definition_error) + """ + raise NotImplementedError + + +def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: + """ + Returns the value from parsing a matching definition expression. + + If there was an error, it will return a NULL pointer, otherwise returns the + value as a NULL-terminated string. The returned string must be freed using + the `pactffi_string_delete` function once done with it. + + [Rust + `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matcher_definition_value) + + Note that different expressions values can have types other than a string. + Use `pactffi_matcher_definition_value_type` to get the actual type of the + value. This function will always return the string representation of the + value. + """ + raise NotImplementedError + + +def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: + """ + Frees the memory used by the result of parsing the matching definition expression. + + [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matcher_definition_delete) + """ + raise NotImplementedError + + +def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Generator: + """ + Returns the generator from parsing a matching definition expression. + + If there was an error or there is no associated generator, it will return a + NULL pointer, otherwise returns the generator as a pointer. + + [Rust + `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matcher_definition_generator) + + The generator pointer will be a valid pointer as long as + `pactffi_matcher_definition_delete` has not been called on the definition. + Using the generator pointer after the definition has been deleted will + result in undefined behaviour. + """ + raise NotImplementedError + + +def matcher_definition_value_type( + definition: MatchingRuleDefinitionResult, +) -> ExpressionValueType: + """ + Returns the type of the value from parsing a matching definition expression. + + If there was an error parsing the expression, it will return Unknown. + + [Rust + `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matcher_definition_value_type) + """ + raise NotImplementedError + + +def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_iter_delete) + """ + raise NotImplementedError + + +def matcher_definition_iter( + definition: MatchingRuleDefinitionResult, +) -> MatchingRuleIterator: + """ + Returns an iterator over the matching rules from the parsed definition. + + The iterator needs to be deleted with the + `pactffi_matching_rule_iter_delete` function once done with it. + + [Rust + `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matcher_definition_iter) + + If there was an error parsing the expression, this function will return a + NULL pointer. + """ + raise NotImplementedError + + +def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: + """ + Get the next matching rule or reference from the iterator. + + As the values returned are owned by the iterator, they do not need to be + deleted but will be cleaned up when the iterator is deleted. + + [Rust + `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_iter_next) + + Will return a NULL pointer when the iterator has advanced past the end of + the list. + + # Safety + + This function is safe. + + # Error Handling + + This function will return a NULL pointer if passed a NULL pointer or if an + error occurs. + """ + raise NotImplementedError + + +def matching_rule_id(rule_result: MatchingRuleResult) -> int: + """ + Return the ID of the matching rule. + + [Rust + `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_id) + + The ID corresponds to the following rules: + + | Rule | ID | + | ---- | -- | + | Equality | 1 | + | Regex | 2 | + | Type | 3 | + | MinType | 4 | + | MaxType | 5 | + | MinMaxType | 6 | + | Timestamp | 7 | + | Time | 8 | + | Date | 9 | + | Include | 10 | + | Number | 11 | + | Integer | 12 | + | Decimal | 13 | + | Null | 14 | + | ContentType | 15 | + | ArrayContains | 16 | + | Values | 17 | + | Boolean | 18 | + | StatusCode | 19 | + | NotEmpty | 20 | + | Semver | 21 | + | EachKey | 22 | + | EachValue | 23 | + + # Safety + + This function is safe as long as the MatchingRuleResult pointer is a valid + pointer and the iterator has not been deleted. + """ + raise NotImplementedError + + +def matching_rule_value(rule_result: MatchingRuleResult) -> str: + """ + Returns the associated value for the matching rule. + + If the matching rule does not have an associated value, will return a NULL + pointer. + + [Rust + `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_value) + + The associated values for the rules are: + + | Rule | ID | VALUE | + | ---- | -- | ----- | + | Equality | 1 | NULL | + | Regex | 2 | Regex value | + | Type | 3 | NULL | + | MinType | 4 | Minimum value | + | MaxType | 5 | Maximum value | + | MinMaxType | 6 | "min:max" | + | Timestamp | 7 | Format string | + | Time | 8 | Format string | + | Date | 9 | Format string | + | Include | 10 | String value | + | Number | 11 | NULL | + | Integer | 12 | NULL | + | Decimal | 13 | NULL | + | Null | 14 | NULL | + | ContentType | 15 | Content type | + | ArrayContains | 16 | NULL | + | Values | 17 | NULL | + | Boolean | 18 | NULL | + | StatusCode | 19 | NULL | + | NotEmpty | 20 | NULL | + | Semver | 21 | NULL | + | EachKey | 22 | NULL | + | EachValue | 23 | NULL | + + Will return a NULL pointer if the matching rule was a reference or does not + have an associated value. + + # Safety + + This function is safe as long as the MatchingRuleResult pointer is a valid + pointer and the iterator it came from has not been deleted. + """ + raise NotImplementedError + + +def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: + """ + Returns the matching rule pointer for the matching rule. + + Will return a NULL pointer if the matching rule result was a reference. + + [Rust + `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_pointer) + + # Safety + + This function is safe as long as the MatchingRuleResult pointer is a valid + pointer and the iterator it came from has not been deleted. + """ + raise NotImplementedError + + +def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: + """ + Return any matching rule reference to a attribute by name. + + This is when the matcher should be configured to match the type of a + structure. I.e., + + [Rust + `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_reference_name) + + ```json + { + "pact:match": "eachValue(matching($'person'))", + "person": { + "name": "Fred", + "age": 100 + } + } + ``` + + Will return a NULL pointer if the matching rule was not a reference. + + # Safety + + This function is safe as long as the MatchingRuleResult pointer is a valid + pointer and the iterator has not been deleted. + """ + raise NotImplementedError + + +def validate_datetime(value: str, format: str) -> int: + """ + Validates the date/time value against the date/time format string. + + If the value is valid, this function will return a zero status code + (EXIT_SUCCESS). If the value is not valid, will return a value of 1 + (EXIT_FAILURE) and set the error message which can be retrieved with + `pactffi_get_error_message`. + + [Rust + `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_validate_datetime) + + # Errors If the function receives a panic, it will return 2 and the message + associated with the panic can be retrieved with `pactffi_get_error_message`. + + # Safety + + This function is safe as long as the value and format parameters point to + valid NULL-terminated strings. + """ + raise NotImplementedError + + +def generator_to_json(generator: Generator) -> str: + """ + Get the JSON form of the generator. + + [Rust + `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generator_to_json) + + The returned string must be deleted with `pactffi_string_delete`. + + # Safety + + This function will fail if it is passed a NULL pointer, or the owner of the + generator has been deleted. + """ + raise NotImplementedError + + +def generator_generate_string(generator: Generator, context_json: str) -> str: + """ + Generate a string value using the provided generator. + + An optional JSON payload containing any generator context ca be given. The + context value is used for generators like `MockServerURL` (which should + contain details about the running mock server) and `ProviderStateGenerator` + (which should be the values returned from the Provider State callback + function). + + [Rust + `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generator_generate_string) + + If anything goes wrong, it will return a NULL pointer. + """ + raise NotImplementedError + + +def generator_generate_integer(generator: Generator, context_json: str) -> int: + """ + Generate an integer value using the provided generator. + + An optional JSON payload containing any generator context can be given. The + context value is used for generators like `ProviderStateGenerator` (which + should be the values returned from the Provider State callback function). + + [Rust + `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generator_generate_integer) + + If anything goes wrong or the generator is not a type that can generate an + integer value, it will return a zero value. + """ + raise NotImplementedError + + +def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generators_iter_delete) + """ + raise NotImplementedError + + +def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePair: + """ + Get the next path and generator out of the iterator, if possible. + + [Rust + `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generators_iter_next) + + The returned pointer must be deleted with + `pactffi_generator_iter_pair_delete`. + + # Safety + + The underlying data is owned by the `GeneratorKeyValuePair`, so is always + safe to use. + + # Error Handling + + If no further data is present, returns NULL. + """ + raise NotImplementedError + + +def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: + """ + Free a pair of key and value returned from `pactffi_generators_iter_next`. + + [Rust + `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generators_iter_pair_delete) + """ + raise NotImplementedError + + +def sync_http_new() -> SynchronousHttp: + """ + Get a mutable pointer to a newly-created default interaction on the heap. + + [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_new) + + # Safety + + This function is safe. + + # Error Handling + + Returns NULL on error. + """ + raise NotImplementedError + + +def sync_http_delete(interaction: SynchronousHttp) -> None: + """ + Destroy the `SynchronousHttp` interaction being pointed to. + + [Rust + `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_delete) + """ + raise NotImplementedError + + +def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: + """ + Get the request of a `SynchronousHttp` interaction. + + [Rust + `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_request) + + # Safety + + The data pointed to by the pointer this function returns will be deleted + when the interaction is deleted. Trying to use if after the interaction is + deleted will result in undefined behaviour. + + # Error Handling + + If the interaction is NULL, returns NULL. + """ + raise NotImplementedError + + +def sync_http_get_request_contents(interaction: SynchronousHttp) -> str: + """ + Get the request contents of a `SynchronousHttp` interaction in string form. + + [Rust + `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_request_contents) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + The returned string can outlive the interaction. + + # Error Handling + + If the interaction is NULL, returns NULL. If the body of the request is + missing, then this function also returns NULL. This means there's no + mechanism to differentiate with this function call alone between a NULL body + and a missing body. + """ + raise NotImplementedError + + +def sync_http_set_request_contents( + interaction: SynchronousHttp, + contents: str, + content_type: str, +) -> None: + """ + Sets the request contents of the interaction. + + [Rust + `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_set_request_contents) + + - `interaction` - the interaction to set the request contents for + - `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The request contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the request contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: + """ + Get the length of the request contents of a `SynchronousHttp` interaction. + + [Rust + `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) + + # Safety + + This function is safe. + + # Error Handling + + If the interaction is NULL, returns 0. If the body of the request is + missing, then this function also returns 0. + """ + raise NotImplementedError + + +def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes: + """ + Get the request contents of a `SynchronousHttp` interaction as bytes. + + [Rust + `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) + + # Safety + + The number of bytes in the buffer will be returned by + `pactffi_sync_http_get_request_contents_length`. It is safe to use the + pointer while the interaction is not deleted or changed. Using the pointer + after the interaction is mutated or deleted may lead to undefined behaviour. + + # Error Handling + + If the interaction is NULL, returns NULL. If the body of the request is + missing, then this function also returns NULL. + """ + raise NotImplementedError + + +def sync_http_set_request_contents_bin( + interaction: SynchronousHttp, + contents: str, + len: int, + content_type: str, +) -> None: + """ + Sets the request contents of the interaction as an array of bytes. + + [Rust + `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) + + - `interaction` - the interaction to set the request contents for + - `contents` - pointer to contents to copy from + - `len` - number of bytes to copy from the contents pointer + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the request contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: + """ + Get the response of a `SynchronousHttp` interaction. + + [Rust + `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_response) + + # Safety + + The data pointed to by the pointer this function returns will be deleted + when the interaction is deleted. Trying to use if after the interaction is + deleted will result in undefined behaviour. + + # Error Handling + + If the interaction is NULL, returns NULL. + """ + raise NotImplementedError + + +def sync_http_get_response_contents(interaction: SynchronousHttp) -> str: + """ + Get the response contents of a `SynchronousHttp` interaction in string form. + + [Rust + `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_response_contents) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + The returned string can outlive the interaction. + + # Error Handling + + If the interaction is NULL, returns NULL. + + If the body of the response is missing, then this function also returns + NULL. This means there's no mechanism to differentiate with this function + call alone between a NULL body and a missing body. + """ + raise NotImplementedError + + +def sync_http_set_response_contents( + interaction: SynchronousHttp, + contents: str, + content_type: str, +) -> None: + """ + Sets the response contents of the interaction. + + [Rust + `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_set_response_contents) + + - `interaction` - the interaction to set the response contents for + - `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The response contents and content type must either be NULL pointers, or + point to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the response contents as + null. If the content type is a null pointer, or can't be parsed, it will set + the content type as unknown. + """ + raise NotImplementedError + + +def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: + """ + Get the length of the response contents of a `SynchronousHttp` interaction. + + [Rust + `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) + + # Safety + + This function is safe. + + # Error Handling + + If the interaction is NULL or the index is not valid, returns 0. If the body + of the response is missing, then this function also returns 0. + """ + raise NotImplementedError + + +def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes: + """ + Get the response contents of a `SynchronousHttp` interaction as bytes. + + [Rust + `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) + + # Safety + + The number of bytes in the buffer will be returned by + `pactffi_sync_http_get_response_contents_length`. It is safe to use the + pointer while the interaction is not deleted or changed. Using the pointer + after the interaction is mutated or deleted may lead to undefined behaviour. + + # Error Handling + + If the interaction is NULL, returns NULL. If the body of the response is + missing, then this function also returns NULL. + """ + raise NotImplementedError + + +def sync_http_set_response_contents_bin( + interaction: SynchronousHttp, + contents: str, + len: int, + content_type: str, +) -> None: + """ + Sets the response contents of the `SynchronousHttp` interaction as bytes. + + [Rust + `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) + + - `interaction` - the interaction to set the response contents for + - `contents` - pointer to contents to copy from + - `len` - number of bytes to copy + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the response contents as + null. If the content type is a null pointer, or can't be parsed, it will set + the content type as unknown. + """ + raise NotImplementedError + + +def sync_http_get_description(interaction: SynchronousHttp) -> str: + r""" + Get a copy of the description. + + [Rust + `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_description) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + Since it is a copy, the returned string may safely outlive the + `SynchronousHttp` interaction. + + # Errors + + On failure, this function will return a NULL pointer. + + This function may fail if the Rust string contains embedded null ('\0') + bytes. + """ + raise NotImplementedError + + +def sync_http_set_description(interaction: SynchronousHttp, description: str) -> int: + """ + Write the `description` field on the `SynchronousHttp`. + + [Rust + `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_set_description) + + # Safety + + `description` must contain valid UTF-8. Invalid UTF-8 will be replaced with + U+FFFD REPLACEMENT CHARACTER. + + This function will only reallocate if the new string does not fit in the + existing buffer. + + # Error Handling + + Errors will be reported with a non-zero return value. + """ + raise NotImplementedError + + +def sync_http_get_provider_state( + interaction: SynchronousHttp, + index: int, +) -> ProviderState: + r""" + Get a copy of the provider state at the given index from this interaction. + + [Rust + `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_provider_state) + + # Safety + + The returned structure must be deleted with `provider_state_delete`. + + Since it is a copy, the returned structure may safely outlive the + `SynchronousHttp`. + + # Error Handling + + On failure, this function will return a variant other than Success. + + This function may fail if the index requested is out of bounds, or if any of + the Rust strings contain embedded null ('\0') bytes. + """ + raise NotImplementedError + + +def sync_http_get_provider_state_iter( + interaction: SynchronousHttp, +) -> ProviderStateIterator: + """ + Get an iterator over provider states. + + [Rust + `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) + + # Safety + + The underlying data must not change during iteration. + + # Error Handling + + Returns NULL if an error occurs. + """ + raise NotImplementedError + + +def pact_interaction_as_synchronous_http( + interaction: PactInteraction, +) -> SynchronousHttp: + r""" + Casts this interaction to a `SynchronousHttp` interaction. + + Returns a NULL pointer if the interaction can not be casted to a + `SynchronousHttp` interaction (for instance, it is a message interaction). + The returned pointer must be freed with `pactffi_sync_http_delete` when no + longer required. + + [Rust + `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) + + # Safety This function is safe as long as the interaction pointer is a valid + pointer. + + # Errors On any error, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def pact_interaction_as_message(interaction: PactInteraction) -> Message: + """ + Casts this interaction to a `Message` interaction. + + Returns a NULL pointer if the interaction can not be casted to a `Message` + interaction (for instance, it is a http interaction). The returned pointer + must be freed with `pactffi_message_delete` when no longer required. + + [Rust + `pactffi_pact_interaction_as_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_as_message) + + Note that if the interaction is a V4 `AsynchronousMessage`, it will be + converted to a V3 `Message` before being returned. + + # Safety This function is safe as long as the interaction pointer is a valid + pointer. + + # Errors On any error, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def pact_interaction_as_asynchronous_message( + interaction: PactInteraction, +) -> AsynchronousMessage: + """ + Casts this interaction to a `AsynchronousMessage` interaction. + + Returns a NULL pointer if the interaction can not be casted to a + `AsynchronousMessage` interaction (for instance, it is a http interaction). + The returned pointer must be freed with `pactffi_async_message_delete` when + no longer required. + + [Rust + `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) + + Note that if the interaction is a V3 `Message`, it will be converted to a V4 + `AsynchronousMessage` before being returned. + + # Safety This function is safe as long as the interaction pointer is a valid + pointer. + + # Errors On any error, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def pact_interaction_as_synchronous_message( + interaction: PactInteraction, +) -> SynchronousMessage: + """ + Casts this interaction to a `SynchronousMessage` interaction. + + Returns a NULL pointer if the interaction can not be casted to a + `SynchronousMessage` interaction (for instance, it is a http interaction). + The returned pointer must be freed with `pactffi_sync_message_delete` when + no longer required. + + [Rust + `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) + + # Safety This function is safe as long as the interaction pointer is a valid + pointer. + + # Errors On any error, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def pact_message_iter_delete(iter: PactMessageIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_message_iter_delete) + """ + raise NotImplementedError + + +def pact_message_iter_next(iter: PactMessageIterator) -> Message: + """ + Get the next message from the message pact. + + As the messages returned are owned by the iterator, they do not need to be + deleted but will be cleaned up when the iterator is deleted. + + [Rust + `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_message_iter_next) + + Will return a NULL pointer when the iterator has advanced past the end of + the list. + + # Safety + + This function is safe. + + Deleting a message returned by the iterator can lead to undefined behaviour. + + # Error Handling + + This function will return a NULL pointer if passed a NULL pointer or if an + error occurs. + """ + raise NotImplementedError + + +def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMessage: + """ + Get the next synchronous request/response message from the V4 pact. + + As the messages returned are owned by the iterator, they do not need to be + deleted but will be cleaned up when the iterator is deleted. + + [Rust + `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_sync_message_iter_next) + + Will return a NULL pointer when the iterator has advanced past the end of + the list. + + # Safety + + This function is safe. + + Deleting a message returned by the iterator can lead to undefined behaviour. + + # Error Handling + + This function will return a NULL pointer if passed a NULL pointer or if an + error occurs. + """ + raise NotImplementedError + + +def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) + """ + raise NotImplementedError + + +def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: + """ + Get the next synchronous HTTP request/response interaction from the V4 pact. + + As the interactions returned are owned by the iterator, they do not need to + be deleted but will be cleaned up when the iterator is deleted. + + [Rust + `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_sync_http_iter_next) + + Will return a NULL pointer when the iterator has advanced past the end of + the list. + + # Safety + + This function is safe. + + Deleting an interaction returned by the iterator can lead to undefined + behaviour. + + # Error Handling + + This function will return a NULL pointer if passed a NULL pointer or if an + error occurs. + """ + raise NotImplementedError + + +def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) + """ + raise NotImplementedError + + +def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction: + """ + Get the next interaction from the pact. + + As the interactions returned are owned by the iterator, they do not need to + be deleted but will be cleaned up when the iterator is deleted. + + [Rust + `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_iter_next) + + Will return a NULL pointer when the iterator has advanced past the end of + the list. + + # Safety + + This function is safe. + + Deleting an interaction returned by the iterator can lead to undefined + behaviour. + + # Error Handling + + This function will return a NULL pointer if passed a NULL pointer or if an + error occurs. + """ + raise NotImplementedError + + +def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_iter_delete) + """ + raise NotImplementedError + + +def matching_rule_to_json(rule: MatchingRule) -> str: + """ + Get the JSON form of the matching rule. + + [Rust + `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_to_json) + + The returned string must be deleted with `pactffi_string_delete`. + + # Safety + + This function will fail if it is passed a NULL pointer, or the iterator that + owns the value of the matching rule has been deleted. + """ + raise NotImplementedError + + +def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rules_iter_delete) + """ + raise NotImplementedError + + +def matching_rules_iter_next( + iter: MatchingRuleCategoryIterator, +) -> MatchingRuleKeyValuePair: + """ + Get the next path and matching rule out of the iterator, if possible. + + [Rust + `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rules_iter_next) + + The returned pointer must be deleted with + `pactffi_matching_rules_iter_pair_delete`. + + # Safety + + The underlying data is owned by the `MatchingRuleKeyValuePair`, so is always + safe to use. + + # Error Handling + + If no further data is present, returns NULL. + """ + raise NotImplementedError + + +def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: + """ + Free a pair of key and value returned from `message_metadata_iter_next`. + + [Rust + `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) + """ + raise NotImplementedError + + +def message_new() -> Message: + """ + Get a mutable pointer to a newly-created default message on the heap. + + [Rust + `pactffi_message_new`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_new) + + # Safety + + This function is safe. + + # Error Handling + + Returns NULL on error. + """ + raise NotImplementedError + + +def message_new_from_json( + index: int, + json_str: str, + spec_version: PactSpecification, +) -> Message: + """ + Constructs a `Message` from the JSON string. + + [Rust + `pactffi_message_new_from_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_new_from_json) + + # Safety + + This function is safe. + + # Error Handling + + If the JSON string is invalid or not UTF-8 encoded, returns a NULL. + """ + raise NotImplementedError + + +def message_new_from_body(body: str, content_type: str) -> Message: + """ + Constructs a `Message` from a body with a given content-type. + + [Rust + `pactffi_message_new_from_body`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_new_from_body) + + # Safety + + This function is safe. + + # Error Handling + + If the body or content type are invalid or not UTF-8 encoded, returns NULL. + """ + raise NotImplementedError + + +def message_delete(message: Message) -> None: + """ + Destroy the `Message` being pointed to. + + [Rust + `pactffi_message_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_delete) + """ + raise NotImplementedError + + +def message_get_contents(message: Message) -> str: + """ + Get the contents of a `Message` in string form. + + [Rust + `pactffi_message_get_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_contents) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + The returned string can outlive the message. + + # Error Handling + + If the message is NULL, returns NULL. If the body of the message is missing, + then this function also returns NULL. This means there's no mechanism to + differentiate with this function call alone between a NULL message and a + missing message body. + """ + raise NotImplementedError + + +def message_set_contents(message: Message, contents: str, content_type: str) -> None: + """ + Sets the contents of the message. + + [Rust + `pactffi_message_set_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_set_contents) + + # Safety + + The message contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def message_get_contents_length(message: Message) -> int: + """ + Get the length of the contents of a `Message`. + + [Rust + `pactffi_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_contents_length) + + # Safety + + This function is safe. + + # Error Handling + + If the message is NULL, returns 0. If the body of the message is missing, + then this function also returns 0. + """ + raise NotImplementedError + + +def message_get_contents_bin(message: Message) -> str: + """ + Get the contents of a `Message` as a pointer to an array of bytes. + + [Rust + `pactffi_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_contents_bin) + + # Safety + + The number of bytes in the buffer will be returned by + `pactffi_message_get_contents_length`. It is safe to use the pointer while + the message is not deleted or changed. Using the pointer after the message + is mutated or deleted may lead to undefined behaviour. + + # Error Handling + + If the message is NULL, returns NULL. If the body of the message is missing, + then this function also returns NULL. + """ + raise NotImplementedError + + +def message_set_contents_bin( + message: Message, + contents: str, + len: int, + content_type: str, +) -> None: + """ + Sets the contents of the message as an array of bytes. + + [Rust + `pactffi_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_set_contents_bin) + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def message_get_description(message: Message) -> str: + r""" + Get a copy of the description. + + [Rust + `pactffi_message_get_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_description) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + Since it is a copy, the returned string may safely outlive the `Message`. + + # Errors + + On failure, this function will return a NULL pointer. + + This function may fail if the Rust string contains embedded null ('\0') + bytes. + """ + raise NotImplementedError + + +def message_set_description(message: Message, description: str) -> int: + """ + Write the `description` field on the `Message`. + + [Rust + `pactffi_message_set_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_set_description) + + # Safety + + `description` must contain valid UTF-8. Invalid UTF-8 will be replaced with + U+FFFD REPLACEMENT CHARACTER. + + This function will only reallocate if the new string does not fit in the + existing buffer. + + # Error Handling + + Errors will be reported with a non-zero return value. + """ + raise NotImplementedError + + +def message_get_provider_state(message: Message, index: int) -> ProviderState: + r""" + Get a copy of the provider state at the given index from this message. + + [Rust + `pactffi_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_provider_state) + + # Safety + + The returned structure must be deleted with `provider_state_delete`. + + Since it is a copy, the returned structure may safely outlive the `Message`. + + # Error Handling + + On failure, this function will return a variant other than Success. + + This function may fail if the index requested is out of bounds, or if any of + the Rust strings contain embedded null ('\0') bytes. + """ + raise NotImplementedError + + +def message_get_provider_state_iter(message: Message) -> ProviderStateIterator: + """ + Get an iterator over provider states. + + [Rust + `pactffi_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_provider_state_iter) + + # Safety + + The underlying data must not change during iteration. + + # Error Handling + + Returns NULL if an error occurs. + """ + raise NotImplementedError + + +def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: + """ + Get the next value from the iterator. + + [Rust + `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_iter_next) + + # Safety + + The underlying data must not change during iteration. + + If a previous call panicked, then the internal mutex will have been poisoned + and this function will return NULL. + + # Error Handling + + Returns NULL if an error occurs. + """ + raise NotImplementedError + + +def provider_state_iter_delete(iter: ProviderStateIterator) -> None: + """ + Delete the iterator. + + [Rust + `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_iter_delete) + """ + raise NotImplementedError + + +def message_find_metadata(message: Message, key: str) -> str: + r""" + Get a copy of the metadata value indexed by `key`. + + [Rust + `pactffi_message_find_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_find_metadata) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + Since it is a copy, the returned string may safely outlive the `Message`. + + The returned pointer will be NULL if the metadata does not contain the given + key, or if an error occurred. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if the provided `key` string contains invalid UTF-8, + or if the Rust string contains embedded null ('\0') bytes. + """ + raise NotImplementedError + + +def message_insert_metadata(message: Message, key: str, value: str) -> int: + r""" + Insert the (`key`, `value`) pair into this Message's `metadata` HashMap. + + [Rust + `pactffi_message_insert_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_insert_metadata) + + # Safety + + This function returns an enum indicating the result; see the comments on + HashMapInsertStatus for details. + + # Error Handling + + This function may fail if the provided `key` or `value` strings contain + invalid UTF-8. + """ + raise NotImplementedError + + +def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadataPair: + """ + Get the next key and value out of the iterator, if possible. + + [Rust + `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_metadata_iter_next) + + The returned pointer must be deleted with + `pactffi_message_metadata_pair_delete`. + + # Safety + + The underlying data must not change during iteration. + + # Error Handling + + If no further data is present, returns NULL. + """ + raise NotImplementedError + + +def message_get_metadata_iter(message: Message) -> MessageMetadataIterator: + r""" + Get an iterator over the metadata of a message. + + [Rust + `pactffi_message_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_metadata_iter) + + # Safety + + This iterator carries a pointer to the message, and must not outlive the + message. + + The message metadata also must not be modified during iteration. If it is, + the old iterator must be deleted and a new iterator created. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. + """ + raise NotImplementedError + + +def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: + """ + Free the metadata iterator when you're done using it. + + [Rust + `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_metadata_iter_delete) + """ + raise NotImplementedError + + +def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: + """ + Free a pair of key and value returned from `message_metadata_iter_next`. + + [Rust + `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_metadata_pair_delete) + """ + raise NotImplementedError + + +def message_pact_new_from_json(file_name: str, json_str: str) -> MessagePact: + """ + Construct a new `MessagePact` from the JSON string. + + The provided file name is used when generating error messages. + + [Rust + `pactffi_message_pact_new_from_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_new_from_json) + + # Safety + + The `file_name` and `json_str` parameters must both be valid UTF-8 encoded + strings. + + # Error Handling + + On error, this function will return a null pointer. + """ + raise NotImplementedError + + +def message_pact_delete(message_pact: MessagePact) -> None: + """ + Delete the `MessagePact` being pointed to. + + [Rust + `pactffi_message_pact_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_delete) + """ + raise NotImplementedError + + +def message_pact_get_consumer(message_pact: MessagePact) -> Consumer: + """ + Get a pointer to the Consumer struct inside the MessagePact. + + This is a mutable borrow: The caller may mutate the Consumer through this + pointer. + + [Rust + `pactffi_message_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_get_consumer) + + # Safety + + This function is safe. + + # Error Handling + + This function will only fail if it is passed a NULL pointer. In the case of + error, a NULL pointer will be returned. + """ + raise NotImplementedError + + +def message_pact_get_provider(message_pact: MessagePact) -> Provider: + """ + Get a pointer to the Provider struct inside the MessagePact. + + This is a mutable borrow: The caller may mutate the Provider through this + pointer. + + [Rust + `pactffi_message_pact_get_provider`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_get_provider) + + # Safety + + This function is safe. + + # Error Handling + + This function will only fail if it is passed a NULL pointer. In the case of + error, a NULL pointer will be returned. + """ + raise NotImplementedError + + +def message_pact_get_message_iter( + message_pact: MessagePact, +) -> MessagePactMessageIterator: + r""" + Get an iterator over the messages of a message pact. + + [Rust + `pactffi_message_pact_get_message_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_get_message_iter) + + # Safety + + This iterator carries a pointer to the message pact, and must not outlive + the message pact. + + The message pact messages also must not be modified during iteration. If + they are, the old iterator must be deleted and a new iterator created. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. + """ + raise NotImplementedError + + +def message_pact_message_iter_next(iter: MessagePactMessageIterator) -> Message: + """ + Get the next message from the message pact. + + [Rust + `pactffi_message_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_message_iter_next) + + # Safety + + This function is safe. + + # Error Handling + + This function will return a NULL pointer if passed a NULL pointer or if an + error occurs. + """ + raise NotImplementedError + + +def message_pact_message_iter_delete(iter: MessagePactMessageIterator) -> None: + """ + Delete the iterator. + + [Rust + `pactffi_message_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_message_iter_delete) + """ + raise NotImplementedError + + +def message_pact_find_metadata(message_pact: MessagePact, key1: str, key2: str) -> str: + r""" + Get a copy of the metadata value indexed by `key1` and `key2`. + + [Rust + `pactffi_message_pact_find_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_find_metadata) + + # Safety + + Since it is a copy, the returned string may safely outlive the `Message`. + + The returned string must be deleted with `pactffi_string_delete`. + + The returned pointer will be NULL if the metadata does not contain the given + key, or if an error occurred. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if the provided `key1` or `key2` strings contains + invalid UTF-8, or if the Rust string contains embedded null ('\0') bytes. + """ + raise NotImplementedError + + +def message_pact_get_metadata_iter( + message_pact: MessagePact, +) -> MessagePactMetadataIterator: + r""" + Get an iterator over the metadata of a message pact. + + [Rust + `pactffi_message_pact_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_get_metadata_iter) + + # Safety + + This iterator carries a pointer to the message pact, and must not outlive + the message pact. + + The message pact metadata also must not be modified during iteration. If it + is, the old iterator must be deleted and a new iterator created. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. + """ + raise NotImplementedError + + +def message_pact_metadata_iter_next( + iter: MessagePactMetadataIterator, +) -> MessagePactMetadataTriple: + """ + Get the next triple out of the iterator, if possible. + + [Rust + `pactffi_message_pact_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_metadata_iter_next) + + # Safety + + This operation is invalid if the underlying data has been changed during + iteration. + + # Error Handling + + Returns null if no next element is present. + """ + raise NotImplementedError + + +def message_pact_metadata_iter_delete(iter: MessagePactMetadataIterator) -> None: + """ + Free the metadata iterator when you're done using it. + + [Rust `pactffi_message_pact_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_metadata_iter_delete) + """ + raise NotImplementedError + + +def message_pact_metadata_triple_delete(triple: MessagePactMetadataTriple) -> None: + """ + Free a triple returned from `pactffi_message_pact_metadata_iter_next`. + + [Rust `pactffi_message_pact_metadata_triple_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_metadata_triple_delete) + """ + raise NotImplementedError + + +def provider_get_name(provider: Provider) -> str: + r""" + Get a copy of this provider's name. + + [Rust + `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_get_name) + + The copy must be deleted with `pactffi_string_delete`. + + # Usage + + ```c + // Assuming `file_name` and `json_str` are already defined. + + MessagePact *message_pact = pactffi_message_pact_new_from_json(file_name, json_str); + if (message_pact == NULLPTR) { + // handle error. + } + + Provider *provider = pactffi_message_pact_get_provider(message_pact); + if (provider == NULLPTR) { + // handle error. + } + + char *name = pactffi_provider_get_name(provider); + if (name == NULL) { + // handle error. + } + + printf("%s\n", name); + + pactffi_string_delete(name); + ``` + + # Errors + + This function will fail if it is passed a NULL pointer, or the Rust string + contains an embedded NULL byte. In the case of error, a NULL pointer will be + returned. + """ + raise NotImplementedError + + +def pact_get_provider(pact: Pact) -> Provider: + """ + Get the provider from a Pact. + + This returns a copy of the provider model, and needs to be cleaned up with + `pactffi_pact_provider_delete` when no longer required. + + [Rust + `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_get_provider) + + # Errors + + This function will fail if it is passed a NULL pointer. In the case of + error, a NULL pointer will be returned. + """ + raise NotImplementedError + + +def pact_provider_delete(provider: Provider) -> None: + """ + Frees the memory used by the Pact provider. + + [Rust + `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_provider_delete) + """ + raise NotImplementedError + + +def provider_state_get_name(provider_state: ProviderState) -> str: + """ + Get the name of the provider state as a string. + + This needs to be deleted with `pactffi_string_delete`. + + [Rust + `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_get_name) + + # Safety + + This function is safe. + + # Error Handling + + If the provider_state param is NULL, this returns NULL. + """ + raise NotImplementedError + + +def provider_state_get_param_iter( + provider_state: ProviderState, +) -> ProviderStateParamIterator: + r""" + Get an iterator over the params of a provider state. + + [Rust + `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_get_param_iter) + + # Safety + + This iterator carries a pointer to the provider state, and must not outlive + the provider state. + + The provider state params also must not be modified during iteration. If it + is, the old iterator must be deleted and a new iterator created. + + # Errors + + On failure, this function will return a NULL pointer. + + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. + """ + raise NotImplementedError + + +def provider_state_param_iter_next( + iter: ProviderStateParamIterator, +) -> ProviderStateParamPair: + """ + Get the next key and value out of the iterator, if possible. + + [Rust + `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_param_iter_next) + + Returns a pointer to a heap allocated array of 2 elements, the pointer to + the key string on the heap, and the pointer to the value string on the heap. + + # Safety + + The underlying data must not be modified during iteration. + + The user needs to free both the contained strings and the array. + + # Error Handling + + Returns NULL if there's no further elements or the iterator is NULL. + """ + raise NotImplementedError + + +def provider_state_delete(provider_state: ProviderState) -> None: + """ + Free the provider state when you're done using it. + + [Rust + `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_delete) + """ + raise NotImplementedError + + +def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: + """ + Free the provider state param iterator when you're done using it. + + [Rust + `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_param_iter_delete) + """ + raise NotImplementedError + + +def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: + """ + Free a pair of key and value. + + [Rust + `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_param_pair_delete) + """ + raise NotImplementedError + + +def sync_message_new() -> SynchronousMessage: + """ + Get a mutable pointer to a newly-created default message on the heap. + + [Rust + `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_new) + + # Safety + + This function is safe. + + # Error Handling + + Returns NULL on error. + """ + raise NotImplementedError + + +def sync_message_delete(message: SynchronousMessage) -> None: + """ + Destroy the `Message` being pointed to. + + [Rust + `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_delete) + """ + raise NotImplementedError + + +def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: + """ + Get the request contents of a `SynchronousMessage` in string form. + + [Rust + `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + The returned string can outlive the message. + + # Error Handling + + If the message is NULL, returns NULL. If the body of the request message is + missing, then this function also returns NULL. This means there's no + mechanism to differentiate with this function call alone between a NULL + message and a missing message body. + """ + raise NotImplementedError + + +def sync_message_set_request_contents_str( + message: SynchronousMessage, + contents: str, + content_type: str, +) -> None: + """ + Sets the request contents of the message. + + [Rust + `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) + + - `message` - the message to set the request contents for + - `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The message contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def sync_message_get_request_contents_length(message: SynchronousMessage) -> int: + """ + Get the length of the request contents of a `SynchronousMessage`. + + [Rust + `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) + + # Safety + + This function is safe. + + # Error Handling + + If the message is NULL, returns 0. If the body of the request is missing, + then this function also returns 0. + """ + raise NotImplementedError + + +def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes: + """ + Get the request contents of a `SynchronousMessage` as a bytes. + + [Rust + `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) + + # Safety + + The number of bytes in the buffer will be returned by + `pactffi_sync_message_get_request_contents_length`. It is safe to use the + pointer while the message is not deleted or changed. Using the pointer after + the message is mutated or deleted may lead to undefined behaviour. + + # Error Handling + + If the message is NULL, returns NULL. If the body of the message is missing, + then this function also returns NULL. + """ + raise NotImplementedError + + +def sync_message_set_request_contents_bin( + message: SynchronousMessage, + contents: str, + len: int, + content_type: str, +) -> None: + """ + Sets the request contents of the message as an array of bytes. + + [Rust + `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) + + * `message` - the message to set the request contents for + * `contents` - pointer to contents to copy from + * `len` - number of bytes to copy from the contents pointer + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def sync_message_get_request_contents(message: SynchronousMessage) -> MessageContents: + """ + Get the request contents of an `SynchronousMessage` as a `MessageContents`. + + [Rust + `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_request_contents) + + # Safety + + The data pointed to by the pointer this function returns will be deleted + when the message is deleted. Trying to use if after the message is deleted + will result in undefined behaviour. + + # Error Handling + + If the message is NULL, returns NULL. + """ + raise NotImplementedError + + +def sync_message_get_number_responses(message: SynchronousMessage) -> int: + """ + Get the number of response messages in the `SynchronousMessage`. + + [Rust + `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_number_responses) + + # Safety + + The message pointer must point to a valid SynchronousMessage. + + # Error Handling + + If the message is NULL, returns 0. + """ + raise NotImplementedError + + +def sync_message_get_response_contents_str( + message: SynchronousMessage, + index: int, +) -> str: + """ + Get the response contents of a `SynchronousMessage` in string form. + + [Rust + `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + The returned string can outlive the message. + + # Error Handling + + If the message is NULL or the index is not valid, returns NULL. + + If the body of the response message is missing, then this function also + returns NULL. This means there's no mechanism to differentiate with this + function call alone between a NULL message and a missing message body. + """ + raise NotImplementedError + + +def sync_message_set_response_contents_str( + message: SynchronousMessage, + index: int, + contents: str, + content_type: str, +) -> None: + """ + Sets the response contents of the message as a string. + + If index is greater + than the number of responses in the message, the responses will be padded + with default values. + + [Rust + `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) + + * `message` - the message to set the response contents for + * `index` - index of the response to set. 0 is the first response. + * `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The message contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the response contents as + null. If the content type is a null pointer, or can't be parsed, it will set + the content type as unknown. + """ + raise NotImplementedError + + +def sync_message_get_response_contents_length( + message: SynchronousMessage, + index: int, +) -> int: + """ + Get the length of the response contents of a `SynchronousMessage`. + + [Rust + `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) + + # Safety + + This function is safe. + + # Error Handling + + If the message is NULL or the index is not valid, returns 0. If the body of + the request is missing, then this function also returns 0. + """ + raise NotImplementedError + + +def sync_message_get_response_contents_bin( + message: SynchronousMessage, + index: int, +) -> bytes: + """ + Get the response contents of a `SynchronousMessage` as bytes. + + [Rust + `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) + + # Safety + + The number of bytes in the buffer will be returned by + `pactffi_sync_message_get_response_contents_length`. It is safe to use the + pointer while the message is not deleted or changed. Using the pointer after + the message is mutated or deleted may lead to undefined behaviour. + + # Error Handling + + If the message is NULL or the index is not valid, returns NULL. If the body + of the message is missing, then this function also returns NULL. + """ + raise NotImplementedError + + +def sync_message_set_response_contents_bin( + message: SynchronousMessage, + index: int, + contents: str, + len: int, + content_type: str, +) -> None: + """ + Sets the response contents of the message at the given index as bytes. + + If index is greater than the number of responses in the message, the + responses will be padded with default values. + + [Rust + `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) + + * `message` - the message to set the response contents for + * `index` - index of the response to set. 0 is the first response + * `contents` - pointer to contents to copy from + * `len` - number of bytes to copy + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def sync_message_get_response_contents( + message: SynchronousMessage, + index: int, +) -> MessageContents: + """ + Get the response contents of an `SynchronousMessage` as a `MessageContents`. + + [Rust + `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_response_contents) + + # Safety + + The data pointed to by the pointer this function returns will be deleted + when the message is deleted. Trying to use if after the message is deleted + will result in undefined behaviour. + + # Error Handling + + If the message is NULL or the index is not valid, returns NULL. + """ + raise NotImplementedError + + +def sync_message_get_description(message: SynchronousMessage) -> str: + r""" + Get a copy of the description. + + [Rust + `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_description) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + Since it is a copy, the returned string may safely outlive the + `SynchronousMessage`. + + # Errors + + On failure, this function will return a NULL pointer. + + This function may fail if the Rust string contains embedded null ('\0') + bytes. + """ + raise NotImplementedError + + +def sync_message_set_description(message: SynchronousMessage, description: str) -> int: + """ + Write the `description` field on the `SynchronousMessage`. + + [Rust + `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_set_description) + + # Safety + + `description` must contain valid UTF-8. Invalid UTF-8 will be replaced with + U+FFFD REPLACEMENT CHARACTER. + + This function will only reallocate if the new string does not fit in the + existing buffer. + + # Error Handling + + Errors will be reported with a non-zero return value. + """ + raise NotImplementedError + + +def sync_message_get_provider_state( + message: SynchronousMessage, + index: int, +) -> ProviderState: + r""" + Get a copy of the provider state at the given index from this message. + + [Rust + `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_provider_state) + + # Safety + + The returned structure must be deleted with `provider_state_delete`. + + Since it is a copy, the returned structure may safely outlive the + `SynchronousMessage`. + + # Error Handling + + On failure, this function will return a variant other than Success. + + This function may fail if the index requested is out of bounds, or if any of + the Rust strings contain embedded null ('\0') bytes. + """ + raise NotImplementedError + + +def sync_message_get_provider_state_iter( + message: SynchronousMessage, +) -> ProviderStateIterator: + """ + Get an iterator over provider states. + + [Rust + `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) + + # Safety + + The underlying data must not change during iteration. + + # Error Handling + + Returns NULL if an error occurs. + """ + raise NotImplementedError + + +def string_delete(string: str) -> None: + """ + Delete a string previously returned by this FFI. + + [Rust + `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_string_delete) + + It is explicitly allowed to pass a null pointer to this function; in that + case the function will do nothing. + + # Safety Passing an invalid pointer, or one that was not returned by a FFI + function can result in undefined behaviour. + """ + raise NotImplementedError + + +def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: + """ + [DEPRECATED] External interface to create a HTTP mock server. + + A pointer to the pact JSON as a NULL-terminated C string is passed in, as + well as the port for the mock server to run on. A value of 0 for the port + will result in a port being allocated by the operating system. The port of + the mock server is returned. + + [Rust + `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_create_mock_server) + + * `pact_str` - Pact JSON + * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:80) + * `tls` - boolean flag to indicate of the mock server should use TLS (using + a self-signed certificate) + + This function is deprecated and replaced with + `pactffi_create_mock_server_for_transport`. + + # Errors + + Errors are returned as negative values. + + | Error | Description | + |-------|-------------| + | -1 | A null pointer was received | + | -2 | The pact JSON could not be parsed | + | -3 | The mock server could not be started | + | -4 | The method panicked | + | -5 | The address is not valid | + | -6 | Could not create the TLS configuration with the self-signed certificate | + """ + warnings.warn( + "This function is deprecated, use create_mock_server_for_transport instead", + DeprecationWarning, + stacklevel=2, + ) + raise NotImplementedError + + +def get_tls_ca_certificate() -> str: + """ + Fetch the CA Certificate used to generate the self-signed certificate. + + [Rust + `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_get_tls_ca_certificate) + + **NOTE:** The string for the result is allocated on the heap, and will have + to be freed by the caller using pactffi_string_delete. + + # Errors + + An empty string indicates an error reading the pem file. + """ + raise NotImplementedError + + +def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) -> int: + """ + [DEPRECATED] External interface to create a HTTP mock server. + + A Pact handle is passed in, as well as the port for the mock server to run + on. A value of 0 for the port will result in a port being allocated by the + operating system. The port of the mock server is returned. + + [Rust + `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_create_mock_server_for_pact) + + * `pact` - Handle to a Pact model created with created with + `pactffi_new_pact`. + * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:0). + Must be a valid UTF-8 NULL-terminated string. + * `tls` - boolean flag to indicate of the mock server should use TLS (using + a self-signed certificate) + + This function is deprecated and replaced with + `pactffi_create_mock_server_for_transport`. + + # Errors + + Errors are returned as negative values. + + | Error | Description | + |-------|-------------| + | -1 | An invalid handle was received. Handles should be created with `pactffi_new_pact` | + | -3 | The mock server could not be started | + | -4 | The method panicked | + | -5 | The address is not valid | + | -6 | Could not create the TLS configuration with the self-signed certificate | + """ # noqa: E501 + warnings.warn( + "This function is deprecated, use create_mock_server_for_transport instead", + DeprecationWarning, + stacklevel=2, + ) + raise NotImplementedError + + +def create_mock_server_for_transport( + pact: PactHandle, + addr: str, + port: int, + transport: str, + transport_config: str, +) -> int: + """ + Create a mock server for the provided Pact handle and transport. + + If the transport is not provided (it is a NULL pointer or an empty string), + will default to an HTTP transport. The address is the interface bind to, and + will default to the loopback adapter if not specified. Specifying a value of + zero for the port will result in the operating system allocating the port. + + [Rust + `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_create_mock_server_for_transport) + + Parameters: + + * `pact` - Handle to a Pact model created with created with + `pactffi_new_pact`. + * `addr` - Address to bind to (i.e. `127.0.0.1` or `[::1]`). Must be a valid + UTF-8 NULL-terminated string, or NULL or empty, in which case the loopback + adapter is used. + * `port` - Port number to bind to. A value of zero will result in the + operating system allocating an available port. + * `transport` - The transport to use (i.e. http, https, grpc). Must be a + valid UTF-8 NULL-terminated string, or NULL or empty, in which case http + will be used. + * `transport_config` - (OPTIONAL) Configuration for the transport as a valid + JSON string. Set to NULL or empty if not required. + + The port of the mock server is returned. + + # Safety NULL pointers or empty strings can be passed in for the address, + transport and transport_config, in which case a default value will be used. + Passing in an invalid pointer will result in undefined behaviour. + + # Errors + + Errors are returned as negative values. + + | Error | Description | + |-------|-------------| + | -1 | An invalid handle was received. Handles should be created with `pactffi_new_pact` | + | -2 | transport_config is not valid JSON | + | -3 | The mock server could not be started | + | -4 | The method panicked | + | -5 | The address is not valid | + + """ # noqa: E501 + raise NotImplementedError + + +def mock_server_matched(mock_server_port: int) -> bool: + """ + External interface to check if a mock server has matched all its requests. + + The port number is passed in, and if all requests have been matched, true is + returned. False is returned if there is no mock server on the given port, or + if any request has not been successfully matched, or the method panics. + + [Rust + `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mock_server_matched) + """ + raise NotImplementedError + + +def mock_server_mismatches(mock_server_port: int) -> str: + """ + External interface to get all the mismatches from a mock server. + + The port number of the mock server is passed in, and a pointer to a C string + with the mismatches in JSON format is returned. + + [Rust + `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mock_server_mismatches) + + **NOTE:** The JSON string for the result is allocated on the heap, and will + have to be freed once the code using the mock server is complete. The + [`cleanup_mock_server`](fn.cleanup_mock_server.html) function is provided + for this purpose. + + # Errors + + If there is no mock server with the provided port number, or the function + panics, a NULL pointer will be returned. Don't try to dereference it, it + will not end well for you. + """ + raise NotImplementedError + + +def cleanup_mock_server(mock_server_port: int) -> bool: + """ + External interface to cleanup a mock server. + + This function will try terminate the mock server with the given port number + and cleanup any memory allocated for it. Returns true, unless a mock server + with the given port number does not exist, or the function panics. + + [Rust + `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_cleanup_mock_server) + """ + raise NotImplementedError + + +def write_pact_file(mock_server_port: int, directory: str, *, overwrite: bool) -> int: + """ + External interface to trigger a mock server to write out its pact file. + + This function should be called if all the consumer tests have passed. The + directory to write the file to is passed as the second parameter. If a NULL + pointer is passed, the current working directory is used. + + [Rust + `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_write_pact_file) + + If overwrite is true, the file will be overwritten with the contents of the + current pact. Otherwise, it will be merged with any existing pact file. + + Returns 0 if the pact file was successfully written. Returns a positive code + if the file can not be written, or there is no mock server running on that + port or the function panics. + + # Errors + + Errors are returned as positive values. + + | Error | Description | + |-------|-------------| + | 1 | A general panic was caught | + | 2 | The pact file was not able to be written | + | 3 | A mock server with the provided port was not found | + """ + raise NotImplementedError + + +def mock_server_logs(mock_server_port: int) -> str: + """ + Fetch the logs for the mock server. + + This needs the memory buffer log sink to be setup before the mock server is + started. Returned string will be freed with the `cleanup_mock_server` + function call. + + [Rust + `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mock_server_logs) + + Will return a NULL pointer if the logs for the mock server can not be + retrieved. + """ + raise NotImplementedError + + +def generate_datetime_string(format: str) -> StringResult: + """ + Generates a datetime value from the provided format string. + + This uses the current system date and time NOTE: The memory for the returned + string needs to be freed with the `pactffi_string_delete` function + + [Rust + `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generate_datetime_string) + + # Safety + + If the format string pointer is NULL or has invalid UTF-8 characters, an + error result will be returned. If the format string pointer is not a valid + pointer or is not a NULL-terminated string, this will lead to undefined + behaviour. + """ + raise NotImplementedError + + +def check_regex(regex: str, example: str) -> bool: + """ + Checks that the example string matches the given regex. + + [Rust + `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_check_regex) + + # Safety + + Both the regex and example pointers must be valid pointers to + NULL-terminated strings. Invalid pointers will result in undefined + behaviour. + """ + raise NotImplementedError + + +def generate_regex_value(regex: str) -> StringResult: + """ + Generates an example string based on the provided regex. + + NOTE: The memory for the returned string needs to be freed with the + `pactffi_string_delete` function. + + [Rust + `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generate_regex_value) + + # Safety + + The regex pointer must be a valid pointer to a NULL-terminated string. + Invalid pointers will result in undefined behaviour. + """ + raise NotImplementedError + + +def free_string(s: str) -> None: + """ + [DEPRECATED] Frees the memory allocated to a string by another function. + + [Rust + `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_free_string) + + This function is deprecated. Use `pactffi_string_delete` instead. + + # Safety + + The string pointer can be NULL (which is a no-op), but if it is not a valid + pointer the call will result in undefined behaviour. + """ + warnings.warn( + "This function is deprecated, use string_delete instead", + DeprecationWarning, + stacklevel=2, + ) + raise NotImplementedError + + +def new_pact(consumer_name: str, provider_name: str) -> PactHandle: + """ + Creates a new Pact model and returns a handle to it. + + [Rust + `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_pact) + + * `consumer_name` - The name of the consumer for the pact. + * `provider_name` - The name of the provider for the pact. + + Returns a new `PactHandle`. The handle will need to be freed with the + `pactffi_free_pact_handle` method to release its resources. + """ + raise NotImplementedError + + +def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: + """ + Creates a new HTTP Interaction and returns a handle to it. + + [Rust + `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_interaction) + + * `description` - The interaction description. It needs to be unique for + each interaction. + + Returns a new `InteractionHandle`. + """ + raise NotImplementedError + + +def new_message_interaction(pact: PactHandle, description: str) -> InteractionHandle: + """ + Creates a new message interaction and return a handle to it. + + * `description` - The interaction description. It needs to be unique for + each interaction. + + [Rust + `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_message_interaction) + + Returns a new `InteractionHandle`. + """ + raise NotImplementedError + + +def new_sync_message_interaction( + pact: PactHandle, + description: str, +) -> InteractionHandle: + """ + Creates a new synchronous message interaction and return a handle to it. + + * `description` - The interaction description. It needs to be unique for + each interaction. + + [Rust + `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_sync_message_interaction) + + Returns a new `InteractionHandle`. + """ + raise NotImplementedError + + +def upon_receiving(interaction: InteractionHandle, description: str) -> bool: + """ + Sets the description for the Interaction. + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) + + [Rust + `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_upon_receiving) + + * `description` - The interaction description. It needs to be unique for + each interaction. + """ + raise NotImplementedError + + +def given(interaction: InteractionHandle, description: str) -> bool: + """ + Adds a provider state to the Interaction. + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) + + [Rust + `pactffi_given`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_given) + + * `description` - The provider state description. It needs to be unique. + """ + raise NotImplementedError + + +def interaction_test_name(interaction: InteractionHandle, test_name: str) -> int: + """ + Sets the test name annotation for the interaction. + + This allows capturing the name of the test as metadata. This can only be + used with V4 interactions. + + [Rust + `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_interaction_test_name) + + # Safety + + The test name parameter must be a valid pointer to a NULL terminated string. + + # Error Handling + + If the test name can not be set, this will return a positive value. + + * `1` - Function panicked. Error message will be available by calling + `pactffi_get_error_message`. + * `2` - Handle was not valid. + * `3` - Mock server was already started and the integration can not be + modified. + * `4` - Not a V4 interaction. + """ + raise NotImplementedError + + +def given_with_param( + interaction: InteractionHandle, + description: str, + name: str, + value: str, +) -> bool: + """ + Adds a parameter key and value to a provider state to the Interaction. + + If the provider state does not exist, a new one will be created, otherwise + the parameter will be merged into the existing one. The parameter value will + be parsed as JSON. + + [Rust + `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_given_with_param) + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started). + + # Parameters + + * `description` - The provider state description. It needs to be unique. + * `name` - Parameter name. + * `value` - Parameter value as JSON. + """ + raise NotImplementedError + + +def given_with_params( + interaction: InteractionHandle, + description: str, + params: str, +) -> int: + """ + Adds a provider state to the Interaction. + + If the params is not an JSON object, it will add it as a single parameter + with a `value` key. + + [Rust + `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_given_with_params) + + # Parameters + + * `description` - The provider state description. + * `params` - Parameter values as a JSON fragment. + + # Errors + + Returns EXIT_FAILURE (1) if the interaction or Pact can't be modified (i.e. + the mock server for it has already started). + + Returns 2 and sets the error message (which can be retrieved with + `pactffi_get_error_message`) if the parameter values con't be parsed as + JSON. + + Returns 3 if any of the C strings are not valid. + + """ + raise NotImplementedError + + +def with_request(interaction: InteractionHandle, method: str, path: str) -> bool: + r""" + Configures the request for the Interaction. + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) + + [Rust + `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_request) + + * `method` - The request method. Defaults to GET. + * `path` - The request path. Defaults to `/`. + + To include matching rules for the path (only regex really makes sense to + use), include the matching rule JSON format with the value as a single JSON + document. I.e. + + ```c + const char* value = "{\"value\":\"/path/to/100\", \"pact:matcher:type\":\"regex\", \"regex\":\"\\/path\\/to\\/\\\\d+\"}"; + pactffi_with_request(handle, "GET", value); + ``` + See + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md) + """ # noqa: E501 + raise NotImplementedError + + +def with_query_parameter( + interaction: InteractionHandle, + name: str, + index: int, + value: str, +) -> bool: + """ + Configures a query parameter for the Interaction. + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) + + [Rust + `pactffi_with_query_parameter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_query_parameter) + + * `name` - the query parameter name. + * `value` - the query parameter value. + * `index` - the index of the value (starts at 0). You can use this to create + a query parameter with multiple values + + **DEPRECATED:** Use `pactffi_with_query_parameter_v2`, which deals with + multiple values correctly + """ + warnings.warn( + "This function is deprecated, use with_query_parameter_v2 instead", + DeprecationWarning, + stacklevel=2, + ) + raise NotImplementedError + + +def with_query_parameter_v2( + interaction: InteractionHandle, + name: str, + index: int, + value: str, +) -> bool: + r""" + Configures a query parameter for the Interaction. + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) + + [Rust + `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_query_parameter_v2) + + * `name` - the query parameter name. + * `value` - the query parameter value. Either a simple string or a JSON + document. + * `index` - the index of the value (starts at 0). You can use this to create + a query parameter with multiple values + + To setup a query parameter with multiple values, you can either call this + function multiple times with a different index value, i.e. to create + `id=2&id=3` + + ```c + pactffi_with_query_parameter_v2(handle, "id", 0, "2"); + pactffi_with_query_parameter_v2(handle, "id", 1, "3"); + ``` + + Or you can call it once with a JSON value that contains multiple values: + + ```c + const char* value = "{\"value\": [\"2\",\"3\"]}"; + pactffi_with_query_parameter_v2(handle, "id", 0, value); + ``` + + To include matching rules for the query parameter, include the matching rule + JSON format with the value as a single JSON document. I.e. + + ```c + const char* value = "{\"value\":\"2\", \"pact:matcher:type\":\"regex\", \"regex\":\"\\\\d+\"}"; + pactffi_with_query_parameter_v2(handle, "id", 0, value); + ``` + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md) + + # Safety + + The name and value parameters must be valid pointers to NULL terminated strings. + ``` + """ # noqa: E501 + raise NotImplementedError + + +def with_specification(pact: PactHandle, version: PactSpecification) -> bool: + """ + Sets the specification version for a given Pact model. + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) or the version is invalid. + + [Rust + `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_specification) + + * `pact` - Handle to a Pact model + * `version` - the spec version to use + """ + raise NotImplementedError + + +def with_pact_metadata( + pact: PactHandle, + namespace_: str, + name: str, + value: str, +) -> bool: + """ + Sets the additional metadata on the Pact file. + + Common uses are to add the client library details such as the name and + version Returns false if the interaction or Pact can't be modified (i.e. the + mock server for it has already started) + + [Rust + `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_pact_metadata) + + * `pact` - Handle to a Pact model + * `namespace` - the top level metadat key to set any key values on + * `name` - the key to set + * `value` - the value to set + """ + raise NotImplementedError + + +def with_header( + interaction: InteractionHandle, + part: InteractionPart, + name: str, + index: int, + value: str, +) -> bool: + """ + Configures a header for the Interaction. + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) + + [Rust + `pactffi_with_header`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_header) + + * `part` - The part of the interaction to add the header to (Request or + Response). + * `name` - the header name. + * `value` - the header value. + * `index` - the index of the value (starts at 0). You can use this to create + a header with multiple values + + **DEPRECATED:** Use `pactffi_with_header_v2`, which deals with multiple + values correctly + """ + warnings.warn( + "This function is deprecated, use with_header_v2 instead", + DeprecationWarning, + stacklevel=2, + ) + raise NotImplementedError + + +def with_header_v2( + interaction: InteractionHandle, + part: InteractionPart, + name: str, + index: int, + value: str, +) -> bool: + r""" + Configures a header for the Interaction. + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) + + [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_header_v2) + + * `part` - The part of the interaction to add the header to (Request or + Response). + * `name` - the header name. + * `value` - the header value. + * `index` - the index of the value (starts at 0). You can use this to create + a header with multiple values + + To setup a header with multiple values, you can either call this function + multiple times with a different index value, i.e. to create `x-id=2, 3` + + ```c + pactffi_with_header_v2(handle, InteractionPart::Request, "x-id", 0, "2"); + pactffi_with_header_v2(handle, InteractionPart::Request, "x-id", 1, "3"); + ``` + + Or you can call it once with a JSON value that contains multiple values: + + ```c + const char* value = "{\"value\": [\"2\",\"3\"]}"; + pactffi_with_header_v2(handle, InteractionPart::Request, "x-id", 0, value); + ``` + + To include matching rules for the header, include the matching rule JSON + format with the value as a single JSON document. I.e. + + ```c + const char* value = "{\"value\":\"2\", \"pact:matcher:type\":\"regex\", \"regex\":\"\\\\d+\"}"; + pactffi_with_header_v2(handle, InteractionPart::Request, "id", 0, value); + ``` + + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md) + + NOTE: If you pass in a form with multiple values, the index will be ignored. + + # Safety + + The name and value parameters must be valid pointers to NULL terminated strings. + """ # noqa: E501 + raise NotImplementedError + + +def set_header( + interaction: InteractionHandle, + part: InteractionPart, + name: str, + value: str, +) -> bool: + """ + Sets a header for the Interaction. + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started). Note that this function will overwrite + any previously set header values. Also, this function will not process the + value in any way, so matching rules and generators can not be configured + with it. + + [Rust + `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_set_header) + + If matching rules are required to be set, use `pactffi_with_header_v2`. + + * `part` - The part of the interaction to add the header to (Request or + Response). + * `name` - the header name. + * `value` - the header value. + + # Safety The name and value parameters must be valid pointers to NULL + terminated strings. + """ + raise NotImplementedError + + +def response_status(interaction: InteractionHandle, status: int) -> bool: + """ + Configures the response for the Interaction. + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) + + [Rust + `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_response_status) + + * `status` - the response status. Defaults to 200. + """ + raise NotImplementedError + + +def with_body( + interaction: InteractionHandle, + part: InteractionPart, + content_type: str, + body: str, +) -> bool: + """ + Adds the body for the interaction. + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) + + [Rust + `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_body) + + * `part` - The part of the interaction to add the body to (Request or + Response). + * `content_type` - The content type of the body. Defaults to `text/plain`. + Will be ignored if a content type header is already set. + * `body` - The body contents. For JSON payloads, matching rules can be + embedded in the body. See + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md) + + For HTTP and async message interactions, this will overwrite the body. With + asynchronous messages, the part parameter will be ignored. With synchronous + messages, the request contents will be overwritten, while a new response + will be appended to the message. + + # Safety + + The interaction contents and content type must either be NULL pointers, or + point to valid UTF-8 encoded NULL-terminated strings. Otherwise, behaviour + is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the body contents as null. If + the content type is a null pointer, or can't be parsed, it will set the + content type as TEXT. Returns false if the interaction or Pact can't be + modified (i.e. the mock server for it has already started) or an error has + occurred. + """ + raise NotImplementedError + + +def with_binary_file( + interaction: InteractionHandle, + part: InteractionPart, + content_type: str, + body: List[int], + size: int, +) -> bool: + """ + Adds a binary file as the body with the expected content type and contents. + + Will use a mime type matcher to match the body. Returns false if the + interaction or Pact can't be modified (i.e. the mock server for it has + already started) + + [Rust + `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_binary_file) + + * `interaction` - Interaction handle to set the body for. + * `part` - Request or response part. + * `content_type` - Expected content type. + * `body` - example body contents in bytes + * `size` - number of bytes in the body + + For HTTP and async message interactions, this will overwrite the body. With + asynchronous messages, the part parameter will be ignored. With synchronous + messages, the request contents will be overwritten, while a new response + will be appended to the message. + + # Safety + + The content type must be a valid UTF-8 encoded NULL-terminated string. The + body pointer must be valid for reads of `size` bytes, and it must be + properly aligned and consecutive. + + # Error Handling + + If the body is a NULL pointer, it will set the body contents as null. If the + content type is a null pointer, or can't be parsed, it will return false. + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) or an error has occurred. + """ + raise NotImplementedError + + +def with_multipart_file_v2( # noqa: PLR0913 + interaction: InteractionHandle, + part: InteractionPart, + content_type: str, + file: str, + part_name: str, + boundary: str, +) -> StringResult: + """ + Adds a binary file as the body as a MIME multipart. + + Will use a mime type matcher to match the body. Returns an error if the + interaction or Pact can't be modified (i.e. the mock server for it has + already started) or an error occurs. + + [Rust + `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_multipart_file_v2) + + * `interaction` - Interaction handle to set the body for. + * `part` - Request or response part. + * `content_type` - Expected content type of the file. + * `file` - path to the example file + * `part_name` - name for the mime part + * `boundary` - boundary for the multipart separation + + This function can be called multiple times. In that case, each subsequent + call will be appended to the existing multipart body as a new part. + + # Safety + + The content type, file path and part name must be valid pointers to UTF-8 + encoded NULL-terminated strings. Passing invalid pointers or pointers to + strings that are not NULL terminated will lead to undefined behaviour. + + # Error Handling + + If the boundary is a NULL pointer, a random string will be used. If the file + path is a NULL pointer, it will set the body contents as as an empty + mime-part. If the file path does not point to a valid file, or is not able + to be read, it will return an error result. If the content type is a null + pointer, or can't be parsed, it will return an error result. Returns an + error if the interaction or Pact can't be modified (i.e. the mock server for + it has already started), the interaction is not an HTTP interaction or some + other error occurs. + """ + raise NotImplementedError + + +def with_multipart_file( + interaction: InteractionHandle, + part: InteractionPart, + content_type: str, + file: str, + part_name: str, +) -> StringResult: + """ + Adds a binary file as the body as a MIME multipart. + + Will use a mime type matcher to match the body. Returns an error if the + interaction or Pact can't be modified (i.e. the mock server for it has + already started) or an error occurs. + + [Rust + `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_multipart_file) + + * `interaction` - Interaction handle to set the body for. + * `part` - Request or response part. + * `content_type` - Expected content type of the file. + * `file` - path to the example file + * `part_name` - name for the mime part + + This function can be called multiple times. In that case, each subsequent + call will be appended to the existing multipart body as a new part. + + # Safety + + The content type, file path and part name must be valid pointers to UTF-8 + encoded NULL-terminated strings. Passing invalid pointers or pointers to + strings that are not NULL terminated will lead to undefined behaviour. + + # Error Handling + + If the file path is a NULL pointer, it will set the body contents as as an + empty mime-part. If the file path does not point to a valid file, or is not + able to be read, it will return an error result. If the content type is a + null pointer, or can't be parsed, it will return an error result. Returns an + error if the interaction or Pact can't be modified (i.e. the mock server for + it has already started), the interaction is not an HTTP interaction or some + other error occurs. + """ + raise NotImplementedError + + +def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: + r""" + Get an iterator over all the messages of the Pact. + + The returned iterator needs to be freed with + `pactffi_pact_message_iter_delete`. + + [Rust + `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_handle_get_message_iter) + + # Safety + + The iterator contains a copy of the Pact, so it is always safe to use. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. + """ + raise NotImplementedError + + +def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterator: + r""" + Get an iterator over all the synchronous messages of the Pact. + + The returned iterator needs to be freed with + `pactffi_pact_sync_message_iter_delete`. + + [Rust + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + + # Safety + + The iterator contains a copy of the Pact, so it is always safe to use. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. + """ + raise NotImplementedError + + +def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: + r""" + Get an iterator over all the synchronous HTTP request/response interactions. + + The returned iterator needs to be freed with + `pactffi_pact_sync_http_iter_delete`. + + [Rust + `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) + + # Safety + + The iterator contains a copy of the Pact, so it is always safe to use. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. + """ + raise NotImplementedError + + +def new_message_pact(consumer_name: str, provider_name: str) -> MessagePactHandle: + """ + Creates a new Pact Message model and returns a handle to it. + + [Rust + `pactffi_new_message_pact`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_message_pact) + + * `consumer_name` - The name of the consumer for the pact. + * `provider_name` - The name of the provider for the pact. + + Returns a new `MessagePactHandle`. The handle will need to be freed with the + `pactffi_free_message_pact_handle` function to release its resources. + """ + raise NotImplementedError + + +def new_message(pact: MessagePactHandle, description: str) -> MessageHandle: + """ + Creates a new Message and returns a handle to it. + + [Rust + `pactffi_new_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_message) + + * `description` - The message description. It needs to be unique for each + Message. + + Returns a new `MessageHandle`. + """ + raise NotImplementedError + + +def message_expects_to_receive(message: MessageHandle, description: str) -> None: + """ + Sets the description for the Message. + + [Rust + `pactffi_message_expects_to_receive`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_expects_to_receive) + + * `description` - The message description. It needs to be unique for each + message. + """ + raise NotImplementedError + + +def message_given(message: MessageHandle, description: str) -> None: + """ + Adds a provider state to the Interaction. + + [Rust + `pactffi_message_given`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_given) + + * `description` - The provider state description. It needs to be unique for + each message + """ + raise NotImplementedError + + +def message_given_with_param( + message: MessageHandle, + description: str, + name: str, + value: str, +) -> None: + """ + Adds a provider state to the Message with a parameter key and value. + + [Rust + `pactffi_message_given_with_param`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_given_with_param) + + * `description` - The provider state description. It needs to be unique. + * `name` - Parameter name. + * `value` - Parameter value. + """ + raise NotImplementedError + + +def message_with_contents( + message_handle: MessageHandle, + content_type: str, + body: List[int], + size: int, +) -> None: + """ + Adds the contents of the Message. + + [Rust + `pactffi_message_with_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_with_contents) + + Accepts JSON, binary and other payload types. Binary data will be base64 + encoded when serialised. + + Note: For text bodies (plain text, JSON or XML), you can pass in a C string + (NULL terminated) and the size of the body is not required (it will be + ignored). For binary bodies, you need to specify the number of bytes in the + body. + + * `content_type` - The content type of the body. Defaults to `text/plain`, + supports JSON structures with matchers and binary data. + * `body` - The body contents as bytes. For text payloads (JSON, XML, etc.), + a C string can be used and matching rules can be embedded in the body. + * `content_type` - Expected content type (e.g. application/json, + application/octet-stream) + * `size` - number of bytes in the message body to read. This is not required + for text bodies (JSON, XML, etc.). + """ + raise NotImplementedError + + +def message_with_metadata(message_handle: MessageHandle, key: str, value: str) -> None: + """ + Adds expected metadata to the Message. + + [Rust + `pactffi_message_with_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_with_metadata) + + * `key` - metadata key + * `value` - metadata value. + """ + raise NotImplementedError + + +def message_reify(message_handle: MessageHandle) -> str: + """ + Reifies the given message. + + [Rust + `pactffi_message_reify`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_reify) + + Reification is the process of stripping away any matchers, and returning the + original contents. NOTE: the returned string needs to be deallocated with + the `free_string` function + """ + raise NotImplementedError + + +def write_message_pact_file( + pact: MessagePactHandle, + directory: str, + *, + overwrite: bool, +) -> int: + """ + External interface to write out the message pact file. + + This function should be called if all the consumer tests have passed. The + directory to write the file to is passed as the second parameter. If a NULL + pointer is passed, the current working directory is used. + + [Rust + `pactffi_write_message_pact_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_write_message_pact_file) + + If overwrite is true, the file will be overwritten with the contents of the + current pact. Otherwise, it will be merged with any existing pact file. + + Returns 0 if the pact file was successfully written. Returns a positive code + if the file can not be written, or there is no mock server running on that + port or the function panics. + + # Errors + + Errors are returned as positive values. + + | Error | Description | + |-------|-------------| + | 1 | The pact file was not able to be written | + | 2 | The message pact for the given handle was not found | + """ + raise NotImplementedError + + +def with_message_pact_metadata( + pact: MessagePactHandle, + namespace_: str, + name: str, + value: str, +) -> None: + """ + Sets the additional metadata on the Pact file. + + Common uses are to add the client library details such as the name and + version + + [Rust + `pactffi_with_message_pact_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_message_pact_metadata) + + * `pact` - Handle to a Pact model + * `namespace` - the top level metadat key to set any key values on + * `name` - the key to set + * `value` - the value to set + """ + raise NotImplementedError + + +def pact_handle_write_file(pact: PactHandle, directory: str, *, overwrite: bool) -> int: + """ + External interface to write out the pact file. + + This function should be called if all the consumer tests have passed. The + directory to write the file to is passed as the second parameter. If a NULL + pointer is passed, the current working directory is used. + + [Rust + `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_handle_write_file) + + If overwrite is true, the file will be overwritten with the contents of the + current pact. Otherwise, it will be merged with any existing pact file. + + Returns 0 if the pact file was successfully written. Returns a positive code + if the file can not be written or the function panics. + + # Safety + + The directory parameter must either be NULL or point to a valid NULL + terminated string. + + # Errors + + Errors are returned as positive values. + + | Error | Description | + |-------|-------------| + | 1 | The function panicked. | + | 2 | The pact file was not able to be written. | + | 3 | The pact for the given handle was not found. | + """ + raise NotImplementedError + + +def new_async_message(pact: PactHandle, description: str) -> MessageHandle: + """ + Creates a new V4 asynchronous message and returns a handle to it. + + [Rust + `pactffi_new_async_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_async_message) + + * `description` - The message description. It needs to be unique for each + Message. + + Returns a new `MessageHandle`. + """ + raise NotImplementedError + + +def free_pact_handle(pact: PactHandle) -> int: + """ + Delete a Pact handle and free the resources used by it. + + [Rust + `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_free_pact_handle) + + # Error Handling + + On failure, this function will return a positive integer value. + + * `1` - The handle is not valid or does not refer to a valid Pact. Could be + that it was previously deleted. + + """ + raise NotImplementedError + + +def free_message_pact_handle(pact: MessagePactHandle) -> int: + """ + Delete a Pact handle and free the resources used by it. + + [Rust + `pactffi_free_message_pact_handle`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_free_message_pact_handle) + + # Error Handling + + On failure, this function will return a positive integer value. + + * `1` - The handle is not valid or does not refer to a valid Pact. Could be + that it was previously deleted. + + """ + raise NotImplementedError + + +def verify(args: str) -> int: + """ + External interface to verifier a provider. + + [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verify) + + * `args` - the same as the CLI interface, except newline delimited + + # Errors + + Errors are returned as non-zero numeric values. + + | Error | Description | + |-------|-------------| + | 1 | The verification process failed, see output for errors | + | 2 | A null pointer was received | + | 3 | The method panicked | + | 4 | Invalid arguments were provided to the verification process | + + # Safety + + Exported functions are inherently unsafe. Deal. + """ + raise NotImplementedError + + +def verifier_new() -> VerifierHandle: + """ + Get a Handle to a newly created verifier. + + You should call `pactffi_verifier_shutdown` when done with the verifier to + free all allocated resources. + + [Rust + `pactffi_verifier_new`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_new) + + Deprecated: This function is deprecated. Use + `pactffi_verifier_new_for_application` which allows the calling + application/framework name and version to be specified. + + # Safety + + This function is safe. + + # Error Handling + + Returns NULL on error. + """ + warnings.warn( + "This function is deprecated, use verifier_new_for_application instead", + DeprecationWarning, + stacklevel=2, + ) + raise NotImplementedError + + +def verifier_new_for_application(name: str, version: str) -> VerifierHandle: + """ + Get a Handle to a newly created verifier. + + You should call `pactffi_verifier_shutdown` when done with the verifier to + free all allocated resources + + [Rust + `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_new_for_application) + + # Safety + + This function is safe. + + # Error Handling + + Returns NULL on error. + """ + raise NotImplementedError + + +def verifier_shutdown(handle: VerifierHandle) -> None: + """ + Shutdown the verifier and release all resources. + + [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_shutdown) + """ + raise NotImplementedError + + +def verifier_set_provider_info( # noqa: PLR0913 + handle: VerifierHandle, + name: str, + scheme: str, + host: str, + port: int, + path: str, +) -> None: + """ + Set the provider details for the Pact verifier. + + Passing a NULL for any field will use the default value for that field. + + [Rust + `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_provider_info) + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + """ + raise NotImplementedError + + +def verifier_add_provider_transport( + handle: VerifierHandle, + protocol: str, + port: int, + path: str, + scheme: str, +) -> None: + """ + Adds a new transport for the given provider. + + Passing a NULL for any field will use the default value for that field. + + [Rust + `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_add_provider_transport) + + For non-plugin based message interactions, set protocol to "message" and set + scheme to an empty string or "https" if secure HTTP is required. + Communication to the calling application will be over HTTP to the default + provider hostname. + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + """ + raise NotImplementedError + + +def verifier_set_filter_info( + handle: VerifierHandle, + filter_description: str, + filter_state: str, + filter_no_state: int, +) -> None: + """ + Set the filters for the Pact verifier. + + [Rust + `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_filter_info) + + If `filter_description` is not empty, it needs to be as a regular + expression. + + `filter_no_state` is a boolean value. Set it to greater than zero to turn + the option on. + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + + """ + raise NotImplementedError + + +def verifier_set_provider_state( + handle: VerifierHandle, + url: str, + teardown: int, + body: int, +) -> None: + """ + Set the provider state URL for the Pact verifier. + + [Rust + `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_provider_state) + + `teardown` is a boolean value. If teardown state change requests should be + made after an interaction is validated (default is false). Set it to greater + than zero to turn the option on. `body` is a boolean value. Sets if state + change request data should be sent in the body (> 0, true) or as query + parameters (== 0, false). Set it to greater than zero to turn the option on. + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + + """ + raise NotImplementedError + + +def verifier_set_verification_options( + handle: VerifierHandle, + disable_ssl_verification: int, + request_timeout: int, +) -> int: + """ + Set the options used by the verifier when calling the provider. + + [Rust + `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_verification_options) + + `disable_ssl_verification` is a boolean value. Set it to greater than zero + to turn the option on. + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + + """ + raise NotImplementedError + + +def verifier_set_coloured_output(handle: VerifierHandle, coloured_output: int) -> int: + """ + Enables or disables coloured output using ANSI escape codes. + + By default, coloured output is enabled. + + [Rust + `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_coloured_output) + + `coloured_output` is a boolean value. Set it to greater than zero to turn + the option on. + + # Safety + + This function is safe as long as the handle pointer points to a valid + handle. + + """ + raise NotImplementedError + + +def verifier_set_no_pacts_is_error(handle: VerifierHandle, is_error: int) -> int: + """ + Enables or disables if no pacts are found to verify results in an error. + + [Rust + `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) + + `is_error` is a boolean value. Set it to greater than zero to enable an + error when no pacts are found to verify, and set it to zero to disable this. + + # Safety + + This function is safe as long as the handle pointer points to a valid + handle. + + """ + raise NotImplementedError + + +def verifier_set_publish_options( # noqa: PLR0913 + handle: VerifierHandle, + provider_version: str, + build_url: str, + provider_tags: List[str], + provider_tags_len: int, + provider_branch: str, +) -> int: + """ + Set the options used when publishing verification results to the Broker. + + [Rust + `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_publish_options) + + # Args + + - `handle` - The pact verifier handle to update + - `provider_version` - Version of the provider to publish + - `build_url` - URL to the build which ran the verification + - `provider_tags` - Collection of tags for the provider + - `provider_tags_len` - Number of provider tags supplied + - `provider_branch` - Name of the branch used for verification + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + + """ + raise NotImplementedError + + +def verifier_set_consumer_filters( + handle: VerifierHandle, + consumer_filters: List[str], + consumer_filters_len: int, +) -> None: + """ + Set the consumer filters for the Pact verifier. + + [Rust + `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_consumer_filters) + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + + """ + raise NotImplementedError + + +def verifier_add_custom_header( + handle: VerifierHandle, + header_name: str, + header_value: str, +) -> None: + """ + Adds a custom header to be added to the requests made to the provider. + + [Rust + `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_add_custom_header) + + # Safety + + The header name and value must point to a valid NULL terminated string and + must contain valid UTF-8. + """ + raise NotImplementedError + + +def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: + """ + Adds a Pact file as a source to verify. + + [Rust + `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_add_file_source) + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + + """ + raise NotImplementedError + + +def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> None: + """ + Adds a Pact directory as a source to verify. + + All pacts from the directory that match the provider name will be verified. + + [Rust + `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_add_directory_source) + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + + """ + raise NotImplementedError + + +def verifier_url_source( + handle: VerifierHandle, + url: str, + username: str, + password: str, + token: str, +) -> None: + """ + Adds a URL as a source to verify. + + The Pact file will be fetched from the URL. + + [Rust + `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_url_source) + + If a username and password is given, then basic authentication will be used + when fetching the pact file. If a token is provided, then bearer token + authentication will be used. + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + + """ + raise NotImplementedError + + +def verifier_broker_source( + handle: VerifierHandle, + url: str, + username: str, + password: str, + token: str, +) -> None: + """ + Adds a Pact broker as a source to verify. + + This will fetch all the pact files from the broker that match the provider + name. + + [Rust + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_broker_source) + + If a username and password is given, then basic authentication will be used + when fetching the pact file. If a token is provided, then bearer token + authentication will be used. + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + + """ + raise NotImplementedError + + +def verifier_broker_source_with_selectors( # noqa: PLR0913 + handle: VerifierHandle, + url: str, + username: str, + password: str, + token: str, + enable_pending: int, + include_wip_pacts_since: str, + provider_tags: List[str], + provider_tags_len: int, + provider_branch: str, + consumer_version_selectors: List[str], + consumer_version_selectors_len: int, + consumer_version_tags: List[str], + consumer_version_tags_len: int, +) -> None: + """ + Adds a Pact broker as a source to verify. + + This will fetch all the pact files from the broker that match the provider + name and the consumer version selectors (See + `https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/`). + + [Rust + `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) + + The consumer version selectors must be passed in in JSON format. + + `enable_pending` is a boolean value. Set it to greater than zero to turn the + option on. + + If the `include_wip_pacts_since` option is provided, it needs to be a date + formatted in ISO format (YYYY-MM-DD). + + If a username and password is given, then basic authentication will be used + when fetching the pact file. If a token is provided, then bearer token + authentication will be used. + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + + """ + raise NotImplementedError + + +def verifier_execute(handle: VerifierHandle) -> int: + """ + Runs the verification. + + [Rust `pactffi_verifier_execute`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_execute) + + # Error Handling + + Errors will be reported with a non-zero return value. + """ + raise NotImplementedError + + +def verifier_cli_args() -> str: + """ + External interface to retrieve the CLI options and arguments. + + This available when calling the CLI interface, returning them as a JSON + string. + + [Rust + `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_cli_args) + + The purpose is to then be able to use in other languages which wrap the FFI + library, to implement the same CLI functionality automatically without + manual maintenance of arguments, help descriptions etc. + + # Example structure + + ```json + { + "options": [ + { + "long": "scheme", + "help": "Provider URI scheme (defaults to http)", + "possible_values": [ + "http", + "https" + ], + "default_value": "http" + "multiple": false, + }, + { + "long": "file", + "short": "f", + "help": "Pact file to verify (can be repeated)", + "multiple": true + }, + { + "long": "user", + "help": "Username to use when fetching pacts from URLS", + "multiple": false, + "env": "PACT_BROKER_USERNAME" + } + ], + "flags": [ + { + "long": "disable-ssl-verification", + "help": "Disables validation of SSL certificates", + "multiple": false + } + ] + } + ``` + + # Safety + + Exported functions are inherently unsafe. + """ + raise NotImplementedError + + +def verifier_logs(handle: VerifierHandle) -> str: + """ + Extracts the logs for the verification run. + + This needs the memory buffer log sink to be setup before the verification is + executed. The returned string will need to be freed with the `free_string` + function call to avoid leaking memory. + + [Rust + `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_logs) + + Will return a NULL pointer if the logs for the verification can not be + retrieved. + """ + raise NotImplementedError + + +def verifier_logs_for_provider(provider_name: str) -> str: + """ + Extracts the logs for the verification run for the provider name. + + This needs the memory buffer log sink to be setup before the verification is + executed. The returned string will need to be freed with the `free_string` + function call to avoid leaking memory. + + [Rust + `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_logs_for_provider) + + Will return a NULL pointer if the logs for the verification can not be + retrieved. + """ + raise NotImplementedError + + +def verifier_output(handle: VerifierHandle, strip_ansi: int) -> str: + """ + Extracts the standard output for the verification run. + + The returned string will need to be freed with the `free_string` function + call to avoid leaking memory. + + [Rust + `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_output) + + * `strip_ansi` - This parameter controls ANSI escape codes. Setting it to a + non-zero value + will cause the ANSI control codes to be stripped from the output. + + Will return a NULL pointer if the handle is invalid. + """ + raise NotImplementedError + + +def verifier_json(handle: VerifierHandle) -> str: + """ + Extracts the verification result as a JSON document. + + The returned string will need to be freed with the `free_string` function + call to avoid leaking memory. + + [Rust + `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_json) + + Will return a NULL pointer if the handle is invalid. + """ + raise NotImplementedError + + +def using_plugin(pact: PactHandle, plugin_name: str, plugin_version: str) -> int: + """ + Add a plugin to be used by the test. + + The plugin needs to be installed correctly for this function to work. + + [Rust + `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_using_plugin) + + * `plugin_name` is the name of the plugin to load. + * `plugin_version` is the version of the plugin to load. It is optional, and + can be NULL. + + Returns zero on success, and a positive integer value on failure. + + Note that plugins run as separate processes, so will need to be cleaned up + afterwards by calling `pactffi_cleanup_plugins` otherwise you will have + plugin processes left running. + + # Safety + + `plugin_name` must be a valid pointer to a NULL terminated string. + `plugin_version` may be null, and if not NULL must also be a valid pointer + to a NULL terminated string. Invalid pointers will result in undefined + behaviour. + + # Errors + + * `1` - A general panic was caught. + * `2` - Failed to load the plugin. + * `3` - Pact Handle is not valid. + + When an error errors, LAST_ERROR will contain the error message. + """ + raise NotImplementedError + + +def cleanup_plugins(pact: PactHandle) -> None: + """ + Decrement the access count on any plugins that are loaded for the Pact. + + This will shutdown any plugins that are no longer required (access count is + zero). + + [Rust + `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_cleanup_plugins) + """ + raise NotImplementedError + + +def interaction_contents( + interaction: InteractionHandle, + part: InteractionPart, + content_type: str, + contents: str, +) -> int: + """ + Setup the interaction part using a plugin. + + The contents is a JSON string that will be passed on to the plugin to + configure the interaction part. Refer to the plugin documentation on the + format of the JSON contents. + + [Rust + `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_interaction_contents) + + Returns zero on success, and a positive integer value on failure. + + * `interaction` - Handle to the interaction to configure. + * `part` - The part of the interaction to configure (request or response). + It is ignored for messages. + * `content_type` - NULL terminated C string of the content type of the part. + * `contents` - NULL terminated C string of the JSON contents that gets + passed to the plugin. + + # Safety + + `content_type` and `contents` must be a valid pointers to NULL terminated + strings. Invalid pointers will result in undefined behaviour. + + # Errors + + * `1` - A general panic was caught. + * `2` - The mock server has already been started. + * `3` - The interaction handle is invalid. + * `4` - The content type is not valid. + * `5` - The contents JSON is not valid JSON. + * `6` - The plugin returned an error. + + When an error errors, LAST_ERROR will contain the error message. + """ + raise NotImplementedError + + +def matches_string_value( + matching_rule: MatchingRule, + expected_value: str, + actual_value: str, + cascaded: int, +) -> str: + """ + Determines if the string value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_string_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get as a NULL terminated string + * actual_value - value to match as a NULL terminated string + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule pointer must be a valid pointer, and the value parameters + must be valid pointers to a NULL terminated strings. + """ + raise NotImplementedError + + +def matches_u64_value( + matching_rule: MatchingRule, + expected_value: int, + actual_value: int, + cascaded: int, +) -> str: + """ + Determines if the unsigned integer value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_u64_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get + * actual_value - value to match + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule pointer must be a valid pointer. + """ + raise NotImplementedError + + +def matches_i64_value( + matching_rule: MatchingRule, + expected_value: int, + actual_value: int, + cascaded: int, +) -> str: + """ + Determines if the signed integer value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_i64_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get + * actual_value - value to match + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule pointer must be a valid pointer. + """ + raise NotImplementedError + + +def matches_f64_value( + matching_rule: MatchingRule, + expected_value: float, + actual_value: float, + cascaded: int, +) -> str: + """ + Determines if the floating point value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_f64_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get + * actual_value - value to match + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule pointer must be a valid pointer. + """ + raise NotImplementedError + + +def matches_bool_value( + matching_rule: MatchingRule, + expected_value: int, + actual_value: int, + cascaded: int, +) -> str: + """ + Determines if the boolean value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_bool_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get, 0 == false and 1 == true + * actual_value - value to match, 0 == false and 1 == true + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule pointer must be a valid pointer. + """ + raise NotImplementedError + + +def matches_binary_value( # noqa: PLR0913 + matching_rule: MatchingRule, + expected_value: str, + expected_value_len: int, + actual_value: str, + actual_value_len: int, + cascaded: int, +) -> str: + """ + Determines if the binary value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_binary_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get + * expected_value_len - length of the expected value bytes + * actual_value - value to match + * actual_value_len - length of the actual value bytes + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule, expected value and actual value pointers must be a valid + pointers. expected_value_len and actual_value_len must contain the number of + bytes that the value pointers point to. Passing invalid lengths can lead to + undefined behaviour. + """ + raise NotImplementedError + + +def matches_json_value( + matching_rule: MatchingRule, + expected_value: str, + actual_value: str, + cascaded: int, +) -> str: + """ + Determines if the JSON value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_json_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get as a NULL terminated string + * actual_value - value to match as a NULL terminated string + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule pointer must be a valid pointer, and the value parameters + must be valid pointers to a NULL terminated strings. + """ + raise NotImplementedError diff --git a/tests/__init__.py b/pact/v3/py.typed similarity index 100% rename from tests/__init__.py rename to pact/v3/py.typed diff --git a/pyproject.toml b/pyproject.toml index c059fc2d1..baf875c06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,11 @@ dependencies = [ pact-verifier = "pact.cli.verify:main" [project.optional-dependencies] -types = ["mypy ~= 1.1", "types-requests ~= 2.31"] +types = [ + "mypy ~= 1.1", + "types-cffi ~= 1.15", + "types-requests ~= 2.31", +] test = [ "coverage[toml] ~= 7.3", "flask[async] ~= 2.3", diff --git a/tests/ruff.toml b/tests/ruff.toml index 7d4b21b00..164732ac2 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -1,6 +1,7 @@ extend = "../pyproject.toml" ignore = [ "D103", # Require docstrings on public functions - "S101", # Disable assert + "INP001", # Forbid implicit namespaces "PLR2004", # Forbid magic numbers + "S101", # Disable assert ] diff --git a/tests/test_ffi.py b/tests/v3/test_ffi.py similarity index 100% rename from tests/test_ffi.py rename to tests/v3/test_ffi.py From e758ffb5a68bfa62c73e4f163a72f140d38dafed Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 9 Oct 2023 20:14:56 +1100 Subject: [PATCH 0062/1376] docs(v3): update ffi documentation Specifically want to make it clear how this module is going to be implemented, and lay down guidelines to be followed going forward. Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index b2f3b03b8..d8eda4983 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -16,15 +16,56 @@ - Basic Enum classes - Simple wrappers around functions, including the casting of input and output values between the high level Python types and the low level C types. +- Simple wrappers around some of the low-level types. Specifically designed to + automatically handle the freeing of memory when the Python object is + destroyed. These low-level functions may then be combined into higher level classes and -modules. +modules. Ideally, all code outside of this module should be written in pure +Python and not worry about allocating or freeing memory. During initial implementation, a lot of these functions will simply raise a `NotImplementedError`. For those unfamiliar with CFFI, please make sure to read the [CFFI documentation](https://cffi.readthedocs.io/en/latest/using.html). + +### Handles + +The Rust library exposes a number of handles to internal data structures. This +is done to avoid exposing the internal implementation details of the library to +users of the library, and avoid unnecessarily casting to and from possibly +complicated structs. + +In the Rust library, the handles are thin wrappers around integers, and +unfortunately the CFFI interface sees this and automatically unwraps them, +exposing the underlying integer. As a result, we must re-wrap the integer +returned by the CFFI interface. This unfortunately means that we may be subject +to changes in private implementation details upstream. + +### Freeing Memory + +Python has a garbage collector, and as a result, we don't need to worry about +manually freeing memory. Having said that, Python's garbace collector is only +aware of Python objects, and not of any memory allocated by the Rust library. + +To ensure that the memory allocated by the Rust library is freed, we must make +sure to define the +[`__del__`](https://docs.python.org/3/reference/datamodel.html#object.__del__) +method to call the appropriate free function whenever the Python object is +destroyed. + +Note that there are some rather subtle details as to when this is called, when +it may never be called, and what global variables are accessible. This is +explained in the documentation for `__del__` above, and in Python's [garbage +collection](https://docs.python.org/3/library/gc.html) module. + +### Error Handling + +The FFI function should handle all errors raised by the function call, and raise +an appropriate Python exception. The exception should be raised using the +appropriate Python exception class, and should be documented in the function's +docstring. """ # ruff: noqa: ARG001 (unused-function-argument) # ruff: noqa: A002 (builtin-argument-shadowing) From 0a2d9ebd18982ef86e725046080a32fa272c430b Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 9 Oct 2023 20:18:14 +1100 Subject: [PATCH 0063/1376] chore(tests): remove empty file Signed-off-by: JP-Ellis --- tests/conftest.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 20b94aaa6..000000000 --- a/tests/conftest.py +++ /dev/null @@ -1 +0,0 @@ -# conftest From 28ed51e8bf34e306467eb8608805cd045bc50678 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 9 Oct 2023 21:14:34 +1100 Subject: [PATCH 0064/1376] chore(v3): add str and repr to enums This helps makes enums more manageable during logging and debugging. The `D200` Ruff lint is ignored, ensuring that single-line docstrings use the same formatting as the remaining docstrings. Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 86 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index d8eda4983..37605a1e0 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -264,6 +264,18 @@ class ExpressionValueType(Enum): DECIMAL = lib.ExpressionValueType_Decimal BOOLEAN = lib.ExpressionValueType_Boolean + def __str__(self) -> str: + """ + Informal string representation of the Expression Value Type. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the Expression Value Type. + """ + return f"ExpressionValueType.{self.name}" + class GeneratorCategory(Enum): """ @@ -280,6 +292,18 @@ class GeneratorCategory(Enum): STATUS = lib.GeneratorCategory_STATUS METADATA = lib.GeneratorCategory_METADATA + def __str__(self) -> str: + """ + Informal string representation of the Generator Category. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the Generator Category. + """ + return f"GeneratorCategory.{self.name}" + class InteractionPart(Enum): """ @@ -291,6 +315,18 @@ class InteractionPart(Enum): REQUEST = lib.InteractionPart_Request RESPONSE = lib.InteractionPart_Response + def __str__(self) -> str: + """ + Informal string representation of the Interaction Part. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the Interaction Part. + """ + return f"InteractionPath.{self.name}" + class LevelFilter(Enum): """Level Filter.""" @@ -302,6 +338,18 @@ class LevelFilter(Enum): DEBUG = lib.LevelFilter_Debug TRACE = lib.LevelFilter_Trace + def __str__(self) -> str: + """ + Informal string representation of the Level Filter. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the Level Filter. + """ + return f"LevelFilter.{self.name}" + class MatchingRuleCategory(Enum): """ @@ -319,6 +367,18 @@ class MatchingRuleCategory(Enum): CONTENST = lib.MatchingRuleCategory_CONTENTS METADATA = lib.MatchingRuleCategory_METADATA + def __str__(self) -> str: + """ + Informal string representation of the Matching Rule Category. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the Matching Rule Category. + """ + return f"MatchingRuleCategory.{self.name}" + class PactSpecification(Enum): """ @@ -334,6 +394,18 @@ class PactSpecification(Enum): V3 = lib.PactSpecification_V3 V4 = lib.PactSpecification_V4 + def __str__(self) -> str: + """ + Informal string representation of the Pact Specification. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the Pact Specification. + """ + return f"Pact Specification.{self.name}" + class StringResult(Enum): """ @@ -343,7 +415,19 @@ class StringResult(Enum): """ FAILED = lib.StringResult_Failed - Ok = lib.StringResult_Ok + OK = lib.StringResult_Ok + + def __str__(self) -> str: + """ + Informal string representation of the String Result. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the String Result. + """ + return f"StringResult.{self.name}" def version() -> str: diff --git a/pyproject.toml b/pyproject.toml index baf875c06..5be607eb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -156,6 +156,7 @@ target-version = "py38" select = ["ALL"] ignore = [ + "D200", # Require single line docstrings to be on one line. "D203", # Require blank line before class docstring "D212", # Multi-line docstring summary must start at the first line "ANN101", # `self` must be typed From a82d6b86dd208ce7d297607903479177281abfdd Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 10 Oct 2023 08:31:01 +1100 Subject: [PATCH 0065/1376] chore(test): move pytest cli args definition The `--broker-url` PyTest CLI argument is used by the examples to determine whether a Broker is automatically launched, or whether an existing broker is used. All PyTest CLI arguments _must_ be defined in the root directory of the project. As a result, the previous location where these arguments were defined meant that the examples could only be launched of PyTest was specifically pointed at the `examples/` directory. This commit moves the definition to the root directory, thereby unifying the `examples/` with the remaining tests. Signed-off-by: JP-Ellis --- conftest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/conftest.py b/conftest.py index b6bf257ad..f6b59a455 100644 --- a/conftest.py +++ b/conftest.py @@ -1,8 +1,9 @@ """ -Global Pytest configuration. +Global PyTest configuration. -This file is used to define global Pytest configuration. In this case, we use it -to define additional command line options to customise the examples. +This file is automatically loaded by PyTest before running any tests and is used +to define global fixtures and command line options. Command line options can +only be defined in this file. """ import pytest From 89330cbf18ac19da24ccd729f2d349ced32ab2a6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 10 Oct 2023 12:56:21 +1100 Subject: [PATCH 0066/1376] feat(v3): implement pact class This (rather large) commit implements core functionality for the `Pact` class, and the `Interaction` class to handle specific interactions within a Pact. This does _not_ implement every method that Pacts and/or interactions might have (e.g., it is currently not possible to specify a Pact version). The intent of this commit is to be a minimal working example which can be improved upon more incrementally. Signed-off-by: JP-Ellis --- pact/v3/__init__.py | 7 + pact/v3/ffi.py | 802 +++++++++++++++++++++++++++--------------- pact/v3/pact.py | 788 +++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 26 +- tests/v3/conftest.py | 18 + tests/v3/test_pact.py | 431 +++++++++++++++++++++++ 6 files changed, 1785 insertions(+), 287 deletions(-) create mode 100644 pact/v3/pact.py create mode 100644 tests/v3/conftest.py create mode 100644 tests/v3/test_pact.py diff --git a/pact/v3/__init__.py b/pact/v3/__init__.py index 443bf9164..cb0059d95 100644 --- a/pact/v3/__init__.py +++ b/pact/v3/__init__.py @@ -19,3 +19,10 @@ library is moved to the `pact.v2` scope. The `pact.v2` module will be considered deprecated, and will be removed in a future release. """ + +from .pact import Interaction, Pact + +__all__ = [ + "Pact", + "Interaction", +] diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 37605a1e0..5dc0aebea 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -67,16 +67,28 @@ appropriate Python exception class, and should be documented in the function's docstring. """ +# The following lints are disabled during initial development and should be +# removed later. # ruff: noqa: ARG001 (unused-function-argument) # ruff: noqa: A002 (builtin-argument-shadowing) # ruff: noqa: D101 (undocumented-public-class) +# The following lints are disabled for this file. +# ruff: noqa: SLF001 +# private-member-access, as we need access to other handles' internal +# references, without exposing them to the user. + +from __future__ import annotations + import warnings from enum import Enum -from typing import List +from typing import TYPE_CHECKING, List from ._ffi import ffi, lib # type: ignore[import] +if TYPE_CHECKING: + from pathlib import Path + # The follow types are classes defined in the Rust code. Ultimately, a Python # alternative should be implemented, but for now, the follow lines only serve # to inform the type checker of the existence of these types. @@ -111,7 +123,34 @@ class HttpResponse: class InteractionHandle: - ... + """ + Handle to a HTTP Interaction. + + [Rust + `InteractionHandle`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/mock_server/handles/struct.InteractionHandle.html) + """ + + def __init__(self, ref: int) -> None: + """ + Initialise a new Interaction Handle. + + Args: + ref: + Reference to the Interaction Handle. + """ + self._ref: int = ref + + def __str__(self) -> str: + """ + String representation of the Interaction Handle. + """ + return f"InteractionHandle({self._ref})" + + def __repr__(self) -> str: + """ + String representation of the Interaction Handle. + """ + return f"InteractionHandle({self._ref})" class MatchingRule: @@ -195,7 +234,89 @@ class Pact: class PactHandle: - ... + """ + Handle to a Pact. + + [Rust + `PactHandle`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/mock_server/handles/struct.PactHandle.html) + """ + + def __init__(self, ref: int) -> None: + """ + Initialise a new Pact Handle. + + Args: + ref: + Rust library reference to the Pact Handle. + """ + self._ref: int = ref + + def __del__(self) -> None: + """ + Destructor for the Pact Handle. + """ + free_pact_handle(self) + + def __str__(self) -> str: + """ + String representation of the Pact Handle. + """ + return f"PactHandle({self._ref})" + + def __repr__(self) -> str: + """ + String representation of the Pact Handle. + """ + return f"PactHandle({self._ref})" + + +class PactServerHandle: + """ + Handle to a Pact Server. + + This does not have an exact correspondance in the Rust library. It is used + to manage the lifecycle of the mock server. + + # Implementation Notes + + The Rust library uses the port number as a unique identifier, in much the + same was as it uses a wrapped integer for the Pact handle. + """ + + def __init__(self, ref: int) -> None: + """ + Initialise a new Pact Server Handle. + + Args: + ref: + Rust library reference to the Pact Server. + """ + self._ref: int = ref + + def __del__(self) -> None: + """ + Destructor for the Pact Server Handle. + """ + cleanup_mock_server(self) + + def __str__(self) -> str: + """ + String representation of the Pact Server Handle. + """ + return f"PactServerHandle({self._ref})" + + def __repr__(self) -> str: + """ + String representation of the Pact Server Handle. + """ + return f"PactServerHandle({self._ref})" + + @property + def port(self) -> int: + """ + Port on which the Pact Server is running. + """ + return self._ref class PactInteraction: @@ -432,9 +553,12 @@ def __repr__(self) -> str: def version() -> str: """ - Wraps a Pact model struct. + Return the version of the pact_ffi library. [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_version) + + Returns: + The version of the pact_ffi library as a string, in the form of `x.y.z`. """ return ffi.string(lib.pactffi_version()).decode("utf-8") @@ -665,13 +789,28 @@ def log_to_stdout(level_filter: LevelFilter) -> int: raise NotImplementedError -def log_to_stderr(level_filter: LevelFilter) -> int: +def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: """ Convenience function to direct all logging to stderr. - [Rust `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_to_stderr) + [Rust + `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_to_stderr) + + Args: + level_filter: + The level of logs to filter to. If a string is given, it must match + one of the [`LevelFilter`][pact.v3.ffi.LevelFilter] values (case + insensitive). + + Raises: + RuntimeError: If there was an error setting the logger. """ - raise NotImplementedError + if isinstance(level_filter, str): + level_filter = LevelFilter[level_filter.upper()] + ret = lib.pactffi_log_to_stderr(level_filter.value) + if ret != 0: + msg = "There was an unknown error setting the logger." + raise RuntimeError(msg) def log_to_file(file_name: str, level_filter: LevelFilter) -> int: @@ -4075,54 +4214,67 @@ def create_mock_server_for_transport( addr: str, port: int, transport: str, - transport_config: str, -) -> int: + transport_config: str | None, +) -> PactServerHandle: """ Create a mock server for the provided Pact handle and transport. - If the transport is not provided (it is a NULL pointer or an empty string), - will default to an HTTP transport. The address is the interface bind to, and - will default to the loopback adapter if not specified. Specifying a value of - zero for the port will result in the operating system allocating the port. - [Rust `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_create_mock_server_for_transport) - Parameters: - - * `pact` - Handle to a Pact model created with created with - `pactffi_new_pact`. - * `addr` - Address to bind to (i.e. `127.0.0.1` or `[::1]`). Must be a valid - UTF-8 NULL-terminated string, or NULL or empty, in which case the loopback - adapter is used. - * `port` - Port number to bind to. A value of zero will result in the - operating system allocating an available port. - * `transport` - The transport to use (i.e. http, https, grpc). Must be a - valid UTF-8 NULL-terminated string, or NULL or empty, in which case http - will be used. - * `transport_config` - (OPTIONAL) Configuration for the transport as a valid - JSON string. Set to NULL or empty if not required. - - The port of the mock server is returned. - - # Safety NULL pointers or empty strings can be passed in for the address, - transport and transport_config, in which case a default value will be used. - Passing in an invalid pointer will result in undefined behaviour. - - # Errors - - Errors are returned as negative values. - - | Error | Description | - |-------|-------------| - | -1 | An invalid handle was received. Handles should be created with `pactffi_new_pact` | - | -2 | transport_config is not valid JSON | - | -3 | The mock server could not be started | - | -4 | The method panicked | - | -5 | The address is not valid | - - """ # noqa: E501 - raise NotImplementedError + Args: + pact: + Handle to the Pact model. + + addr: + The address to bind to. + + port: + The port number to bind to. A value of zero will result in the + operating system allocating an available port. + + transport: + The transport to use (i.e. http, https, grpc). The underlying Pact + library will interpret this, typically in a case-sensitive way. + + transport_config: + Configuration to be passed to the transport. This must be a valid + JSON string, or `None` if not required. + + Returns: + A handle to the mock server. + + Raises: + RuntimeError: If the mock server could not be created. The error message + will contain details of the error. + """ + ret: int = lib.pactffi_create_mock_server_for_transport( + pact._ref, + addr.encode("utf-8"), + port, + transport.encode("utf-8"), + ( + transport_config.encode("utf-8") + if transport_config is not None + else ffi.NULL + ), + ) + if ret > 0: + return PactServerHandle(ret) + + if ret == -1: + msg = f"An invalid Pact handle was received: {pact}." + elif ret == -2: # noqa: PLR2004 + msg = "Invalid transport_config JSON." + elif ret == -3: # noqa: PLR2004 + msg = f"Pact mock server could not be started for {pact}." + elif ret == -4: # noqa: PLR2004 + msg = f"Panick during Pact mock server creation for {pact}." + elif ret == -5: # noqa: PLR2004 + msg = f"Address is invalid: {addr}." + else: + msg = f"An unknown error occurred during Pact mock server creation for {pact}." + raise RuntimeError(msg) def mock_server_matched(mock_server_port: int) -> bool: @@ -4163,49 +4315,83 @@ def mock_server_mismatches(mock_server_port: int) -> str: raise NotImplementedError -def cleanup_mock_server(mock_server_port: int) -> bool: +def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: """ External interface to cleanup a mock server. This function will try terminate the mock server with the given port number - and cleanup any memory allocated for it. Returns true, unless a mock server - with the given port number does not exist, or the function panics. + and cleanup any memory allocated for it. [Rust `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_cleanup_mock_server) + + Args: + mock_server_handle: + Handle to the mock server to cleanup. + + Raises: + RuntimeError: If the mock server could not be cleaned up. """ - raise NotImplementedError + success: bool = lib.pactffi_cleanup_mock_server(mock_server_handle._ref) + if not success: + msg = f"Could not cleanup mock server with port {mock_server_handle._ref}" + raise RuntimeError(msg) -def write_pact_file(mock_server_port: int, directory: str, *, overwrite: bool) -> int: +def write_pact_file( + mock_server_handle: PactServerHandle, + directory: str | Path, + *, + overwrite: bool, +) -> None: """ External interface to trigger a mock server to write out its pact file. This function should be called if all the consumer tests have passed. The - directory to write the file to is passed as the second parameter. If a NULL - pointer is passed, the current working directory is used. + directory to write the file to is passed as the second parameter. [Rust `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_write_pact_file) - If overwrite is true, the file will be overwritten with the contents of the - current pact. Otherwise, it will be merged with any existing pact file. - - Returns 0 if the pact file was successfully written. Returns a positive code - if the file can not be written, or there is no mock server running on that - port or the function panics. + Args: + mock_server_handle: + Handle to the mock server to write the pact file for. - # Errors + directory: + Directory to write the pact file to. - Errors are returned as positive values. + overwrite: + Whether to overwrite any existing pact files. If this is false, the + pact file will be merged with any existing pact file. - | Error | Description | - |-------|-------------| - | 1 | A general panic was caught | - | 2 | The pact file was not able to be written | - | 3 | A mock server with the provided port was not found | + Raises: + RuntimeError: If there was an error writing the pact file. """ - raise NotImplementedError + ret: int = lib.pactffi_write_pact_file( + mock_server_handle._ref, + directory, + overwrite, + ) + if ret == 0: + return + if ret == 1: + msg = ( + f"The function panicked while writing the Pact for {mock_server_handle} in" + f" {directory}." + ) + elif ret == 2: # noqa: PLR2004 + msg = ( + f"The Pact file for {mock_server_handle} could not be written in" + f" {directory}." + ) + elif ret == 3: # noqa: PLR2004 + msg = f"The Pact for the {mock_server_handle} was not found." + else: + msg = ( + "An unknown error occurred while writing the Pact for" + f" {mock_server_handle} in {directory}." + ) + raise RuntimeError(msg) def mock_server_logs(mock_server_port: int) -> str: @@ -4308,13 +4494,22 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle: [Rust `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_pact) - * `consumer_name` - The name of the consumer for the pact. - * `provider_name` - The name of the provider for the pact. + Args: + consumer_name: + The name of the consumer for the pact. - Returns a new `PactHandle`. The handle will need to be freed with the - `pactffi_free_pact_handle` method to release its resources. + provider_name: + The name of the provider for the pact. + + Returns: + Handle to the new Pact model. """ - raise NotImplementedError + return PactHandle( + lib.pactffi_new_pact( + consumer_name.encode("utf-8"), + provider_name.encode("utf-8"), + ), + ) def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: @@ -4324,12 +4519,23 @@ def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: [Rust `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_interaction) - * `description` - The interaction description. It needs to be unique for - each interaction. + Args: + pact: + Handle to the Pact model. - Returns a new `InteractionHandle`. + description: + The interaction description. It needs to be unique for each + interaction. + + Returns: + Handle to the new Interaction. """ - raise NotImplementedError + return InteractionHandle( + lib.pactffi_new_interaction( + pact._ref, + description.encode("utf-8"), + ), + ) def new_message_interaction(pact: PactHandle, description: str) -> InteractionHandle: @@ -4381,19 +4587,27 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> bool: raise NotImplementedError -def given(interaction: InteractionHandle, description: str) -> bool: +def given(interaction: InteractionHandle, description: str) -> None: """ Adds a provider state to the Interaction. - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started) - [Rust `pactffi_given`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_given) - * `description` - The provider state description. It needs to be unique. + Args: + interaction: + Handle to the Interaction. + + description: + The provider state description. It needs to be unique. + + Raises: + RuntimeError: If the provider state could not be specified. """ - raise NotImplementedError + success: bool = lib.pactffi_given(interaction._ref, description.encode("utf-8")) + if not success: + msg = "The provider state could not be specified." + raise RuntimeError(msg) def interaction_test_name(interaction: InteractionHandle, test_name: str) -> int: @@ -4486,62 +4700,47 @@ def given_with_params( raise NotImplementedError -def with_request(interaction: InteractionHandle, method: str, path: str) -> bool: +def with_request(interaction: InteractionHandle, method: str, path: str) -> None: r""" Configures the request for the Interaction. - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started) - [Rust `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_request) - * `method` - The request method. Defaults to GET. - * `path` - The request path. Defaults to `/`. - - To include matching rules for the path (only regex really makes sense to - use), include the matching rule JSON format with the value as a single JSON - document. I.e. - - ```c - const char* value = "{\"value\":\"/path/to/100\", \"pact:matcher:type\":\"regex\", \"regex\":\"\\/path\\/to\\/\\\\d+\"}"; - pactffi_with_request(handle, "GET", value); - ``` - See - [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md) - """ # noqa: E501 - raise NotImplementedError - + Args: + interaction: + Handle to the Interaction. -def with_query_parameter( - interaction: InteractionHandle, - name: str, - index: int, - value: str, -) -> bool: - """ - Configures a query parameter for the Interaction. + method: + The request HTTP method. - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started) + path: + The request path. - [Rust - `pactffi_with_query_parameter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_query_parameter) + This may be a simple string in which case it will be used as-is, or + it may be a [JSON matching + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md) + which allows regex patterns. For examples: - * `name` - the query parameter name. - * `value` - the query parameter value. - * `index` - the index of the value (starts at 0). You can use this to create - a query parameter with multiple values + ```json + { + "value": "/path/to/100", + "pact:matcher:type": "regex", + "regex": "/path/to/\\d+" + } + ``` - **DEPRECATED:** Use `pactffi_with_query_parameter_v2`, which deals with - multiple values correctly + Raises: + RuntimeError: If the request could not be specified. """ - warnings.warn( - "This function is deprecated, use with_query_parameter_v2 instead", - DeprecationWarning, - stacklevel=2, + success: bool = lib.pactffi_with_request( + interaction._ref, + method.encode("utf-8"), + path.encode("utf-8"), ) - raise NotImplementedError + if not success: + msg = f"The request '{method} {path}' could not be specified for {interaction}." + raise RuntimeError(msg) def with_query_parameter_v2( @@ -4549,53 +4748,80 @@ def with_query_parameter_v2( name: str, index: int, value: str, -) -> bool: +) -> None: r""" Configures a query parameter for the Interaction. - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started) - [Rust `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_query_parameter_v2) - * `name` - the query parameter name. - * `value` - the query parameter value. Either a simple string or a JSON - document. - * `index` - the index of the value (starts at 0). You can use this to create - a query parameter with multiple values - To setup a query parameter with multiple values, you can either call this - function multiple times with a different index value, i.e. to create - `id=2&id=3` + function multiple times with a different index value: - ```c - pactffi_with_query_parameter_v2(handle, "id", 0, "2"); - pactffi_with_query_parameter_v2(handle, "id", 1, "3"); + ```python + with_query_parameter_v2(handle, "version", 0, "2") + with_query_parameter_v2(handle, "version", 0, "3") ``` Or you can call it once with a JSON value that contains multiple values: - ```c - const char* value = "{\"value\": [\"2\",\"3\"]}"; - pactffi_with_query_parameter_v2(handle, "id", 0, value); + ```python + with_query_parameter_v2( + handle, + "version", + 0, + json.dumps({ "value": ["2", "3"] }) + ) ``` - To include matching rules for the query parameter, include the matching rule - JSON format with the value as a single JSON document. I.e. - - ```c - const char* value = "{\"value\":\"2\", \"pact:matcher:type\":\"regex\", \"regex\":\"\\\\d+\"}"; - pactffi_with_query_parameter_v2(handle, "id", 0, value); + The JSON value can also contain a matcher, which will be used to match the + query parameter value. For example, a semver matcher might look like this: + + ```python + with_query_parameter_v2( + handle, + "version", + 0, + json.dumps({ + "value": "1.2.3", + "pact:matcher:type": "regex", + "regex": r"\d+\.\d+\.\d+", + }), + ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md) - # Safety + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md) - The name and value parameters must be valid pointers to NULL terminated strings. - ``` - """ # noqa: E501 - raise NotImplementedError + Args: + interaction: + Handle to the Interaction. + + name: + The query parameter name. + + index: + The index of the value (starts at 0). You can use this to create a + query parameter with multiple values. + + value: + The query parameter value. + + This may be a simple string in which case it will be used as-is, or + it may be a [JSON matching + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md). + + Raises: + RuntimeError: If there was an error setting the query parameter. + """ + success: bool = lib.pactffi_with_query_parameter_v2( + interaction._ref, + name.encode("utf-8"), + index, + value.encode("utf-8"), + ) + if not success: + msg = f"Failed to add query parameter {name} to request {interaction}." + raise RuntimeError(msg) def with_specification(pact: PactHandle, version: PactSpecification) -> bool: @@ -4638,94 +4864,91 @@ def with_pact_metadata( raise NotImplementedError -def with_header( - interaction: InteractionHandle, - part: InteractionPart, - name: str, - index: int, - value: str, -) -> bool: - """ - Configures a header for the Interaction. - - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started) - - [Rust - `pactffi_with_header`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_header) - - * `part` - The part of the interaction to add the header to (Request or - Response). - * `name` - the header name. - * `value` - the header value. - * `index` - the index of the value (starts at 0). You can use this to create - a header with multiple values - - **DEPRECATED:** Use `pactffi_with_header_v2`, which deals with multiple - values correctly - """ - warnings.warn( - "This function is deprecated, use with_header_v2 instead", - DeprecationWarning, - stacklevel=2, - ) - raise NotImplementedError - - def with_header_v2( interaction: InteractionHandle, part: InteractionPart, name: str, index: int, value: str, -) -> bool: +) -> None: r""" Configures a header for the Interaction. - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started) - [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_header_v2) - * `part` - The part of the interaction to add the header to (Request or - Response). - * `name` - the header name. - * `value` - the header value. - * `index` - the index of the value (starts at 0). You can use this to create - a header with multiple values - - To setup a header with multiple values, you can either call this function - multiple times with a different index value, i.e. to create `x-id=2, 3` + To setup a header with multiple values, you can either call this + function multiple times with a different index value: - ```c - pactffi_with_header_v2(handle, InteractionPart::Request, "x-id", 0, "2"); - pactffi_with_header_v2(handle, InteractionPart::Request, "x-id", 1, "3"); + ```python + with_header_v2(handle, part, "Accept-Version", 0, "2") + with_header_v2(handle, part, "Accept-Version", 0, "3") ``` Or you can call it once with a JSON value that contains multiple values: - ```c - const char* value = "{\"value\": [\"2\",\"3\"]}"; - pactffi_with_header_v2(handle, InteractionPart::Request, "x-id", 0, value); + ```python + with_header_v2( + handle, + part, + "Accept-Version", + 0, + json.dumps({ "value": ["2", "3"] }) + ) ``` - To include matching rules for the header, include the matching rule JSON - format with the value as a single JSON document. I.e. - - ```c - const char* value = "{\"value\":\"2\", \"pact:matcher:type\":\"regex\", \"regex\":\"\\\\d+\"}"; - pactffi_with_header_v2(handle, InteractionPart::Request, "id", 0, value); + The JSON value can also contain a matcher, which will be used to match the + query parameter value. For example, a semver matcher might look like this: + + ```python + with_query_parameter_v2( + handle, + "Accept-Version", + 0, + json.dumps({ + "value": "1.2.3", + "pact:matcher:type": "regex", + "regex": r"\d+\.\d+\.\d+", + }), + ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md) - NOTE: If you pass in a form with multiple values, the index will be ignored. + Args: + interaction: + Handle to the Interaction. - # Safety + part: + The part of the interaction to add the header to (Request or + Response). - The name and value parameters must be valid pointers to NULL terminated strings. - """ # noqa: E501 - raise NotImplementedError + name: + The header name. This is case insensitive. + + index: + The index of the value (starts at 0). You can use this to create a + header with multiple values. + + value: + The header value. + + This may be a simple string in which case it will be used as-is, or + it may be a [JSON matching + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md). + + Raises: + RuntimeError: If there was an error setting the header. + """ + success: bool = lib.pactffi_with_header_v2( + interaction._ref, + part.value, + name.encode("utf-8"), + index, + value.encode("utf-8"), + ) + if not success: + msg = f"The header {name!r} could not be specified for {interaction}." + raise RuntimeError(msg) def set_header( @@ -4733,90 +4956,115 @@ def set_header( part: InteractionPart, name: str, value: str, -) -> bool: +) -> None: """ Sets a header for the Interaction. - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started). Note that this function will overwrite - any previously set header values. Also, this function will not process the - value in any way, so matching rules and generators can not be configured - with it. + Note that this function will overwrite any previously set header values. + Also, this function will not process the value in any way, so matching rules + and generators can not be configured with it. [Rust `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_set_header) If matching rules are required to be set, use `pactffi_with_header_v2`. - * `part` - The part of the interaction to add the header to (Request or - Response). - * `name` - the header name. - * `value` - the header value. + Args: + interaction: + Handle to the Interaction. - # Safety The name and value parameters must be valid pointers to NULL - terminated strings. + part: + The part of the interaction to add the header to (Request or + Response). + + name: + The header name. This is case insensitive. + + value: + The header value. This is handled as-is, with no processing. + + Raises: + RuntimeError: If the header could not be set. """ - raise NotImplementedError + success: bool = lib.pactffi_set_header( + interaction._ref, + part.value, + name.encode("utf-8"), + value.encode("utf-8"), + ) + if not success: + msg = f"The header {name!r} could not be set for {interaction}." + raise RuntimeError(msg) -def response_status(interaction: InteractionHandle, status: int) -> bool: +def response_status(interaction: InteractionHandle, status: int) -> None: """ Configures the response for the Interaction. - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started) - [Rust `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_response_status) - * `status` - the response status. Defaults to 200. + Args: + interaction: + Handle to the Interaction. + + status: + The response status. Defaults to 200. + + Raises: + RuntimeError: If the response status could not be set. """ - raise NotImplementedError + success: bool = lib.pactffi_response_status(interaction._ref, status) + if not success: + msg = f"The response status {status} could not be set for {interaction}." + raise RuntimeError(msg) def with_body( interaction: InteractionHandle, part: InteractionPart, content_type: str, - body: str, -) -> bool: + body: str | None, +) -> None: """ Adds the body for the interaction. - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started) - [Rust `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_body) - * `part` - The part of the interaction to add the body to (Request or - Response). - * `content_type` - The content type of the body. Defaults to `text/plain`. - Will be ignored if a content type header is already set. - * `body` - The body contents. For JSON payloads, matching rules can be - embedded in the body. See - [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md) - For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous messages, the request contents will be overwritten, while a new response will be appended to the message. - # Safety + Args: + interaction: + Handle to the Interaction. - The interaction contents and content type must either be NULL pointers, or - point to valid UTF-8 encoded NULL-terminated strings. Otherwise, behaviour - is undefined. + part: + The part of the interaction to add the body to (Request or + Response). - # Error Handling + content_type: + The content type of the body. Will be ignored if a content type + header is already set. + + body: + The body contents. For JSON payloads, matching rules can be embedded + in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md). - If the contents is a NULL pointer, it will set the body contents as null. If - the content type is a null pointer, or can't be parsed, it will set the - content type as TEXT. Returns false if the interaction or Pact can't be - modified (i.e. the mock server for it has already started) or an error has - occurred. + Raises: + RuntimeError: If the body could not be specified. """ - raise NotImplementedError + success: bool = lib.pactffi_with_body( + interaction._ref, + part.value, + content_type.encode("utf-8"), + body.encode("utf-8") if body is not None else None, + ) + if not success: + msg = f"Unable to set body for {interaction}." + raise RuntimeError(msg) def with_binary_file( @@ -5271,22 +5519,24 @@ def new_async_message(pact: PactHandle, description: str) -> MessageHandle: raise NotImplementedError -def free_pact_handle(pact: PactHandle) -> int: +def free_pact_handle(pact: PactHandle) -> None: """ Delete a Pact handle and free the resources used by it. [Rust `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_free_pact_handle) - # Error Handling - - On failure, this function will return a positive integer value. - - * `1` - The handle is not valid or does not refer to a valid Pact. Could be - that it was previously deleted. - - """ - raise NotImplementedError + Raises: + RuntimeError: If the handle could not be freed. + """ + ret: int = lib.pactffi_free_pact_handle(pact._ref) + if ret == 0: + return + if ret == 1: + msg = f"{pact} is not valid or does not refer to a valid Pact." + else: + msg = f"There was an unknown error freeing {pact}." + raise RuntimeError(msg) def free_message_pact_handle(pact: MessagePactHandle) -> int: diff --git a/pact/v3/pact.py b/pact/v3/pact.py new file mode 100644 index 000000000..92e6d7cfa --- /dev/null +++ b/pact/v3/pact.py @@ -0,0 +1,788 @@ +""" +Pact between a consumer and a provider. + +This module defines the classes that are used to define a Pact between a +consumer and a provider. It defines the interactions between the two parties, +and provides the functionality to verify that the interactions are satisfied. + +For the roles of consumer and provider, see the documentation for the +`pact.v3.service` module. +""" + +from __future__ import annotations + +from collections import defaultdict +from pathlib import Path +from typing import TYPE_CHECKING, Iterable, Literal, Set + +from yarl import URL + +import pact.v3.ffi + +if TYPE_CHECKING: + from types import TracebackType + + try: + from typing import Self + except ImportError: + from typing_extensions import Self + + +class Interaction: + """ + Interaction between a consumer and a provider. + + This class defines an interaction between a consumer and a provider. It + defines a specific request that the consumer makes to the provider, and the + response that the provider should return. + + A set of interactions between a consumer and a provider is called a Pact. + """ + + def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: + """ + Initialise a new Interaction. + + This function should not be called directly. Instead, an Interaction + should be created using the + [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a + [`Pact`][pact.v3.Pact] instance. + """ + self._handle = pact.v3.ffi.new_interaction(pact_handle, description) + self._is_request = True + self._request_indices: dict[ + tuple[pact.v3.ffi.InteractionPart, str], + int, + ] = defaultdict(int) + self._parameter_indices: dict[str, int] = defaultdict(int) + + def __str__(self) -> str: + """ + Informal string representation of the Interaction. + """ + raise NotImplementedError + + def __repr__(self) -> str: + """ + Information-rich string representation of the Interaction. + """ + raise NotImplementedError + + def given(self, state: str) -> Interaction: + """ + Set the provider state. + + This is the state that the provider should be in when the Interaction is + executed. + + Args: + state: + Provider state for the Interaction. + """ + pact.v3.ffi.given(self._handle, state) + return self + + def with_request(self, method: str, path: str) -> Interaction: + """ + Set the request. + + This is the request that the consumer will make to the provider. + + Args: + method: + HTTP method for the request. + path: + Path for the request. + """ + pact.v3.ffi.with_request(self._handle, method, path) + return self + + def _interaction_part( + self, + part: Literal["Request", "Response", None], + ) -> pact.v3.ffi.InteractionPart: + """ + Convert the input into an InteractionPart. + """ + part = part or ("Request" if self._is_request else "Response") + if part == "Request": + return pact.v3.ffi.InteractionPart.REQUEST + if part == "Response": + return pact.v3.ffi.InteractionPart.RESPONSE + msg = f"Invalid part: {part}" + raise ValueError(msg) + + def with_header( + self, + name: str, + value: str, + part: Literal["Request", "Response"] | None = None, + ) -> Interaction: + r""" + Add a header to the request. + + # Repeated Headers + + If the same header has multiple values ([see RFC9110 + §5.2](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.2)), then + the same header must be specified multiple times with _order being + preserved_. For example + + ```python + ( + pact.upon_receiving("a request") + .with_header("X-Foo", "bar") + .with_header("X-Foo", "baz") + ) + ``` + + will expect a request with the following headers: + + ```http + X-Foo: bar + X-Foo: baz + # Or, equivalently: + X-Foo: bar, baz + ``` + + Note that repeated headers are _case insensitive_ in accordance with + [RFC 9110 + §5.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.1). + + # JSON Matching + + Pact's matching rules are defined in the [upstream + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md) + and support a wide range of matching rules. These can be specified + using a JSON object as a strong using `json.dumps(...)`. For example, + the above rule whereby the `X-Foo` header has multiple values can be + specified as: + + ```python + ( + pact.upon_receiving("a request") + .with_header( + "X-Foo", + json.dumps({ + "value": ["bar", "baz"], + }), + ) + ) + ``` + + It is also possible to have a more complicated Regex pattern for the + header. For example, a pattern for an `Accept-Version` header might be + specified as: + + ```python + ( + pact.upon_receiving("a request") + .with_header( + "Accept-Version", + json.dumps({ + "value": "1.2.3", + "pact:matcher:type": "regex", + "regex": r"\d+\.\d+\.\d+", + }), + ) + ) + ``` + + If the value of the header is expected to be a JSON object and clashes + with the above syntax, then it is recommended to make use of the + [`set_header(...)`][pact.v3.Interaction.set_header] method instead. + + Args: + name: + Name of the header. + + value: + Value of the header. + + part: + Whether the header should be added to the request or the + response. If `None`, then the function intelligently determines + whether the header should be added to the request or the + response, based on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + interaction_part = self._interaction_part(part) + name_lower = name.lower() + index = self._request_indices[(interaction_part, name_lower)] + self._request_indices[(interaction_part, name_lower)] += 1 + pact.v3.ffi.with_header_v2( + self._handle, + interaction_part, + name, + index, + value, + ) + return self + + def with_headers( + self, + headers: dict[str, str] | Iterable[tuple[str, str]], + part: Literal["Request", "Response"] | None = None, + ) -> Interaction: + """ + Add multiple headers to the request. + + Note that due to the requirement of Python dictionaries to + have unique keys, it is _not_ possible to specify a header multiple + times to create a multi-valued header. Instead, you may: + + - Use an alternative data structure. Any iterable of key-value pairs + is accepted, including a list of tuples, a list of lists, or a + dictionary view. + + - Make multiple calls to + [`with_header(...)`][pact.v3.Interaction.with_header] or + [`with_headers(...)`][pact.v3.Interaction.with_headers]. + + - Specify the multiple values in a JSON object of the form: + + ```python + ( + pact.upon_receiving("a request") + .with_headers({ + "X-Foo": json.dumps({ + "value": ["bar", "baz"], + }), + ) + ) + ``` + + See [`with_header(...)`][pact.v3.Interaction.with_header] for more + information. + + Args: + headers: + Headers to add to the request. + + part: + Whether the header should be added to the request or the + response. If `None`, then the function intelligently determines + whether the header should be added to the request or the + response, based on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.with_header(name, value, part) + return self + + def set_header( + self, + name: str, + value: str, + part: Literal["Request", "Response"] | None = None, + ) -> Interaction: + r""" + Add a header to the request. + + Unlike [`with_header(...)`][pact.v3.Interaction.with_header], this + function does no additional processing of the header value. This is + useful for headers that contain a JSON object. + + Args: + name: + Name of the header. + + value: + Value of the header. + + part: + Whether the header should be added to the request or the + response. If `None`, then the function intelligently determines + whether the header should be added to the request or the + response, based on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + pact.v3.ffi.set_header( + self._handle, + self._interaction_part(part), + name, + value, + ) + return self + + def set_headers( + self, + headers: dict[str, str] | Iterable[tuple[str, str]], + part: Literal["Request", "Response"] | None = None, + ) -> Interaction: + """ + Add multiple headers to the request. + + This function intelligently determines whether the header should be + added to the request or the response, based on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] method + has been called. + + See [`set_header(...)`][pact.v3.Interaction.set_header] for more + information. + + Args: + headers: + Headers to add to the request. + + part: + Whether the headers should be added to the request or the + response. If `None`, then the function intelligently determines + whether the header should be added to the request or the + response, based on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.set_header(name, value, part) + return self + + def with_query_parameter(self, name: str, value: str) -> Interaction: + r""" + Add a query to the request. + + This is the query parameter(s) that the consumer will send to the + provider. + + If the same parameter can support multiple values, then the same + parameter can be specified multiple times: + + ```python + ( + pact.upon_receiving("a request") + .with_query_parameter("name", "John") + .with_query_parameter("name", "Mary") + ) + ``` + + The above can equivalently be specified as: + + ```python + ( + pact.upon_receiving("a request") + .with_query_parameter( + "name", + json.dumps({ + "value": ["John", "Mary"], + }), + ) + ) + ``` + + It is also possible to have a more complicated Regex pattern for the + paramater. For example, a pattern for an `version` parameter might be + specified as: + + ```python + ( + pact.upon_receiving("a request") + .with_query_parameter( + "version", + json.dumps({ + "value": "1.2.3", + "pact:matcher:type": "regex", + "regex": r"\d+\.\d+\.\d+", + }), + ) + ) + ``` + + For more information on the format of the JSON object, see the [upstream + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md). + + Args: + name: + Name of the query parameter. + + value: + Value of the query parameter. + """ + index = self._parameter_indices[name] + self._parameter_indices[name] += 1 + pact.v3.ffi.with_query_parameter_v2( + self._handle, + name, + index, + value, + ) + return self + + def with_query_parameters( + self, + parameters: dict[str, str] | Iterable[tuple[str, str]], + ) -> Interaction: + """ + Add multiple query parameters to the request. + + See [`with_query_parameter(...)`][pact.v3.Interaction.with_query_parameter] + for more information. + + Args: + parameters: + Query parameters to add to the request. + """ + if isinstance(parameters, dict): + parameters = parameters.items() + for name, value in parameters: + self.with_query_parameter(name, value) + return self + + def with_body( + self, + body: str | None = None, + content_type: str = "text/plain", + part: Literal["Request", "Response"] | None = None, + ) -> Interaction: + """ + Set the body of the request. + + Args: + body: + Body of the request. If this is `None`, then the body is + empty. + + content_type: + Content type of the body. This is ignored if the `Content-Type` + header has already been set. + + part: + Whether the body should be added to the request or the response. + If `None`, then the function intelligently determines whether + the body should be added to the request or the response, based + on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + pact.v3.ffi.with_body( + self._handle, + self._interaction_part(part), + content_type, + body, + ) + return self + + def will_respond_with(self, status: int) -> Interaction: + """ + Set the response status. + + Ideally, this function is called once all of the request information has + been set. This allows functions such as + [`with_header(...)`][pact.v3.Interaction.with_header] to intelligently + determine whether this is a request or response header. + + Alternatively, the `part` argument can be used to explicitly specify + whether the header should be added to the request or the response. + + Args: + status: + Status for the response. + """ + pact.v3.ffi.response_status(self._handle, status) + self._is_request = False + return self + + +class Pact: + """ + A Pact between a consumer and a provider. + + This class defines a Pact between a consumer and a provider. It is the + central class in Pact's framework, and is responsible for defining the + interactions between the two parties. + + One Pact instance should be created for each provider that a consumer + interacts with. This instance can then be used to define the interactions + between the two parties. + """ + + def __init__( + self, + consumer: str, + provider: str, + ) -> None: + """ + Initialise a new Pact. + + Args: + consumer: + Name of the consumer. + + provider: + Name of the provider. + """ + if not consumer: + msg = "Consumer name cannot be empty." + raise ValueError(msg) + if not provider: + msg = "Provider name cannot be empty." + raise ValueError(msg) + + self._consumer = consumer + self._provider = provider + self._interactions: Set[Interaction] = set() + self._handle: pact.v3.ffi.PactHandle = pact.v3.ffi.new_pact( + consumer, + provider, + ) + + def __str__(self) -> str: + """ + Informal string representation of the Pact. + """ + return f"{self.consumer} -> {self.provider}" + + def __repr__(self) -> str: + """ + Information-rich string representation of the Pact. + """ + return f"Pact({self})" + + @property + def consumer(self) -> str: + """ + Consumer name. + """ + return self._consumer + + @property + def provider(self) -> str: + """ + Provider name. + """ + return self._provider + + def upon_receiving(self, description: str) -> Interaction: + """ + Create a new Interaction. + + This is an alias for [`interaction(...)`][pact.v3.Pact.interaction]. + + Args: + description: + Description of the interaction. This must be unique + within the Pact. + """ + return Interaction(self._handle, description) + + def serve( + self, + addr: str = "localhost", + port: int = 0, + transport: str = "http", + transport_config: str | None = None, + ) -> PactServer: + """ + Return a mock server for the Pact. + + This function configures a mock server for the Pact. The mock server + is then started when the Pact is entered into a `with` block: + + ```python + pact = Pact("consumer", "provider") + with pact.serve() as srv: + ... + ``` + + Args: + addr: + Address to bind the mock server to. Defaults to `localhost`. + + port: + Port to bind the mock server to. Defaults to `0`, which will + select a random port. + + transport: + Transport to use for the mock server. Defaults to `HTTP`. + + transport_config: + Configuration for the transport. This is specific to the + transport being used and should be a JSON string. + """ + return PactServer( + self._handle, + addr, + port, + transport, + transport_config, + ) + + +class PactServer: + """ + Pact Server. + + This class handles the lifecycle of the Pact mock server. It is responsible + for starting the mock server when the Pact is entered into a `with` block, + and stopping the mock server when the block is exited. + """ + + def __init__( # noqa: PLR0913 + self, + pact_handle: pact.v3.ffi.PactHandle, + host: str = "localhost", + port: int = 0, + transport: str = "HTTP", + transport_config: str | None = None, + ) -> None: + """ + Initialise a new Pact Server. + + This function should not be called directly. Instead, a Pact Server + should be created using the + [`serve(...)`][pact.v3.Pact.serve] method of a + [`Pact`][pact.v3.Pact] instance: + + ```python + pact = Pact("consumer", "provider") + with pact.serve(...) as srv: + ... + ``` + + Args: + pact_handle: + Handle for the Pact. + + host: + Hostname of IP for the mock server. + + port: + Port to bind the mock server to. The value of `0` will select a + random available port. + + transport: + Transport to use for the mock server. + + transport_config: + Configuration for the transport. This is specific to the + transport being used and should be a JSON string. + """ + self._host = host + self._port = port + self._transport = transport + self._transport_config = transport_config + self._pact_handle = pact_handle + self._handle: None | pact.v3.ffi.PactServerHandle = None + + @property + def port(self) -> int: + """ + Port on which the server is running. + + If the server is not running, then this will be `0`. + """ + # Unlike the other properties, this value might be different to what was + # passed in to the constructor as the server can be started on a random + # port. + return self._handle.port if self._handle else 0 + + @property + def host(self) -> str: + """ + Address to which the server is bound. + """ + return self._host + + @property + def transport(self) -> str: + """ + Transport method. + """ + return self._transport + + @property + def url(self) -> URL: + """ + Base URL for the server. + """ + return URL(str(self)) + + def __str__(self) -> str: + """ + URL for the server. + """ + return f"{self.transport}://{self.host}:{self.port}" + + def __repr__(self) -> str: + """ + Information-rich string representation of the Pact Server. + """ + return f"PactServer({self})" + + def __enter__(self) -> Self: + """ + Launch the server. + + Once the server is running, it is generally no possible to make + modifications to the underlying Pact. + """ + self._handle = pact.v3.ffi.create_mock_server_for_transport( + self._pact_handle, + self._host, + self._port, + self._transport, + self._transport_config, + ) + + return self + + def __exit__( + self, + _exc_type: type[BaseException] | None, + _exc_value: BaseException | None, + _traceback: TracebackType | None, + ) -> None: + """ + Stop the server. + """ + if self._handle: + self._handle = None + + def __truediv__(self, other: str) -> URL: + """ + URL for the server. + """ + if isinstance(other, str): + return self.url / other + return NotImplemented + + def write_file( + self, + directory: str | Path | None = None, + *, + overwrite: bool = False, + ) -> None: + """ + Write out the pact to a file. + + Args: + directory: + The directory to write the pact to. If the directory does not + exist, it will be created. The filename will be + automatically generated from the underlying Pact. + + overwrite: + Whether or not to overwrite the file if it already exists. + """ + if not self._handle: + msg = "The server is not running." + raise RuntimeError(msg) + + directory = Path(directory) if directory else Path.cwd() + if not directory.exists(): + directory.mkdir(parents=True) + elif not directory.is_dir(): + msg = f"{directory} is not a directory" + raise ValueError(msg) + + pact.v3.ffi.write_pact_file( + self._handle, + str(directory), + overwrite=overwrite, + ) diff --git a/pyproject.toml b/pyproject.toml index 5be607eb6..e3e13f942 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "psutil ~= 5.9", "requests ~= 2.31", "six ~= 1.16", + "typing-extensions ~= 4.8 ; python_version < '3.10'", "uvicorn ~= 0.13", ] @@ -53,14 +54,16 @@ types = [ "types-requests ~= 2.31", ] test = [ - "coverage[toml] ~= 7.3", - "flask[async] ~= 2.3", - "httpx ~= 0.24", - "mock ~= 5.1", - "pytest ~= 7.4", - "pytest-cov ~= 4.1", - "testcontainers ~= 3.7", - "yarl ~= 1.9", + "aiohttp[speedups] ~= 3.8", + "coverage[toml] ~= 7.3", + "flask[async] ~= 2.3", + "httpx ~= 0.24", + "mock ~= 5.1", + "pytest ~= 7.4", + "pytest-asyncio ~= 0.21", + "pytest-cov ~= 4.1", + "testcontainers ~= 3.7", + "yarl ~= 1.9", ] dev = [ "pact-python[types]", @@ -142,9 +145,10 @@ filterwarnings = [ [tool.coverage.report] exclude_lines = [ - "if __name__ == .__main__.:", - "if TYPE_CHECKING:", - "pragma: no cover", + "if __name__ == .__main__.:", # Ignore non-runnable code + "if TYPE_CHECKING:", # Ignore typing + "raise NotImplementedError", # Ignore defensive assertions + "@(abc\\.)?abstractmethod", # Ignore abstract methods ] ################################################################################ diff --git a/tests/v3/conftest.py b/tests/v3/conftest.py new file mode 100644 index 000000000..e76e3be75 --- /dev/null +++ b/tests/v3/conftest.py @@ -0,0 +1,18 @@ +""" +PyTest configuration file for the v3 API tests. + +This file is loaded automatically by PyTest when running the tests in this +directory. +""" + +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def _setup_pact_logging() -> None: + """ + Set up logging for the pact package. + """ + from pact.v3 import ffi + + ffi.log_to_stderr(ffi.LevelFilter.DEBUG) diff --git a/tests/v3/test_pact.py b/tests/v3/test_pact.py new file mode 100644 index 000000000..92049b6a6 --- /dev/null +++ b/tests/v3/test_pact.py @@ -0,0 +1,431 @@ +""" +Pact unit tests. +""" + +from __future__ import annotations + +import json + +import aiohttp +import pytest +from pact.v3 import Pact + + +@pytest.fixture() +def pact() -> Pact: + """ + Fixture for a Pact instance. + """ + return Pact("consumer", "provider") + + +def test_init(pact: Pact) -> None: + assert pact.consumer == "consumer" + assert pact.provider == "provider" + + +def test_empty_consumer() -> None: + with pytest.raises(ValueError, match="Consumer name cannot be empty"): + Pact("", "provider") + + +def test_empty_provider() -> None: + with pytest.raises(ValueError, match="Provider name cannot be empty"): + Pact("consumer", "") + + +def test_serve(pact: Pact) -> None: + with pact.serve() as srv: + assert srv.port > 0 + assert srv.host == "localhost" + assert str(srv).startswith("http://localhost") + assert srv.url.scheme == "http" + assert srv.url.host == "localhost" + assert srv.url.path == "/" + assert srv / "foo" == srv.url / "foo" + assert str(srv / "foo") == f"http://localhost:{srv.port}/foo" + + +@pytest.mark.parametrize( + "method", + [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "HEAD", + "OPTIONS", + "TRACE", + "CONNECT", + ], +) +@pytest.mark.asyncio() +async def test_basic_request_method(pact: Pact, method: str) -> None: + ( + pact.upon_receiving(f"a basic {method} request") + .with_request(method, "/") + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request(method, "/") as resp: + assert resp.status == 200 + + +@pytest.mark.parametrize( + "status", + list(range(200, 600, 13)), +) +@pytest.mark.asyncio() +async def test_basic_response_status(pact: Pact, status: int) -> None: + ( + pact.upon_receiving(f"a basic request producing status {status}") + .with_request("GET", "/") + .will_respond_with(status) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request("GET", "/") as resp: + assert resp.status == status + + +@pytest.mark.parametrize( + "headers", + [ + [("X-Test", "true")], + [("X-Foo", "true"), ("X-Bar", "true")], + [("X-Test", "1"), ("X-Test", "2")], + ], +) +@pytest.mark.asyncio() +async def test_with_header_request( + pact: Pact, + headers: list[tuple[str, str]], +) -> None: + ( + pact.upon_receiving("a basic request with a header") + .with_request("GET", "/") + .with_headers(headers) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + headers=headers, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.parametrize( + "headers", + [ + [("X-Test", "true")], + [("X-Foo", "true"), ("X-Bar", "true")], + [("X-Test", "1"), ("X-Test", "2")], + ], +) +@pytest.mark.asyncio() +async def test_with_header_response( + pact: Pact, + headers: list[tuple[str, str]], +) -> None: + ( + pact.upon_receiving("a basic request with a header") + .with_request("GET", "/") + .will_respond_with(200) + .with_headers(headers) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + ) as resp: + assert resp.status == 200 + response_headers = [(h.lower(), v) for h, v in resp.headers.items()] + for header, value in headers: + assert (header.lower(), value) in response_headers + + +@pytest.mark.asyncio() +async def test_with_header_dict(pact: Pact) -> None: + ( + pact.upon_receiving("a basic request with a headers from a dict") + .with_request("GET", "/") + .with_headers({"X-Test": "true", "X-Foo": "bar"}) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + headers={"X-Test": "true", "X-Foo": "bar"}, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.parametrize( + "headers", + [ + [("X-Test", "true")], + [("X-Foo", "true"), ("X-Bar", "true")], + ], +) +@pytest.mark.asyncio() +async def test_set_header_request( + pact: Pact, + headers: list[tuple[str, str]], +) -> None: + ( + pact.upon_receiving("a basic request with a header") + .with_request("GET", "/") + .set_headers(headers) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + headers=headers, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.asyncio() +async def test_set_header_request_repeat( + pact: Pact, +) -> None: + headers = [("X-Test", "1"), ("X-Test", "2")] + ( + pact.upon_receiving("a basic request with a header") + .with_request("GET", "/") + # As set_headers makes no additional processing, the last header will be + # the one that is used. + .set_headers(headers) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + headers=headers, + ) as resp: + assert resp.status == 500 + + +@pytest.mark.parametrize( + "headers", + [ + [("X-Test", "true")], + [("X-Foo", "true"), ("X-Bar", "true")], + ], +) +@pytest.mark.asyncio() +async def test_set_header_response( + pact: Pact, + headers: list[tuple[str, str]], +) -> None: + ( + pact.upon_receiving("a basic request with a header") + .with_request("GET", "/") + .will_respond_with(200) + .set_headers(headers) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + ) as resp: + assert resp.status == 200 + response_headers = [(h.lower(), v) for h, v in resp.headers.items()] + for header, value in headers: + assert (header.lower(), value) in response_headers + + +@pytest.mark.asyncio() +async def test_set_header_response_repeat( + pact: Pact, +) -> None: + headers = [("X-Test", "1"), ("X-Test", "2")] + ( + pact.upon_receiving("a basic request with a header") + .with_request("GET", "/") + .will_respond_with(200) + # As set_headers makes no additional processing, the last header will be + # the one that is used. + .set_headers(headers) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + headers=headers, + ) as resp: + assert resp.status == 200 + response_headers = [(h.lower(), v) for h, v in resp.headers.items()] + assert ("x-test", "2") in response_headers + assert ("x-test", "1") not in response_headers + + +@pytest.mark.asyncio() +async def test_set_header_dict(pact: Pact) -> None: + ( + pact.upon_receiving("a basic request with a headers from a dict") + .with_request("GET", "/") + .set_headers({"X-Test": "true", "X-Foo": "bar"}) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + headers={"X-Test": "true", "X-Foo": "bar"}, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.parametrize( + "query", + [ + [("test", "true")], + [("foo", "true"), ("bar", "true")], + [("test", "1"), ("test", "2")], + ], +) +@pytest.mark.asyncio() +async def test_with_query_parameter_request( + pact: Pact, + query: list[tuple[str, str]], +) -> None: + ( + pact.upon_receiving("a basic request with a query parameter") + .with_request("GET", "/") + .with_query_parameters(query) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + url = srv.url.with_query(query) + async with session.request( + "GET", + url.path_qs, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.asyncio() +async def test_with_query_parameter_dict(pact: Pact) -> None: + ( + pact.upon_receiving("a basic request with a query parameter from a dict") + .with_request("GET", "/") + .with_query_parameters({"test": "true", "foo": "bar"}) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + url = srv.url.with_query({"test": "true", "foo": "bar"}) + async with session.request( + "GET", + url.path_qs, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.parametrize( + "method", + ["GET", "POST", "PUT"], +) +@pytest.mark.asyncio() +async def test_with_body_request(pact: Pact, method: str) -> None: + ( + pact.upon_receiving(f"a basic {method} request with a body") + .with_request(method, "/") + .with_body(json.dumps({"test": True}), "application/json") + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + method, + "/", + json={"test": True}, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.parametrize( + "method", + ["GET", "POST", "PUT"], +) +@pytest.mark.asyncio() +async def test_with_body_response(pact: Pact, method: str) -> None: + ( + pact.upon_receiving( + f"a basic {method} request expecting a response with a body", + ) + .with_request(method, "/") + .will_respond_with(200) + .with_body(json.dumps({"test": True}), "application/json") + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + method, + "/", + json={"test": True}, + ) as resp: + assert resp.status == 200 + assert await resp.json() == {"test": True} + + +@pytest.mark.asyncio() +async def test_with_body_explicit(pact: Pact) -> None: + ( + pact.upon_receiving("") + .with_request("GET", "/") + .will_respond_with(200) + .with_body(json.dumps({"request": True}), "application/json", "Request") + .with_body(json.dumps({"response": True}), "application/json", "Response") + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + json={"request": True}, + ) as resp: + assert resp.status == 200 + assert await resp.json() == {"response": True} + + +def test_with_body_invalid(pact: Pact) -> None: + with pytest.raises(ValueError, match="Invalid part: Invalid"): + ( + pact.upon_receiving("") + .with_request("GET", "/") + .will_respond_with(200) + .with_body(json.dumps({"request": True}), "application/json", "Invalid") + ) + + +@pytest.mark.asyncio() +async def test_given(pact: Pact) -> None: + ( + pact.upon_receiving("a basic request given state 1") + .given("state 1") + .with_request("GET", "/") + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request("GET", "/") as resp: + assert resp.status == 200 From a07097825b896212df6b71e9b728704d78790e90 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Thu, 12 Oct 2023 17:43:53 +0100 Subject: [PATCH 0067/1376] ci: add g++ to cirrus linux image --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index d889977b0..a52b4f0d5 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -20,7 +20,7 @@ linux_arm64_task: image: $IMAGE install_script: - apt update --yes - - apt install --yes gcc make + - apt install --yes gcc make g++ - python -m pip install --upgrade pip pipx - pipx install hatch <<: *TEST_TEMPLATE From 11c385922683ef703744dd829d2ca7a764f6d66c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 20 Oct 2023 14:53:29 +1100 Subject: [PATCH 0068/1376] chore: add label sync This commit adds label synchronisation, pulling the labels from `pact-foundation/.github` and merging them with the labels in this repository's `.github/labels.yml`. Signed-off-by: JP-Ellis --- .github/labels.yml | 13 +++++++++++++ .github/workflows/labels.yml | 30 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 .github/labels.yml create mode 100644 .github/workflows/labels.yml diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 000000000..f8183813c --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,13 @@ +- name: area:v3 + description: Relating to the pact.v3 module + color: "C2E0C6" + +- name: area:v2 + description: Relating to v2 code + color: "C2E0C6" + aliases: + - documentation + +- name: area:examples + description: Relating to the examples + color: "C2E0C6" diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml new file mode 100644 index 000000000..be54b1169 --- /dev/null +++ b/.github/workflows/labels.yml @@ -0,0 +1,30 @@ +name: Labels + +on: + # For downstream repos, we want to run this on a schedule + # so that updates propagate automatically. Weekly is probably + # enough. + schedule: + - cron: "20 0 * * 0" + push: + branches: + - master + paths: + - .github/labels.yml + +jobs: + sync-labels: + name: Synchronise labels + + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Synchronize labels + uses: EndBug/label-sync@v2 + with: + config-file: | + https://raw.githubusercontent.com/pact-foundation/.github/master/.github/labels.yml + .github/labels.yml From c0f221641696075a6f9103bc84bbd63275848ff4 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 19 Oct 2023 13:28:02 +1100 Subject: [PATCH 0069/1376] fix(v3): unconventional __repr__ implementation The __repr__ implementation should produce one of: 1. A valid Python string which could be passed to `eval()` to generate the same object. 2. A string of the form `<{class_name}: {info}>`. A few of the implementations did not quite adhere to this convention and have been fixed. Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 8 ++++---- pact/v3/pact.py | 22 ++++++++++++++++++++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 5dc0aebea..5a64040bb 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -150,7 +150,7 @@ def __repr__(self) -> str: """ String representation of the Interaction Handle. """ - return f"InteractionHandle({self._ref})" + return f"InteractionHandle({self._ref!r})" class MatchingRule: @@ -267,7 +267,7 @@ def __repr__(self) -> str: """ String representation of the Pact Handle. """ - return f"PactHandle({self._ref})" + return f"PactHandle({self._ref!r})" class PactServerHandle: @@ -309,7 +309,7 @@ def __repr__(self) -> str: """ String representation of the Pact Server Handle. """ - return f"PactServerHandle({self._ref})" + return f"PactServerHandle({self._ref!r})" @property def port(self) -> int: @@ -525,7 +525,7 @@ def __repr__(self) -> str: """ Information-rich string representation of the Pact Specification. """ - return f"Pact Specification.{self.name}" + return f"PactSpecification.{self.name}" class StringResult(Enum): diff --git a/pact/v3/pact.py b/pact/v3/pact.py index 92e6d7cfa..10997ecac 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -542,7 +542,15 @@ def __repr__(self) -> str: """ Information-rich string representation of the Pact. """ - return f"Pact({self})" + return "".format( + ", ".join( + [ + f"consumer={self.consumer!r}", + f"provider={self.provider!r}", + f"handle={self._handle!r}", + ], + ), + ) @property def consumer(self) -> str: @@ -713,7 +721,17 @@ def __repr__(self) -> str: """ Information-rich string representation of the Pact Server. """ - return f"PactServer({self})" + return "".format( + ", ".join( + [ + f"transport={self.transport!r}", + f"host={self.host!r}", + f"port={self.port!r}", + f"handle={self._handle!r}", + f"pact={self._pact_handle!r}", + ], + ), + ) def __enter__(self) -> Self: """ From da0989e15951eaf8908798f3b67f876fcafb5a0b Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 19 Oct 2023 13:37:59 +1100 Subject: [PATCH 0070/1376] chore(test): automatically generated xml coverage This can be used by other tools to generate coverage reports, or to annotate code within the IDE. Signed-off-by: JP-Ellis --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index e3e13f942..62db1dd93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,6 +131,7 @@ addopts = [ "--import-mode=importlib", "--cov-config=pyproject.toml", "--cov=pact", + "--cov-report=xml", ] filterwarnings = [ "ignore::DeprecationWarning:pact", From af0681569352ff4dcc8c73601d741960e0342edf Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 19 Oct 2023 13:40:54 +1100 Subject: [PATCH 0071/1376] feat(v3): implement interaction methods This commit adds implementation for all FFI functions which act on an `InteractionHandle`. Most of these map in Python to a method of the `Interaction` class. As the InteractionHandle can point to a HTTP, Sync Message or Async Message Interaction, the initial `Interaction` class has been converted into an abstract class, and then subclassed into three new concrete classes. This will help end-users specifically when it comes to auto-completions within the IDE. Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 528 ++++++++++++++++++------- pact/v3/pact.py | 611 +++++++++++++++++++++++++---- tests/conftest.py | 45 +++ tests/v3/conftest.py | 3 +- tests/v3/test_async_interaction.py | 34 ++ tests/v3/test_ffi.py | 38 ++ tests/v3/test_http_interaction.py | 547 ++++++++++++++++++++++++++ tests/v3/test_pact.py | 388 ------------------ tests/v3/test_sync_interaction.py | 34 ++ 9 files changed, 1606 insertions(+), 622 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/v3/test_async_interaction.py create mode 100644 tests/v3/test_http_interaction.py create mode 100644 tests/v3/test_sync_interaction.py diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 5a64040bb..e1426feec 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -67,6 +67,7 @@ appropriate Python exception class, and should be documented in the function's docstring. """ + # The following lints are disabled during initial development and should be # removed later. # ruff: noqa: ARG001 (unused-function-argument) @@ -80,6 +81,7 @@ from __future__ import annotations +import gc import warnings from enum import Enum from typing import TYPE_CHECKING, List @@ -528,7 +530,7 @@ def __repr__(self) -> str: return f"PactSpecification.{self.name}" -class StringResult(Enum): +class _StringResult(Enum): """ String Result. @@ -548,7 +550,70 @@ def __repr__(self) -> str: """ Information-rich string representation of the String Result. """ - return f"StringResult.{self.name}" + return f"_StringResultEnum.{self.name}" + + +class StringResult: + """ + String result. + """ + + def __init__(self, cdata: ffi.CData) -> None: + """ + Initialise a new String Result. + + Args: + cdata: + CFFI data structure. + """ + if ffi.typeof(cdata).cname != "struct StringResult": + msg = f"cdata must be a struct StringResult, got {ffi.typeof(cdata).cname}" + raise TypeError(msg) + self._cdata: ffi.CData = cdata + + def __str__(self) -> str: + """ + String representation of the String Result. + """ + return self.text + + def __repr__(self) -> str: + """ + Debugging string representation of the String Result. + """ + return f"" + + @property + def is_failed(self) -> bool: + """ + Whether the result is an error. + """ + return self._cdata.tag == _StringResult.FAILED.value + + @property + def is_ok(self) -> bool: + """ + Whether the result is ok. + """ + return self._cdata.tag == _StringResult.OK.value + + @property + def text(self) -> str: + """ + The text of the result. + """ + # The specific `.ok` or `.failed` does not matter. + return ffi.string(self._cdata.ok).decode("utf-8") + + def raise_exception(self) -> None: + """ + Raise an exception with the text of the result. + + Raises: + RuntimeError: If the result is an error. + """ + if self.is_failed: + raise RuntimeError(self.text) def version() -> str: @@ -614,7 +679,11 @@ def enable_ansi_support() -> None: raise NotImplementedError -def log_message(source: str, log_level: str, message: str) -> None: +def log_message( + message: str, + log_level: LevelFilter | str = LevelFilter.ERROR, + source: str | None = None, +) -> None: """ Log using the shared core logging facility. @@ -623,16 +692,27 @@ def log_message(source: str, log_level: str, message: str) -> None: This is useful for callers to have a single set of logs. - - `source`: String. The source of the log, such as the class or caller - framework to disambiguate log lines from the rust logging (e.g. pact_go) - - `log_level`: String. One of TRACE, DEBUG, INFO, WARN, ERROR - - `message`: Message to log + Args: + message: + The contents written to the log - # Safety + log_level: + The verbosity at which this message should be logged. - This function will fail if any of the pointers passed to it are invalid. + source: + The source of the log, such as the class, module or caller. """ - raise NotImplementedError + if isinstance(log_level, str): + log_level = LevelFilter[log_level.upper()] + if source is None: + import inspect + + source = inspect.stack()[1].function + lib.pactffi_log_message( + source.encode("utf-8"), + log_level.name.encode("utf-8"), + message.encode("utf-8"), + ) def match_message(msg_1: Message, msg_2: Message) -> Mismatches: @@ -731,7 +811,7 @@ def mismatch_ansi_description(mismatch: Mismatch) -> str: raise NotImplementedError -def get_error_message(buffer: str, length: int) -> int: +def get_error_message(length: int = 1024) -> str | None: """ Provide the error message from `LAST_ERROR` to the calling C code. @@ -747,37 +827,40 @@ def get_error_message(buffer: str, length: int) -> int: type. If you want more detailed information for debugging purposes, use the logging interface. - # Params - - - `buffer`: a pointer to an array of `char` of sufficient length to hold the - error message. - - `length`: an int providing the length of the `buffer`. - - # Return Codes - - - The number of bytes written to the provided buffer, which may be zero if - there is no last error. - - `-1` if the provided buffer is a null pointer. - - `-2` if the provided buffer length is too small for the error message. - - `-3` if the write failed for some other reason. - - `-4` if the error message had an interior NULL - - # Notes - - Note that this function zeroes out any excess in the provided buffer. - - # Error Handling + Args: + length: + The length of the buffer to allocate for the error message. If the + error message is longer than this, it will be truncated. - The return code must be checked for one of the negative number error codes - before the buffer is used. If an error code is present, the buffer may not - be in a usable state. + Returns: + A string containing the error message, or None if there is no error + message. - If the buffer is longer than needed for the error message, the excess space - will be zeroed as a safety mechanism. This is slightly less efficient than - leaving the contents of the buffer alone, but the difference is expected to - be negligible in practice. + Raises: + RuntimeError: If the error message could not be retrieved. """ - raise NotImplementedError + buffer = ffi.new("char[]", length) + ret: int = lib.pactffi_get_error_message(buffer, length) + + if ret >= 0: + # While the documentation says that the return value is the number of bytes + # written, the actually return value is always 0 on success. + if msg := ffi.string(buffer).decode("utf-8"): + return msg + return None + if ret == -1: + msg = "The provided buffer is a null pointer." + elif ret == -2: # noqa: PLR2004 + # Instead of returning an error here, we call the function again with a + # larger buffer. + return get_error_message(length * 32) + elif ret == -3: # noqa: PLR2004 + msg = "The write failed for some other reason." + elif ret == -4: # noqa: PLR2004 + msg = "The error message had an interior NULL." + else: + msg = "An unknown error occurred." + raise RuntimeError(msg) def log_to_stdout(level_filter: LevelFilter) -> int: @@ -807,7 +890,7 @@ def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: """ if isinstance(level_filter, str): level_filter = LevelFilter[level_filter.upper()] - ret = lib.pactffi_log_to_stderr(level_filter.value) + ret: int = lib.pactffi_log_to_stderr(level_filter.value) if ret != 0: msg = "There was an unknown error setting the logger." raise RuntimeError(msg) @@ -828,13 +911,18 @@ def log_to_file(file_name: str, level_filter: LevelFilter) -> int: raise NotImplementedError -def log_to_buffer(level_filter: LevelFilter) -> int: +def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: """ Convenience function to direct all logging to a task local memory buffer. [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_to_buffer) """ - raise NotImplementedError + if isinstance(level_filter, str): + level_filter = LevelFilter[level_filter.upper()] + ret: int = lib.pactffi_log_to_buffer(level_filter.value) + if ret != 0: + msg = "There was an unknown error setting the logger." + raise RuntimeError(msg) def logger_init() -> None: @@ -1994,10 +2082,12 @@ def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: raise NotImplementedError -def validate_datetime(value: str, format: str) -> int: +def validate_datetime(value: str, format: str) -> None: """ Validates the date/time value against the date/time format string. + Raises an error if the value is not a valid date/time for the format string. + If the value is valid, this function will return a zero status code (EXIT_SUCCESS). If the value is not valid, will return a value of 1 (EXIT_FAILURE) and set the error message which can be retrieved with @@ -2014,7 +2104,17 @@ def validate_datetime(value: str, format: str) -> int: This function is safe as long as the value and format parameters point to valid NULL-terminated strings. """ - raise NotImplementedError + ret = lib.pactffi_validate_datetime(value.encode(), format.encode()) + if ret == 0: + return + if ret == 1: + msg = f"Invalid datetime value {value!r}' for format {format!r}" + raise ValueError(msg) + if ret == 2: # noqa: PLR2004 + msg = f"Panic while validating datetime value: {get_error_message()}" + else: + msg = f"Unknown error while validating datetime value: {ret}" + raise RuntimeError(msg) def generator_to_json(generator: Generator) -> str: @@ -4524,8 +4624,7 @@ def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: Handle to the Pact model. description: - The interaction description. It needs to be unique for each - interaction. + The interaction description. It needs to be unique for each Pact. Returns: Handle to the new Interaction. @@ -4542,15 +4641,26 @@ def new_message_interaction(pact: PactHandle, description: str) -> InteractionHa """ Creates a new message interaction and return a handle to it. - * `description` - The interaction description. It needs to be unique for - each interaction. - [Rust `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_message_interaction) - Returns a new `InteractionHandle`. + Args: + pact: + Handle to the Pact model. + + description: + The interaction description. It needs to be unique for each + Pact. + + Returns: + Handle to the new Interaction """ - raise NotImplementedError + return InteractionHandle( + lib.pactffi_new_message_interaction( + pact._ref, + description.encode("utf-8"), + ), + ) def new_sync_message_interaction( @@ -4560,32 +4670,63 @@ def new_sync_message_interaction( """ Creates a new synchronous message interaction and return a handle to it. - * `description` - The interaction description. It needs to be unique for - each interaction. - [Rust `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_sync_message_interaction) - Returns a new `InteractionHandle`. + Args: + pact: + Handle to the Pact model. + + description: + The interaction description. It needs to be unique for each Pact. + + Returns: + Handle to the new Interaction """ - raise NotImplementedError + return InteractionHandle( + lib.pactffi_new_sync_message_interaction( + pact._ref, + description.encode("utf-8"), + ), + ) -def upon_receiving(interaction: InteractionHandle, description: str) -> bool: +def upon_receiving(interaction: InteractionHandle, description: str) -> None: """ Sets the description for the Interaction. - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started) - [Rust `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_upon_receiving) - * `description` - The interaction description. It needs to be unique for - each interaction. + This function + + Args: + interaction: + Handle to the Interaction. + + description: + The interaction description. It needs to be unique for each Pact. + + Raises: + RuntimeError: If the interaction description could not be set. """ + # This function has intentionally been left unimplemented. The rationale is + # to avoid code of the form: + # + # ```python + # .with_request("GET", "/") + # .upon_receiving("some new description") + # ``` raise NotImplementedError + success: bool = lib.pactffi_upon_receiving( + interaction._ref, + description.encode("utf-8"), + ) + if not success: + msg = "The interaction description could not be set." + raise RuntimeError(msg) + def given(interaction: InteractionHandle, description: str) -> None: """ @@ -4610,7 +4751,7 @@ def given(interaction: InteractionHandle, description: str) -> None: raise RuntimeError(msg) -def interaction_test_name(interaction: InteractionHandle, test_name: str) -> int: +def interaction_test_name(interaction: InteractionHandle, test_name: str) -> None: """ Sets the test name annotation for the interaction. @@ -4620,10 +4761,20 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> int [Rust `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_interaction_test_name) + Args: + interaction: + Handle to the Interaction. + + test_name: + The test name to set. + # Safety The test name parameter must be a valid pointer to a NULL terminated string. + Raises: + RuntimeError: If the test name can not be set. + # Error Handling If the test name can not be set, this will return a positive value. @@ -4635,7 +4786,23 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> int modified. * `4` - Not a V4 interaction. """ - raise NotImplementedError + ret: int = lib.pactffi_interaction_test_name( + interaction._ref, + test_name.encode("utf-8"), + ) + if ret == 0: + return + if ret == 1: + msg = f"Function panicked: {get_error_message()}" + elif ret == 2: # noqa: PLR2004 + msg = f"Invalid handle: {interaction}." + elif ret == 3: # noqa: PLR2004 + msg = f"Mock server for {interaction} has already started." + elif ret == 4: # noqa: PLR2004 + msg = f"Interaction {interaction} is not a V4 interaction." + else: + msg = f"Unknown error setting test name for {interaction}." + raise RuntimeError(msg) def given_with_param( @@ -4643,7 +4810,7 @@ def given_with_param( description: str, name: str, value: str, -) -> bool: +) -> None: """ Adds a parameter key and value to a provider state to the Interaction. @@ -4654,23 +4821,38 @@ def given_with_param( [Rust `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_given_with_param) - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started). + Args: + interaction: + Handle to the Interaction. + + description: + The provider state description. - # Parameters + name: + Parameter name. - * `description` - The provider state description. It needs to be unique. - * `name` - Parameter name. - * `value` - Parameter value as JSON. + value: + Parameter value as JSON. + + Raises: + RuntimeError: If the interaction state could not be updated. """ - raise NotImplementedError + success: bool = lib.pactffi_given_with_param( + interaction._ref, + description.encode("utf-8"), + name.encode("utf-8"), + value.encode("utf-8"), + ) + if not success: + msg = "The interaction state could not be updated." + raise RuntimeError(msg) def given_with_params( interaction: InteractionHandle, description: str, params: str, -) -> int: +) -> None: """ Adds a provider state to the Interaction. @@ -4680,10 +4862,18 @@ def given_with_params( [Rust `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_given_with_params) - # Parameters + Args: + interaction: + Handle to the Interaction. + + description: + The provider state description. + + params: + Parameter values as a JSON fragment. - * `description` - The provider state description. - * `params` - Parameter values as a JSON fragment. + Raises: + RuntimeError: If the interaction state could not be updated. # Errors @@ -4697,7 +4887,22 @@ def given_with_params( Returns 3 if any of the C strings are not valid. """ - raise NotImplementedError + ret: int = lib.pactffi_given_with_params( + interaction._ref, + description.encode("utf-8"), + params.encode("utf-8"), + ) + if ret == 0: + return + if ret == 1: + msg = "The interaction state could not be updated." + elif ret == 2: # noqa: PLR2004 + msg = f"Internal error: {get_error_message()}" + elif ret == 3: # noqa: PLR2004 + msg = "Invalid C string." + else: + msg = "Unknown error." + raise RuntimeError(msg) def with_request(interaction: InteractionHandle, method: str, path: str) -> None: @@ -5071,9 +5276,8 @@ def with_binary_file( interaction: InteractionHandle, part: InteractionPart, content_type: str, - body: List[int], - size: int, -) -> bool: + body: bytes | None, +) -> None: """ Adds a binary file as the body with the expected content type and contents. @@ -5084,41 +5288,53 @@ def with_binary_file( [Rust `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_binary_file) - * `interaction` - Interaction handle to set the body for. - * `part` - Request or response part. - * `content_type` - Expected content type. - * `body` - example body contents in bytes - * `size` - number of bytes in the body - For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous messages, the request contents will be overwritten, while a new response will be appended to the message. - # Safety + Args: + interaction: + Handle to the Interaction. - The content type must be a valid UTF-8 encoded NULL-terminated string. The - body pointer must be valid for reads of `size` bytes, and it must be - properly aligned and consecutive. + part: + The part of the interaction to add the body to (Request or + Response). - # Error Handling + content_type: + The content type of the body. Will be ignored if a content type + header is already set. - If the body is a NULL pointer, it will set the body contents as null. If the - content type is a null pointer, or can't be parsed, it will return false. - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started) or an error has occurred. - """ - raise NotImplementedError + body: + The body contents. If `None`, the body will be set to null. + """ + if len(gc.get_referrers(body)) == 0: + warnings.warn( + "Make sure to assign the body to a variable to avoid having the byte array" + " modified.", + UserWarning, + stacklevel=3, + ) + success: bool = lib.pactffi_with_binary_file( + interaction._ref, + part.value, + content_type.encode("utf-8"), + body if body else ffi.NULL, + len(body) if body else 0, + ) + if not success: + msg = f"Unable to set body for {interaction}." + raise RuntimeError(msg) def with_multipart_file_v2( # noqa: PLR0913 interaction: InteractionHandle, part: InteractionPart, content_type: str, - file: str, + file: Path | None, part_name: str, - boundary: str, -) -> StringResult: + boundary: str | None, +) -> None: """ Adds a binary file as the body as a MIME multipart. @@ -5129,34 +5345,41 @@ def with_multipart_file_v2( # noqa: PLR0913 [Rust `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_multipart_file_v2) - * `interaction` - Interaction handle to set the body for. - * `part` - Request or response part. - * `content_type` - Expected content type of the file. - * `file` - path to the example file - * `part_name` - name for the mime part - * `boundary` - boundary for the multipart separation - This function can be called multiple times. In that case, each subsequent call will be appended to the existing multipart body as a new part. - # Safety - - The content type, file path and part name must be valid pointers to UTF-8 - encoded NULL-terminated strings. Passing invalid pointers or pointers to - strings that are not NULL terminated will lead to undefined behaviour. + Args: + interaction: + Handle to the Interaction. - # Error Handling + part: + The part of the interaction to add the body to (Request or + Response). - If the boundary is a NULL pointer, a random string will be used. If the file - path is a NULL pointer, it will set the body contents as as an empty - mime-part. If the file path does not point to a valid file, or is not able - to be read, it will return an error result. If the content type is a null - pointer, or can't be parsed, it will return an error result. Returns an - error if the interaction or Pact can't be modified (i.e. the mock server for - it has already started), the interaction is not an HTTP interaction or some - other error occurs. - """ - raise NotImplementedError + content_type: + The content type of the body. + + file: + Path to the file to add. If `None`, the body will be set to null. + + part_name: + Name for the mime part. + + boundary: + Boundary for the multipart separation. If `None`, a random string + will be used. + """ + result = StringResult( + lib.pactffi_with_multipart_file_v2( + interaction._ref, + part.value, + content_type.encode("utf-8"), + str(file).encode("utf-8") if file else ffi.NULL, + part_name.encode("utf-8"), + boundary.encode("utf-8") if boundary else ffi.NULL, + ), + ) + result.raise_exception() def with_multipart_file( @@ -5201,6 +5424,8 @@ def with_multipart_file( it has already started), the interaction is not an HTTP interaction or some other error occurs. """ + # This function is intentionally left unimplemented. The + # `with_multipart_file_v2` function should be used instead. raise NotImplementedError @@ -6216,7 +6441,7 @@ def interaction_contents( part: InteractionPart, content_type: str, contents: str, -) -> int: +) -> None: """ Setup the interaction part using a plugin. @@ -6227,32 +6452,43 @@ def interaction_contents( [Rust `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_interaction_contents) - Returns zero on success, and a positive integer value on failure. - - * `interaction` - Handle to the interaction to configure. - * `part` - The part of the interaction to configure (request or response). - It is ignored for messages. - * `content_type` - NULL terminated C string of the content type of the part. - * `contents` - NULL terminated C string of the JSON contents that gets - passed to the plugin. - - # Safety - - `content_type` and `contents` must be a valid pointers to NULL terminated - strings. Invalid pointers will result in undefined behaviour. + Args: + interaction: + Handle to the interaction to configure. - # Errors + part: + The part of the interaction to configure (request or response). It + is ignored for messages. - * `1` - A general panic was caught. - * `2` - The mock server has already been started. - * `3` - The interaction handle is invalid. - * `4` - The content type is not valid. - * `5` - The contents JSON is not valid JSON. - * `6` - The plugin returned an error. + content_type: + Mime type of the contents. - When an error errors, LAST_ERROR will contain the error message. + contents: + JSON contents that gets passed to the plugin. """ - raise NotImplementedError + ret: int = lib.pactffi_interaction_contents( + interaction._ref, + part.value, + content_type.encode("utf-8"), + contents.encode("utf-8"), + ) + if ret == 0: + return + if ret == 1: + msg = f"A general panic was caught: {get_error_message()}" + if ret == 2: # noqa: PLR2004 + msg = "The mock server has already been started." + if ret == 3: # noqa: PLR2004 + msg = f"The interaction handle {interaction} is invalid." + if ret == 4: # noqa: PLR2004 + msg = f"The content type {content_type} is not valid." + if ret == 5: # noqa: PLR2004 + msg = "The content is not valid JSON." + if ret == 6: # noqa: PLR2004 + msg = f"The plugin returned an error: {get_error_message()}" + else: + msg = f"There was an unknown error configuring the interaction: {ret}" + raise RuntimeError(msg) def matches_string_value( diff --git a/pact/v3/pact.py b/pact/v3/pact.py index 10997ecac..b4dfe3af9 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -11,9 +11,11 @@ from __future__ import annotations +import abc +import json from collections import defaultdict from pathlib import Path -from typing import TYPE_CHECKING, Iterable, Literal, Set +from typing import TYPE_CHECKING, Any, Iterable, Literal, Set, overload from yarl import URL @@ -28,61 +30,404 @@ from typing_extensions import Self -class Interaction: +class Interaction(abc.ABC): """ Interaction between a consumer and a provider. - This class defines an interaction between a consumer and a provider. It - defines a specific request that the consumer makes to the provider, and the - response that the provider should return. + This abstract class defines an interaction between a consumer and a + provider. The concrete subclasses define the type of interaction, and include: + + - [`HttpInteraction`][pact.v3.pact.HttpInteraction] + - [`AsyncMessageInteraction`][pact.v3.pact.AsyncMessageInteraction] + - [`SyncMessageInteraction`][pact.v3.pact.SyncMessageInteraction] A set of interactions between a consumer and a provider is called a Pact. """ - def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: + def __init__(self, description: str) -> None: """ - Initialise a new Interaction. + Create a new Interaction. - This function should not be called directly. Instead, an Interaction - should be created using the - [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a - [`Pact`][pact.v3.Pact] instance. + As this class is abstract, this function should not be called directly + but should instead be called through one of the concrete subclasses. + + Args: + description: + Description of the interaction. This must be unique within the + Pact. """ - self._handle = pact.v3.ffi.new_interaction(pact_handle, description) - self._is_request = True - self._request_indices: dict[ - tuple[pact.v3.ffi.InteractionPart, str], - int, - ] = defaultdict(int) - self._parameter_indices: dict[str, int] = defaultdict(int) + self._description = description def __str__(self) -> str: """ - Informal string representation of the Interaction. + Nice representation of the Interaction. """ - raise NotImplementedError + return f"{self.__class__.__name__}({self._description})" def __repr__(self) -> str: """ - Information-rich string representation of the Interaction. + Debugging representation of the Interaction. + """ + return f"{self.__class__.__name__}({self._handle!r})" + + @property + @abc.abstractmethod + def _handle(self) -> pact.v3.ffi.InteractionHandle: + """ + Handle for the Interaction. + + This is used internally by the library to pass the Interaction to the + underlying Pact library. + """ + + @property + @abc.abstractmethod + def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + """ + Interaction part. + + Where interactions have multiple parts, this property keeps track + of which part is currently being set. + """ + + def _parse_interaction_part( + self, + part: Literal["Request", "Response", None], + ) -> pact.v3.ffi.InteractionPart: + """ + Convert the input into an InteractionPart. """ - raise NotImplementedError + if part == "Request": + return pact.v3.ffi.InteractionPart.REQUEST + if part == "Response": + return pact.v3.ffi.InteractionPart.RESPONSE + if part is None: + return self._interaction_part + msg = f"Invalid part: {part}" + raise ValueError(msg) - def given(self, state: str) -> Interaction: + @overload + def given(self, state: str) -> Self: + ... + + @overload + def given(self, state: str, *, name: str, value: str) -> Self: + ... + + @overload + def given(self, state: str, *, parameters: dict[str, Any] | str) -> Self: + ... + + def given( + self, + state: str, + *, + name: str | None = None, + value: str | None = None, + parameters: dict[str, Any] | str | None = None, + ) -> Self: """ Set the provider state. This is the state that the provider should be in when the Interaction is - executed. + executed. When the provider is being verified, the provider state is + passed to the provider so that its internal state can be set to match + the provider state. + + In its simplest form, the provider state is a string. For example, to + match a provider state of `a user exists`, you would use: + + ```python + pact.upon_receiving("a request").given("a user exists") + ``` + + It is also possible to specify a parameter that will be used to match + the provider state. For example, to match a provider state of `a user + exists` with a parameter `id` that has the value `123`, you would use: + + ```python + ( + pact.upon_receiving("a request") + .given("a user exists", name="id", value="123") + ) + ``` + + Lastly, it is possible to specify multiple parameters that will be used + to match the provider state. For example, to match a provider state of + `a user exists` with a parameter `id` that has the value `123` and a + parameter `name` that has the value `John`, you would use: + + ```python + ( + pact.upon_receiving("a request") + .given("a user exists", parameters={ + "id": "123", + "name": "John", + }) + ) + ``` + + This function can be called repeatedly to specify multiple provider + states for the same Interaction. If the same `state` is specified with + different parameters, then the parameters are merged together. The above + example with multiple parameters can equivalently be specified as: + + ```python + ( + pact.upon_receiving("a request") + .given("a user exists", name="id", value="123") + .given("a user exists", name="name", value="John") + ) + ``` Args: state: Provider state for the Interaction. + + name: + Name of the parameter. This must be specified in conjunction + with `value`. + + value: + Value of the parameter. This must be specified in conjunction + with `name`. + + parameters: + Key-value pairs of parameters to use for the provider state. + These must be encodable using [`json.dumps(...)`][json.dumps]. + Alternatively, a string contained the JSON object can be passed + directly. + + If the string does not contain a valid JSON object, then the + string is passed directly as follows: + + ```python + ( + pact.upon_receiving("a request") + .given("a user exists", name="value", value=parameters) + ) + ``` + + Raises: + ValueError: + If the combination of arguments is invalid or inconsistent. + """ + if name is not None and value is not None and parameters is None: + pact.v3.ffi.given_with_param(self._handle, state, name, value) + elif name is None and value is None and parameters is not None: + if isinstance(parameters, dict): + pact.v3.ffi.given_with_params( + self._handle, + state, + json.dumps(parameters), + ) + else: + pact.v3.ffi.given_with_params(self._handle, state, parameters) + elif name is None and value is None and parameters is None: + pact.v3.ffi.given(self._handle, state) + else: + msg = "Invalid combination of arguments." + raise ValueError(msg) + return self + + def with_body( + self, + body: str | None = None, + content_type: str = "text/plain", + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Set the body of the request or response. + + Args: + body: + Body of the request. If this is `None`, then the body is + empty. + + content_type: + Content type of the body. This is ignored if the `Content-Type` + header has already been set. + + part: + Whether the body should be added to the request or the response. + If `None`, then the function intelligently determines whether + the body should be added to the request or the response, based + on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + pact.v3.ffi.with_body( + self._handle, + self._parse_interaction_part(part), + content_type, + body, + ) + return self + + def with_binary_file( + self, + body: bytes | None, + content_type: str = "application/octet-stream", + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Adds a binary file to the request or response. + + Note that for HTTP interactions, this function will overwrite the body + if it has been set using + [`with_body(...)`][pact.v3.Interaction.with_body]. + + Args: + part: + Whether the body should be added to the request or the response. + If `None`, then the function intelligently determines whether + the body should be added to the request or the response, based + on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + + content_type: + Content type of the body. This is ignored if the `Content-Type` + header has already been set. + + body: + Body of the request. + """ + pact.v3.ffi.with_binary_file( + self._handle, + self._parse_interaction_part(part), + content_type, + body, + ) + return self + + def with_multipart_file( # noqa: PLR0913 + self, + part_name: str, + path: Path | None, + content_type: str = "application/octet-stream", + part: Literal["Request", "Response"] | None = None, + boundary: str | None = None, + ) -> Self: + """ + Adds a binary file as the body of a multipart request or response. + + The content type of the body will be set to a MIME multipart message. + """ + pact.v3.ffi.with_multipart_file_v2( + self._handle, + self._parse_interaction_part(part), + part_name, + path, + content_type, + boundary, + ) + return self + + def test_name( + self, + name: str, + ) -> Self: + """ + Set the test name annotation for the interaction. + + This is used by V4 interactions to set the name of the test. + + Args: + name: + Name of the test. + """ + pact.v3.ffi.interaction_test_name(self._handle, name) + return self + + def with_plugin_contents( + self, + contents: dict[str, Any] | str, + content_type: str = "text/plain", + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Set the interaction content using a plugin. + + The value of `contents` is passed directly to the plugin as a JSON + string. The plugin will document the format of the JSON content. + + Args: + contents: + Body of the request. If this is `None`, then the body is empty. + + content_type: + Content type of the body. This is ignored if the `Content-Type` + header has already been set. + + part: + Whether the body should be added to the request or the response. + If `None`, then the function intelligently determines whether + the body should be added to the request or the response, based + on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. """ - pact.v3.ffi.given(self._handle, state) + if isinstance(contents, dict): + contents = json.dumps(contents) + + pact.v3.ffi.interaction_contents( + self._handle, + self._parse_interaction_part(part), + content_type, + contents, + ) return self - def with_request(self, method: str, path: str) -> Interaction: + +class HttpInteraction(Interaction): + """ + A synchronous HTTP interaction. + + This class defines a synchronous HTTP interaction between a consumer and a + provider. It defines a specific request that the consumer makes to the + provider, and the response that the provider should return. + """ + + def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: + """ + Initialise a new HTTP Interaction. + + This function should not be called directly. Instead, an Interaction + should be created using the + [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a + [`Pact`][pact.v3.Pact] instance. + """ + super().__init__(description) + self.__handle = pact.v3.ffi.new_interaction(pact_handle, description) + self.__interaction_part = pact.v3.ffi.InteractionPart.REQUEST + self._request_indices: dict[ + tuple[pact.v3.ffi.InteractionPart, str], + int, + ] = defaultdict(int) + self._parameter_indices: dict[str, int] = defaultdict(int) + + @property + def _handle(self) -> pact.v3.ffi.InteractionHandle: + """ + Handle for the Interaction. + + This is used internally by the library to pass the Interaction to the + underlying Pact library. + """ + return self.__handle + + @property + def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + """ + Interaction part. + + Keeps track whether we are setting by default the request or the + response in the HTTP interaction. + """ + return self.__interaction_part + + def with_request(self, method: str, path: str) -> Self: """ Set the request. @@ -97,27 +442,12 @@ def with_request(self, method: str, path: str) -> Interaction: pact.v3.ffi.with_request(self._handle, method, path) return self - def _interaction_part( - self, - part: Literal["Request", "Response", None], - ) -> pact.v3.ffi.InteractionPart: - """ - Convert the input into an InteractionPart. - """ - part = part or ("Request" if self._is_request else "Response") - if part == "Request": - return pact.v3.ffi.InteractionPart.REQUEST - if part == "Response": - return pact.v3.ffi.InteractionPart.RESPONSE - msg = f"Invalid part: {part}" - raise ValueError(msg) - def with_header( self, name: str, value: str, part: Literal["Request", "Response"] | None = None, - ) -> Interaction: + ) -> Self: r""" Add a header to the request. @@ -207,7 +537,7 @@ def with_header( [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] method has been called. """ - interaction_part = self._interaction_part(part) + interaction_part = self._parse_interaction_part(part) name_lower = name.lower() index = self._request_indices[(interaction_part, name_lower)] self._request_indices[(interaction_part, name_lower)] += 1 @@ -224,7 +554,7 @@ def with_headers( self, headers: dict[str, str] | Iterable[tuple[str, str]], part: Literal["Request", "Response"] | None = None, - ) -> Interaction: + ) -> Self: """ Add multiple headers to the request. @@ -279,7 +609,7 @@ def set_header( name: str, value: str, part: Literal["Request", "Response"] | None = None, - ) -> Interaction: + ) -> Self: r""" Add a header to the request. @@ -304,7 +634,7 @@ def set_header( """ pact.v3.ffi.set_header( self._handle, - self._interaction_part(part), + self._parse_interaction_part(part), name, value, ) @@ -314,7 +644,7 @@ def set_headers( self, headers: dict[str, str] | Iterable[tuple[str, str]], part: Literal["Request", "Response"] | None = None, - ) -> Interaction: + ) -> Self: """ Add multiple headers to the request. @@ -344,7 +674,7 @@ def set_headers( self.set_header(name, value, part) return self - def with_query_parameter(self, name: str, value: str) -> Interaction: + def with_query_parameter(self, name: str, value: str) -> Self: r""" Add a query to the request. @@ -417,7 +747,7 @@ def with_query_parameter(self, name: str, value: str) -> Interaction: def with_query_parameters( self, parameters: dict[str, str] | Iterable[tuple[str, str]], - ) -> Interaction: + ) -> Self: """ Add multiple query parameters to the request. @@ -434,41 +764,7 @@ def with_query_parameters( self.with_query_parameter(name, value) return self - def with_body( - self, - body: str | None = None, - content_type: str = "text/plain", - part: Literal["Request", "Response"] | None = None, - ) -> Interaction: - """ - Set the body of the request. - - Args: - body: - Body of the request. If this is `None`, then the body is - empty. - - content_type: - Content type of the body. This is ignored if the `Content-Type` - header has already been set. - - part: - Whether the body should be added to the request or the response. - If `None`, then the function intelligently determines whether - the body should be added to the request or the response, based - on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - """ - pact.v3.ffi.with_body( - self._handle, - self._interaction_part(part), - content_type, - body, - ) - return self - - def will_respond_with(self, status: int) -> Interaction: + def will_respond_with(self, status: int) -> Self: """ Set the response status. @@ -485,10 +781,104 @@ def will_respond_with(self, status: int) -> Interaction: Status for the response. """ pact.v3.ffi.response_status(self._handle, status) - self._is_request = False + self.__interaction_part = pact.v3.ffi.InteractionPart.RESPONSE return self +class AsyncMessageInteraction(Interaction): + """ + An asynchronous message interaction. + + This class defines an asynchronous message interaction between a consumer + and a provider. It defines the kind of messages a consumer can accept, and + the is agnostic of the underlying protocol, be it a message queue, Apache + Kafka, or some other asynchronous protocol. + """ + + def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: + """ + Initialise a new Asynchronous Message Interaction. + + This function should not be called directly. Instead, an + AsyncMessageInteraction should be created using the + [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a + [`Pact`][pact.v3.Pact] instance using the `"Async"` interaction type. + + Args: + pact_handle: + Handle for the Pact. + + description: + Description of the interaction. This must be unique within the + Pact. + """ + super().__init__(description) + self.__handle = pact.v3.ffi.new_message_interaction(pact_handle, description) + + @property + def _handle(self) -> pact.v3.ffi.InteractionHandle: + """ + Handle for the Interaction. + + This is used internally by the library to pass the Interaction to the + underlying Pact library. + """ + return self.__handle + + @property + def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + return pact.v3.ffi.InteractionPart.REQUEST + + +class SyncMessageInteraction(Interaction): + """ + A synchronous message interaction. + + This class defines a synchronous message interaction between a consumer and + a provider. As with [`HttpInteraction`][pact.v3.pact.HttpInteraction], it + defines a specific request that the consumer makes to the provider, and the + response that the provider should return. + """ + + def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: + """ + Initialise a new Synchronous Message Interaction. + + This function should not be called directly. Instead, an + AsyncMessageInteraction should be created using the + [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a + [`Pact`][pact.v3.Pact] instance using the `"Sync"` interaction type. + + Args: + pact_handle: + Handle for the Pact. + + description: + Description of the interaction. This must be unique within the + Pact. + """ + super().__init__(description) + self.__handle = pact.v3.ffi.new_sync_message_interaction( + pact_handle, + description, + ) + self.__interaction_part = pact.v3.ffi.InteractionPart.REQUEST + + @property + def _handle(self) -> pact.v3.ffi.InteractionHandle: + """ + Handle for the Interaction. + + This is used internally by the library to pass the Interaction to the + underlying Pact library. + """ + return self.__handle + + @property + def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + return self.__interaction_part + + class Pact: """ A Pact between a consumer and a provider. @@ -566,7 +956,42 @@ def provider(self) -> str: """ return self._provider - def upon_receiving(self, description: str) -> Interaction: + @overload + def upon_receiving( + self, + description: str, + ) -> HttpInteraction: + ... + + @overload + def upon_receiving( + self, + description: str, + interaction: Literal["HTTP"], + ) -> HttpInteraction: + ... + + @overload + def upon_receiving( + self, + description: str, + interaction: Literal["Async"], + ) -> AsyncMessageInteraction: + ... + + @overload + def upon_receiving( + self, + description: str, + interaction: Literal["Sync"], + ) -> SyncMessageInteraction: + ... + + def upon_receiving( + self, + description: str, + interaction: Literal["HTTP", "Sync", "Async"] = "HTTP", + ) -> HttpInteraction | AsyncMessageInteraction | SyncMessageInteraction: """ Create a new Interaction. @@ -576,8 +1001,20 @@ def upon_receiving(self, description: str) -> Interaction: description: Description of the interaction. This must be unique within the Pact. + + interaction: + Type of interaction. Defaults to `HTTP`. This must be one of + `HTTP`, `Async`, or `Sync`. """ - return Interaction(self._handle, description) + if interaction == "HTTP": + return HttpInteraction(self._handle, description) + if interaction == "Async": + return AsyncMessageInteraction(self._handle, description) + if interaction == "Sync": + return SyncMessageInteraction(self._handle, description) + + msg = f"Invalid interaction type: {interaction}" + raise ValueError(msg) def serve( self, diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..6d292247d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,45 @@ +""" +Common fixtures for tests. +""" +import json +import shutil +import tempfile +from pathlib import Path +from typing import Any, Generator + +import pytest + + +@pytest.fixture() +def temp_dir() -> Generator[Path, Any, None]: + """ + Create a temporary directory. + + This fixture automatically handles cleanup of the temporary directory once + the test has finished. + + The directory is populated with a few minimal files: + + - `test.py`: A minimal hello-world Python script. + - `test.txt`: A minimal text file. + - `test.json`: A minimal JSON file. + - `test.png`: A minimal PNG image. + """ + temp_dir = Path(tempfile.mkdtemp()) + with (temp_dir / "test.py").open("w") as f: + f.write('print("Hello, world!")') + with (temp_dir / "test.txt").open("w") as f: + f.write("Hello, world!") + with (temp_dir / "test.json").open("w") as f: + json.dump({"hello": "world"}, f) + with (temp_dir / "test.png").open("wb") as f: + f.write( + b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" + b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4" + b"\x89\x00\x00\x00\x0a\x49\x44\x41\x54\x78\x9c\x63\x00\x01\x00\x00" + b"\x05\x00\x01\x0d\x0a\x2d\xb4\x00\x00\x00\x00\x49\x45\x4e\x44\xae" + b"\x42\x60\x82", + ) + + yield temp_dir + shutil.rmtree(temp_dir) diff --git a/tests/v3/conftest.py b/tests/v3/conftest.py index e76e3be75..d509cba19 100644 --- a/tests/v3/conftest.py +++ b/tests/v3/conftest.py @@ -5,6 +5,7 @@ directory. """ + import pytest @@ -15,4 +16,4 @@ def _setup_pact_logging() -> None: """ from pact.v3 import ffi - ffi.log_to_stderr(ffi.LevelFilter.DEBUG) + ffi.log_to_stderr("DEBUG") diff --git a/tests/v3/test_async_interaction.py b/tests/v3/test_async_interaction.py new file mode 100644 index 000000000..64e215481 --- /dev/null +++ b/tests/v3/test_async_interaction.py @@ -0,0 +1,34 @@ +""" +Pact Async Message Interaction unit tests. +""" + +from __future__ import annotations + +import re + +import pytest +from pact.v3 import Pact + + +@pytest.fixture() +def pact() -> Pact: + """ + Fixture for a Pact instance. + """ + return Pact("consumer", "provider") + + +def test_str(pact: Pact) -> None: + interaction = pact.upon_receiving("a basic request", "Async") + assert str(interaction) == "AsyncMessageInteraction(a basic request)" + + +def test_repr(pact: Pact) -> None: + interaction = pact.upon_receiving("a basic request", "Async") + assert ( + re.match( + r"^AsyncMessageInteraction\(InteractionHandle\(\d+\)\)$", + repr(interaction), + ) + is not None + ) diff --git a/tests/v3/test_ffi.py b/tests/v3/test_ffi.py index be3aff537..f899144db 100644 --- a/tests/v3/test_ffi.py +++ b/tests/v3/test_ffi.py @@ -5,7 +5,9 @@ They are not intended to test the Pact API itself, as that is handled by the client library. """ +import re +import pytest from pact.v3 import ffi @@ -13,3 +15,39 @@ def test_version() -> None: assert isinstance(ffi.version(), str) assert len(ffi.version()) > 0 assert ffi.version().count(".") == 2 + + +def test_string_result_ok() -> None: + result = ffi.StringResult(ffi.lib.pactffi_generate_datetime_string(b"yyyy")) + assert result.is_ok + assert not result.is_failed + assert re.match(r"^\d{4}$", result.text) + assert str(result) == result.text + assert repr(result) == f"" + result.raise_exception() + + +def test_string_result_failed() -> None: + result = ffi.StringResult(ffi.lib.pactffi_generate_datetime_string(b"t")) + assert not result.is_ok + assert result.is_failed + assert result.text.startswith("Error parsing") + with pytest.raises(RuntimeError): + result.raise_exception() + + +def test_datetime_valid() -> None: + ffi.validate_datetime("2023-01-01", "yyyy-MM-dd") + + +def test_datetime_invalid() -> None: + with pytest.raises(ValueError, match=r"Invalid datetime value.*"): + ffi.validate_datetime("01/01/2023", "yyyy-MM-dd") + + +def test_get_error_message() -> None: + # The first bit makes sure that an error is generated. + invalid_utf8 = b"\xc3\x28" + ret: int = ffi.lib.pactffi_validate_datetime(invalid_utf8, invalid_utf8) + assert ret == 2 + assert ffi.get_error_message() == "error parsing value as UTF-8" diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py new file mode 100644 index 000000000..2e9ed652d --- /dev/null +++ b/tests/v3/test_http_interaction.py @@ -0,0 +1,547 @@ +""" +Pact Http Interaction unit tests. +""" + +from __future__ import annotations + +import json +import re +from typing import TYPE_CHECKING + +import aiohttp +import pytest +from pact.v3 import Pact + +if TYPE_CHECKING: + from pathlib import Path + +ALL_HTTP_METHODS = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "HEAD", + "OPTIONS", + "TRACE", + "CONNECT", +] + + +@pytest.fixture() +def pact() -> Pact: + """ + Fixture for a Pact instance. + """ + return Pact("consumer", "provider") + + +def test_str(pact: Pact) -> None: + interaction = pact.upon_receiving("a basic request") + assert str(interaction) == "HttpInteraction(a basic request)" + + +def test_repr(pact: Pact) -> None: + interaction = pact.upon_receiving("a basic request") + assert ( + re.match(r"^HttpInteraction\(InteractionHandle\(\d+\)\)$", repr(interaction)) + is not None + ) + + +@pytest.mark.parametrize( + "method", + ALL_HTTP_METHODS, +) +@pytest.mark.asyncio() +async def test_basic_request_method(pact: Pact, method: str) -> None: + ( + pact.upon_receiving(f"a basic {method} request") + .with_request(method, "/") + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + for m in ALL_HTTP_METHODS: + async with session.request(m, "/") as resp: + assert resp.status == (200 if m == method else 500) + + +@pytest.mark.parametrize( + "status", + list(range(200, 600, 13)), +) +@pytest.mark.asyncio() +async def test_basic_response_status(pact: Pact, status: int) -> None: + ( + pact.upon_receiving(f"a basic request producing status {status}") + .with_request("GET", "/") + .will_respond_with(status) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request("GET", "/") as resp: + assert resp.status == status + + +@pytest.mark.parametrize( + "headers", + [ + [("X-Test", "true")], + [("X-Foo", "true"), ("X-Bar", "true")], + [("X-Test", "1"), ("X-Test", "2")], + ], +) +@pytest.mark.asyncio() +async def test_with_header_request( + pact: Pact, + headers: list[tuple[str, str]], +) -> None: + ( + pact.upon_receiving("a basic request with a header") + .with_request("GET", "/") + .with_headers(headers) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + headers=headers, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.parametrize( + "headers", + [ + [("X-Test", "true")], + [("X-Foo", "true"), ("X-Bar", "true")], + [("X-Test", "1"), ("X-Test", "2")], + ], +) +@pytest.mark.asyncio() +async def test_with_header_response( + pact: Pact, + headers: list[tuple[str, str]], +) -> None: + ( + pact.upon_receiving("a basic request with a header") + .with_request("GET", "/") + .will_respond_with(200) + .with_headers(headers) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + ) as resp: + assert resp.status == 200 + response_headers = [(h.lower(), v) for h, v in resp.headers.items()] + for header, value in headers: + assert (header.lower(), value) in response_headers + + +@pytest.mark.asyncio() +async def test_with_header_dict(pact: Pact) -> None: + ( + pact.upon_receiving("a basic request with a headers from a dict") + .with_request("GET", "/") + .with_headers({"X-Test": "true", "X-Foo": "bar"}) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + headers={"X-Test": "true", "X-Foo": "bar"}, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.parametrize( + "headers", + [ + [("X-Test", "true")], + [("X-Foo", "true"), ("X-Bar", "true")], + ], +) +@pytest.mark.asyncio() +async def test_set_header_request( + pact: Pact, + headers: list[tuple[str, str]], +) -> None: + ( + pact.upon_receiving("a basic request with a header") + .with_request("GET", "/") + .set_headers(headers) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + headers=headers, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.asyncio() +async def test_set_header_request_repeat( + pact: Pact, +) -> None: + headers = [("X-Test", "1"), ("X-Test", "2")] + ( + pact.upon_receiving("a basic request with a header") + .with_request("GET", "/") + # As set_headers makes no additional processing, the last header will be + # the one that is used. + .set_headers(headers) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + headers=headers, + ) as resp: + assert resp.status == 500 + + +@pytest.mark.parametrize( + "headers", + [ + [("X-Test", "true")], + [("X-Foo", "true"), ("X-Bar", "true")], + ], +) +@pytest.mark.asyncio() +async def test_set_header_response( + pact: Pact, + headers: list[tuple[str, str]], +) -> None: + ( + pact.upon_receiving("a basic request with a header") + .with_request("GET", "/") + .will_respond_with(200) + .set_headers(headers) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + ) as resp: + assert resp.status == 200 + response_headers = [(h.lower(), v) for h, v in resp.headers.items()] + for header, value in headers: + assert (header.lower(), value) in response_headers + + +@pytest.mark.asyncio() +async def test_set_header_response_repeat( + pact: Pact, +) -> None: + headers = [("X-Test", "1"), ("X-Test", "2")] + ( + pact.upon_receiving("a basic request with a header") + .with_request("GET", "/") + .will_respond_with(200) + # As set_headers makes no additional processing, the last header will be + # the one that is used. + .set_headers(headers) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + headers=headers, + ) as resp: + assert resp.status == 200 + response_headers = [(h.lower(), v) for h, v in resp.headers.items()] + assert ("x-test", "2") in response_headers + assert ("x-test", "1") not in response_headers + + +@pytest.mark.asyncio() +async def test_set_header_dict(pact: Pact) -> None: + ( + pact.upon_receiving("a basic request with a headers from a dict") + .with_request("GET", "/") + .set_headers({"X-Test": "true", "X-Foo": "bar"}) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + headers={"X-Test": "true", "X-Foo": "bar"}, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.parametrize( + "query", + [ + [("test", "true")], + [("foo", "true"), ("bar", "true")], + [("test", "1"), ("test", "2")], + ], +) +@pytest.mark.asyncio() +async def test_with_query_parameter_request( + pact: Pact, + query: list[tuple[str, str]], +) -> None: + ( + pact.upon_receiving("a basic request with a query parameter") + .with_request("GET", "/") + .with_query_parameters(query) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + url = srv.url.with_query(query) + async with session.request( + "GET", + url.path_qs, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.asyncio() +async def test_with_query_parameter_dict(pact: Pact) -> None: + ( + pact.upon_receiving("a basic request with a query parameter from a dict") + .with_request("GET", "/") + .with_query_parameters({"test": "true", "foo": "bar"}) + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + url = srv.url.with_query({"test": "true", "foo": "bar"}) + async with session.request( + "GET", + url.path_qs, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.parametrize( + "method", + ["GET", "POST", "PUT"], +) +@pytest.mark.asyncio() +async def test_with_body_request(pact: Pact, method: str) -> None: + ( + pact.upon_receiving(f"a basic {method} request with a body") + .with_request(method, "/") + .with_body(json.dumps({"test": True}), "application/json") + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + method, + "/", + json={"test": True}, + ) as resp: + assert resp.status == 200 + + +@pytest.mark.parametrize( + "method", + ["GET", "POST", "PUT"], +) +@pytest.mark.asyncio() +async def test_with_body_response(pact: Pact, method: str) -> None: + ( + pact.upon_receiving( + f"a basic {method} request expecting a response with a body", + ) + .with_request(method, "/") + .will_respond_with(200) + .with_body(json.dumps({"test": True}), "application/json") + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + method, + "/", + json={"test": True}, + ) as resp: + assert resp.status == 200 + assert await resp.json() == {"test": True} + + +@pytest.mark.asyncio() +async def test_with_body_explicit(pact: Pact) -> None: + ( + pact.upon_receiving("") + .with_request("GET", "/") + .will_respond_with(200) + .with_body(json.dumps({"request": True}), "application/json", "Request") + .with_body(json.dumps({"response": True}), "application/json", "Response") + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request( + "GET", + "/", + json={"request": True}, + ) as resp: + assert resp.status == 200 + assert await resp.json() == {"response": True} + + +def test_with_body_invalid(pact: Pact) -> None: + with pytest.raises(ValueError, match="Invalid part: Invalid"): + ( + pact.upon_receiving("") + .with_request("GET", "/") + .will_respond_with(200) + .with_body(json.dumps({"request": True}), "application/json", "Invalid") + ) + + +@pytest.mark.asyncio() +async def test_given(pact: Pact) -> None: + ( + pact.upon_receiving("a basic request given state 1") + .given("state 1") + .with_request("GET", "/state") + .will_respond_with(200) + ) + ( + pact.upon_receiving("a basic request given a user exists (1)") + .given("a user exists", name="id", value="123") + .given("a user exists", name="name", value="John") + .with_request("GET", "/user1") + .will_respond_with(201) + ) + ( + pact.upon_receiving("a basic request given a user exists (2)") + .given("a user exists", parameters={"id": "123", "name": "John"}) + .with_request("GET", "/user2") + .will_respond_with(202) + ) + + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.request("GET", "/state") as resp: + assert resp.status == 200 + async with session.request("GET", "/user1") as resp: + assert resp.status == 201 + async with session.request("GET", "/user2") as resp: + assert resp.status == 202 + + +@pytest.mark.asyncio() +async def test_binary_file_request(pact: Pact) -> None: + payload = bytes(range(8)) + ( + pact.upon_receiving("a basic request with a binary file") + .with_request("POST", "/") + .with_binary_file(payload, "application/octet-stream") + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.post("/", data=payload) as resp: + assert resp.status == 200 + async with session.post("/", data=payload[:2]) as resp: + # The match _only_ checks the content type, not the content + # itself. See + # https://pact-foundation.slack.com/archives/C02BXLDJ7JR/p1697032990681329 + assert resp.status == 200 + + +@pytest.mark.asyncio() +async def test_binary_file_response(pact: Pact) -> None: + payload = bytes(range(5)) + ( + pact.upon_receiving("a basic request with a binary file response") + .with_request("GET", "/") + .will_respond_with(200) + .with_binary_file(payload, "application/bytes") + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.get("/") as resp: + assert resp.status == 200 + assert await resp.read() == payload + assert payload == bytes(range(5)) # to make sure it's not mutated + assert resp.headers["Content-Type"] == "application/bytes" + + +@pytest.mark.skip(reason="Not working yet") +@pytest.mark.asyncio() +async def test_multipart_file_request(pact: Pact, temp_dir: Path) -> None: + fpy = temp_dir / "test.py" + fpng = temp_dir / "test.png" + ( + pact.upon_receiving("a basic request with a multipart file") + .with_request("POST", "/") + .with_multipart_file( + fpy.name, + fpy, + "text/x-python", + ) + .with_multipart_file( + fpng.name, + fpng, + "image/png", + ) + .will_respond_with(200) + ) + with pact.serve() as srv, aiohttp.MultipartWriter() as mpwriter: + mpwriter.append( + fpy.open("rb"), + {"Content-Type": "text/x-python"}, + ) + mpwriter.append( + fpng.open("rb"), + {"Content-Type": "image/png"}, + ) + + async with aiohttp.ClientSession(srv.url) as session, session.post( + "/", + data=mpwriter, + ) as resp: + assert resp.status == 200 + assert await resp.read() == b"" + + +@pytest.mark.asyncio() +async def test_name(pact: Pact) -> None: + ( + pact.upon_receiving("a basic request with a test name") + .test_name("a test name") + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.get("/") as resp: + assert resp.status == 200 + assert await resp.read() == b"" + + +@pytest.mark.asyncio() +async def test_with_plugin(pact: Pact) -> None: + ( + pact.upon_receiving("a basic request with a plugin") + .with_plugin_contents("{}") + .will_respond_with(200) + ) + with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.get("/") as resp: + assert resp.status == 200 + assert await resp.read() == b"" diff --git a/tests/v3/test_pact.py b/tests/v3/test_pact.py index 92049b6a6..6f24c04a6 100644 --- a/tests/v3/test_pact.py +++ b/tests/v3/test_pact.py @@ -4,9 +4,6 @@ from __future__ import annotations -import json - -import aiohttp import pytest from pact.v3 import Pact @@ -44,388 +41,3 @@ def test_serve(pact: Pact) -> None: assert srv.url.path == "/" assert srv / "foo" == srv.url / "foo" assert str(srv / "foo") == f"http://localhost:{srv.port}/foo" - - -@pytest.mark.parametrize( - "method", - [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE", - "HEAD", - "OPTIONS", - "TRACE", - "CONNECT", - ], -) -@pytest.mark.asyncio() -async def test_basic_request_method(pact: Pact, method: str) -> None: - ( - pact.upon_receiving(f"a basic {method} request") - .with_request(method, "/") - .will_respond_with(200) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request(method, "/") as resp: - assert resp.status == 200 - - -@pytest.mark.parametrize( - "status", - list(range(200, 600, 13)), -) -@pytest.mark.asyncio() -async def test_basic_response_status(pact: Pact, status: int) -> None: - ( - pact.upon_receiving(f"a basic request producing status {status}") - .with_request("GET", "/") - .will_respond_with(status) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request("GET", "/") as resp: - assert resp.status == status - - -@pytest.mark.parametrize( - "headers", - [ - [("X-Test", "true")], - [("X-Foo", "true"), ("X-Bar", "true")], - [("X-Test", "1"), ("X-Test", "2")], - ], -) -@pytest.mark.asyncio() -async def test_with_header_request( - pact: Pact, - headers: list[tuple[str, str]], -) -> None: - ( - pact.upon_receiving("a basic request with a header") - .with_request("GET", "/") - .with_headers(headers) - .will_respond_with(200) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - headers=headers, - ) as resp: - assert resp.status == 200 - - -@pytest.mark.parametrize( - "headers", - [ - [("X-Test", "true")], - [("X-Foo", "true"), ("X-Bar", "true")], - [("X-Test", "1"), ("X-Test", "2")], - ], -) -@pytest.mark.asyncio() -async def test_with_header_response( - pact: Pact, - headers: list[tuple[str, str]], -) -> None: - ( - pact.upon_receiving("a basic request with a header") - .with_request("GET", "/") - .will_respond_with(200) - .with_headers(headers) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - ) as resp: - assert resp.status == 200 - response_headers = [(h.lower(), v) for h, v in resp.headers.items()] - for header, value in headers: - assert (header.lower(), value) in response_headers - - -@pytest.mark.asyncio() -async def test_with_header_dict(pact: Pact) -> None: - ( - pact.upon_receiving("a basic request with a headers from a dict") - .with_request("GET", "/") - .with_headers({"X-Test": "true", "X-Foo": "bar"}) - .will_respond_with(200) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - headers={"X-Test": "true", "X-Foo": "bar"}, - ) as resp: - assert resp.status == 200 - - -@pytest.mark.parametrize( - "headers", - [ - [("X-Test", "true")], - [("X-Foo", "true"), ("X-Bar", "true")], - ], -) -@pytest.mark.asyncio() -async def test_set_header_request( - pact: Pact, - headers: list[tuple[str, str]], -) -> None: - ( - pact.upon_receiving("a basic request with a header") - .with_request("GET", "/") - .set_headers(headers) - .will_respond_with(200) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - headers=headers, - ) as resp: - assert resp.status == 200 - - -@pytest.mark.asyncio() -async def test_set_header_request_repeat( - pact: Pact, -) -> None: - headers = [("X-Test", "1"), ("X-Test", "2")] - ( - pact.upon_receiving("a basic request with a header") - .with_request("GET", "/") - # As set_headers makes no additional processing, the last header will be - # the one that is used. - .set_headers(headers) - .will_respond_with(200) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - headers=headers, - ) as resp: - assert resp.status == 500 - - -@pytest.mark.parametrize( - "headers", - [ - [("X-Test", "true")], - [("X-Foo", "true"), ("X-Bar", "true")], - ], -) -@pytest.mark.asyncio() -async def test_set_header_response( - pact: Pact, - headers: list[tuple[str, str]], -) -> None: - ( - pact.upon_receiving("a basic request with a header") - .with_request("GET", "/") - .will_respond_with(200) - .set_headers(headers) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - ) as resp: - assert resp.status == 200 - response_headers = [(h.lower(), v) for h, v in resp.headers.items()] - for header, value in headers: - assert (header.lower(), value) in response_headers - - -@pytest.mark.asyncio() -async def test_set_header_response_repeat( - pact: Pact, -) -> None: - headers = [("X-Test", "1"), ("X-Test", "2")] - ( - pact.upon_receiving("a basic request with a header") - .with_request("GET", "/") - .will_respond_with(200) - # As set_headers makes no additional processing, the last header will be - # the one that is used. - .set_headers(headers) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - headers=headers, - ) as resp: - assert resp.status == 200 - response_headers = [(h.lower(), v) for h, v in resp.headers.items()] - assert ("x-test", "2") in response_headers - assert ("x-test", "1") not in response_headers - - -@pytest.mark.asyncio() -async def test_set_header_dict(pact: Pact) -> None: - ( - pact.upon_receiving("a basic request with a headers from a dict") - .with_request("GET", "/") - .set_headers({"X-Test": "true", "X-Foo": "bar"}) - .will_respond_with(200) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - headers={"X-Test": "true", "X-Foo": "bar"}, - ) as resp: - assert resp.status == 200 - - -@pytest.mark.parametrize( - "query", - [ - [("test", "true")], - [("foo", "true"), ("bar", "true")], - [("test", "1"), ("test", "2")], - ], -) -@pytest.mark.asyncio() -async def test_with_query_parameter_request( - pact: Pact, - query: list[tuple[str, str]], -) -> None: - ( - pact.upon_receiving("a basic request with a query parameter") - .with_request("GET", "/") - .with_query_parameters(query) - .will_respond_with(200) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - url = srv.url.with_query(query) - async with session.request( - "GET", - url.path_qs, - ) as resp: - assert resp.status == 200 - - -@pytest.mark.asyncio() -async def test_with_query_parameter_dict(pact: Pact) -> None: - ( - pact.upon_receiving("a basic request with a query parameter from a dict") - .with_request("GET", "/") - .with_query_parameters({"test": "true", "foo": "bar"}) - .will_respond_with(200) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - url = srv.url.with_query({"test": "true", "foo": "bar"}) - async with session.request( - "GET", - url.path_qs, - ) as resp: - assert resp.status == 200 - - -@pytest.mark.parametrize( - "method", - ["GET", "POST", "PUT"], -) -@pytest.mark.asyncio() -async def test_with_body_request(pact: Pact, method: str) -> None: - ( - pact.upon_receiving(f"a basic {method} request with a body") - .with_request(method, "/") - .with_body(json.dumps({"test": True}), "application/json") - .will_respond_with(200) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - method, - "/", - json={"test": True}, - ) as resp: - assert resp.status == 200 - - -@pytest.mark.parametrize( - "method", - ["GET", "POST", "PUT"], -) -@pytest.mark.asyncio() -async def test_with_body_response(pact: Pact, method: str) -> None: - ( - pact.upon_receiving( - f"a basic {method} request expecting a response with a body", - ) - .with_request(method, "/") - .will_respond_with(200) - .with_body(json.dumps({"test": True}), "application/json") - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - method, - "/", - json={"test": True}, - ) as resp: - assert resp.status == 200 - assert await resp.json() == {"test": True} - - -@pytest.mark.asyncio() -async def test_with_body_explicit(pact: Pact) -> None: - ( - pact.upon_receiving("") - .with_request("GET", "/") - .will_respond_with(200) - .with_body(json.dumps({"request": True}), "application/json", "Request") - .with_body(json.dumps({"response": True}), "application/json", "Response") - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - json={"request": True}, - ) as resp: - assert resp.status == 200 - assert await resp.json() == {"response": True} - - -def test_with_body_invalid(pact: Pact) -> None: - with pytest.raises(ValueError, match="Invalid part: Invalid"): - ( - pact.upon_receiving("") - .with_request("GET", "/") - .will_respond_with(200) - .with_body(json.dumps({"request": True}), "application/json", "Invalid") - ) - - -@pytest.mark.asyncio() -async def test_given(pact: Pact) -> None: - ( - pact.upon_receiving("a basic request given state 1") - .given("state 1") - .with_request("GET", "/") - .will_respond_with(200) - ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request("GET", "/") as resp: - assert resp.status == 200 diff --git a/tests/v3/test_sync_interaction.py b/tests/v3/test_sync_interaction.py new file mode 100644 index 000000000..64e215481 --- /dev/null +++ b/tests/v3/test_sync_interaction.py @@ -0,0 +1,34 @@ +""" +Pact Async Message Interaction unit tests. +""" + +from __future__ import annotations + +import re + +import pytest +from pact.v3 import Pact + + +@pytest.fixture() +def pact() -> Pact: + """ + Fixture for a Pact instance. + """ + return Pact("consumer", "provider") + + +def test_str(pact: Pact) -> None: + interaction = pact.upon_receiving("a basic request", "Async") + assert str(interaction) == "AsyncMessageInteraction(a basic request)" + + +def test_repr(pact: Pact) -> None: + interaction = pact.upon_receiving("a basic request", "Async") + assert ( + re.match( + r"^AsyncMessageInteraction\(InteractionHandle\(\d+\)\)$", + repr(interaction), + ) + is not None + ) From 1c8c9e5c74fa28c1ce8326224b324da528da37d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 23:45:19 +0000 Subject: [PATCH 0072/1376] chore(deps): add renovate.json Signed-off-by: JP-Ellis --- .github/renovate.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/renovate.json diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 000000000..41784f53e --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:base"], + "pre-commit": { + "enabled": true + }, + "prHourlyLimit": 0, + "prConcurrentLimit": 0 +} From 6ea85742b667d31d5538edf5c2dbdc2fe87b36f3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 20 Oct 2023 12:50:30 +1100 Subject: [PATCH 0073/1376] chore(deps): update deps and set guideline As Python dependencies are shared within a virtual environment and the latest version is installed by default (as there is no lock file), there is benefit to specifying a very broad range of compatible versions. Signed-off-by: JP-Ellis --- pyproject.toml | 52 +++++++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 62db1dd93..5e475df5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,14 +27,20 @@ classifiers = [ ] requires-python = ">=3.8,<4.0" + +# Dependencies of Pact Python should be specified using the broadest range +# compatible version unless: +# +# - A specific feature is required in a new minor release +# - A minor version address vulnerability which directly impacts Pact Python dependencies = [ - "click ~= 8.1", - "fastapi ~= 0.103", - "psutil ~= 5.9", - "requests ~= 2.31", - "six ~= 1.16", - "typing-extensions ~= 4.8 ; python_version < '3.10'", - "uvicorn ~= 0.13", + "click ~= 8.0", + "fastapi ~= 0.0", + "psutil ~= 5.0", + "requests ~= 2.0", + "six ~= 1.0", + "typing-extensions ~= 4.0 ; python_version < '3.10'", + "uvicorn ~= 0.0", ] [project.urls] @@ -48,28 +54,30 @@ dependencies = [ pact-verifier = "pact.cli.verify:main" [project.optional-dependencies] +# Linting and formatting tools use a more narrow specification to ensure +# developper consistency. All other dependencies are as above. types = [ - "mypy ~= 1.1", - "types-cffi ~= 1.15", - "types-requests ~= 2.31", + "mypy ~= 1.6.0", + "types-cffi ~= 1.0", + "types-requests ~= 2.0", ] test = [ - "aiohttp[speedups] ~= 3.8", - "coverage[toml] ~= 7.3", - "flask[async] ~= 2.3", - "httpx ~= 0.24", - "mock ~= 5.1", - "pytest ~= 7.4", - "pytest-asyncio ~= 0.21", - "pytest-cov ~= 4.1", - "testcontainers ~= 3.7", - "yarl ~= 1.9", + "aiohttp[speedups] ~= 3.0", + "coverage[toml] ~= 7.0", + "flask[async] ~= 3.0", + "httpx ~= 0.0", + "mock ~= 5.0", + "pytest ~= 7.0", + "pytest-asyncio ~= 0.0", + "pytest-cov ~= 4.0", + "testcontainers ~= 3.0", + "yarl ~= 1.0", ] dev = [ "pact-python[types]", "pact-python[test]", - "black ~= 23.7", - "ruff ~= 0.0", + "black ~= 23.10.0", + "ruff ~= 0.1.0", ] ################################################################################ From c7c00b72bb58349b27bbecbcac3fe3a81b83f7af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 23:48:21 +0000 Subject: [PATCH 0074/1376] chore(deps): update dependencies --- .github/workflows/build.yml | 4 ++-- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe1c559cf..aa05b14ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@v2.15.0 + uses: pypa/cibuildwheel@v2.16.2 env: CIBW_ARCHS: ${{ matrix.archs }} @@ -88,7 +88,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@v2.15.0 + uses: pypa/cibuildwheel@v2.16.2 env: CIBW_ARCHS: ${{ matrix.archs }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 13ecd07be..af425ec09 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,7 +53,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 88eb34759..f2a9f2db7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ default_install_hook_types: repos: # Generic hooks that apply to a lot of files - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.289 + rev: v0.1.1 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the @@ -48,7 +48,7 @@ repos: stages: [pre-push] - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.0 hooks: - id: black # Exclude python files in pact/** and tests/**, except for the @@ -57,7 +57,7 @@ repos: stages: [pre-push] - repo: https://github.com/commitizen-tools/commitizen - rev: 3.8.2 + rev: v3.12.0 hooks: - id: commitizen stages: [commit-msg] From ab02630bf32f343a77c053ac00bedc12dc111b76 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 19 Oct 2023 17:35:41 +1100 Subject: [PATCH 0075/1376] chore: enable lints fully As there is a significant existing codebase which will eventually be deprecated, there is little benefit to applying formatting and linting rules to the files which are to be deprecated. This commit configures Ruff, Mypy and Black to exclude files in the v2 codebase so that `hatch run lint` can function correctly. As a result, the linting in the CI workflow has also been enabled. An equivalent was already implemented for the pre-commit hooks. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 27 +++++++++++----------- examples/.ruff.toml | 1 + examples/__init__.py | 0 examples/conftest.py | 3 ++- examples/src/fastapi.py | 2 +- examples/src/message.py | 3 ++- examples/tests/__init__.py | 0 examples/tests/test_00_consumer.py | 4 +++- examples/tests/test_01_provider_fastapi.py | 7 ++++-- examples/tests/test_01_provider_flask.py | 6 ++--- examples/tests/test_02_message_consumer.py | 10 +++++--- pyproject.toml | 20 ++++++++++++++++ tests/__init__.py | 0 tests/v3/__init__.py | 0 tests/v3/test_http_interaction.py | 12 +++++++--- 15 files changed, 66 insertions(+), 29 deletions(-) create mode 100644 examples/__init__.py create mode 100644 examples/tests/__init__.py create mode 100644 tests/__init__.py create mode 100644 tests/v3/__init__.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af425ec09..d6785a55d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,22 +103,21 @@ jobs: run: > hatch run example --broker-url=http://pactbroker:pactbroker@localhost:9292 - # TODO: Fix lints before enabling this - # lint: - # name: Lint + lint: + name: Lint - # runs-on: ubuntu-latest + runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 + steps: + - uses: actions/checkout@v4 - # - name: Set up Python - # uses: actions/setup-python@v4 - # with: - # python-version: ${{ env.STABLE_PYTHON_VERSION }} + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.STABLE_PYTHON_VERSION }} - # - name: Install Hatch - # run: pip install --upgrade hatch + - name: Install Hatch + run: pip install --upgrade hatch - # - name: Lint - # run: hatch run lint + - name: Lint + run: hatch run lint diff --git a/examples/.ruff.toml b/examples/.ruff.toml index 03292c3c2..94028649c 100644 --- a/examples/.ruff.toml +++ b/examples/.ruff.toml @@ -3,6 +3,7 @@ extend = "../pyproject.toml" ignore = [ "S101", # Forbid assert statements "D103", # Require docstring in public function + "D104", # Require docstring in public package ] [per-file-ignores] diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/conftest.py b/examples/conftest.py index eba8ffc73..64b4ed788 100644 --- a/examples/conftest.py +++ b/examples/conftest.py @@ -9,13 +9,14 @@ Pact files will be stored. You are encouraged to have a look at these files after the examples have been run. """ + from __future__ import annotations from pathlib import Path from typing import Any, Generator, Union import pytest -from testcontainers.compose import DockerCompose +from testcontainers.compose import DockerCompose # type: ignore[import-untyped] from yarl import URL EXAMPLE_DIR = Path(__file__).parent.resolve() diff --git a/examples/src/fastapi.py b/examples/src/fastapi.py index 52c6e3ff3..d7d2ac9a0 100644 --- a/examples/src/fastapi.py +++ b/examples/src/fastapi.py @@ -38,7 +38,7 @@ @app.get("/users/{uid}") -async def get_user_by_id(uid: int) -> Dict[str, Any]: +async def get_user_by_id(uid: int) -> JSONResponse: """ Fetch a user by their ID. diff --git a/examples/src/message.py b/examples/src/message.py index ed0a755bf..977c343c7 100644 --- a/examples/src/message.py +++ b/examples/src/message.py @@ -6,6 +6,7 @@ handler is solely responsible for processing the message, and does not necessarily need to send a response. """ + from __future__ import annotations from pathlib import Path @@ -59,7 +60,7 @@ def process(self, event: Dict[str, Any]) -> Union[str, None]: self.validate_event(event) if event["action"] == "WRITE": - return self.fs.write(event["path"], event.get("contents", "")) + self.fs.write(event["path"], event.get("contents", "")) if event["action"] == "READ": return self.fs.read(event["path"]) diff --git a/examples/tests/__init__.py b/examples/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/tests/test_00_consumer.py b/examples/tests/test_00_consumer.py index 7cb8369bd..52f24a36a 100644 --- a/examples/tests/test_00_consumer.py +++ b/examples/tests/test_00_consumer.py @@ -21,10 +21,11 @@ import pytest import requests -from examples.src.consumer import User, UserConsumer from pact import Consumer, Format, Like, Provider from yarl import URL +from examples.src.consumer import User, UserConsumer + if TYPE_CHECKING: from pathlib import Path @@ -138,5 +139,6 @@ def test_get_unknown_user(pact: Pact, user_consumer: UserConsumer) -> None: with pact: with pytest.raises(requests.HTTPError) as excinfo: user_consumer.get_user(123) + assert excinfo.value.response is not None assert excinfo.value.response.status_code == HTTPStatus.NOT_FOUND pact.verify() diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/tests/test_01_provider_fastapi.py index d2d7486b8..03d8d382c 100644 --- a/examples/tests/test_01_provider_fastapi.py +++ b/examples/tests/test_01_provider_fastapi.py @@ -30,11 +30,12 @@ import pytest import uvicorn -from examples.src.fastapi import app from pact import Verifier from pydantic import BaseModel from yarl import URL +from examples.src.fastapi import app + PROVIDER_URL = URL("http://localhost:8080") @@ -78,7 +79,9 @@ def run_server() -> None: lambda cannot be used as the target of a `multiprocessing.Process` as it cannot be pickled. """ - uvicorn.run(app, host=PROVIDER_URL.host, port=PROVIDER_URL.port) + host = PROVIDER_URL.host if PROVIDER_URL.host else "localhost" + port = PROVIDER_URL.port if PROVIDER_URL.port else 8080 + uvicorn.run(app, host=host, port=port) @pytest.fixture(scope="module") diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py index 50ad3fc34..05323246e 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/tests/test_01_provider_flask.py @@ -22,7 +22,6 @@ section of the Pact documentation. """ - from __future__ import annotations from multiprocessing import Process @@ -30,11 +29,12 @@ from unittest.mock import MagicMock import pytest -from examples.src.flask import app from flask import request from pact import Verifier from yarl import URL +from examples.src.flask import app + PROVIDER_URL = URL("http://localhost:8080") @@ -58,7 +58,7 @@ async def mock_pact_provider_states() -> Dict[str, Union[str, None]]: "user 123 doesn't exist": mock_user_123_doesnt_exist, "user 123 exists": mock_user_123_exists, } - return {"result": mapping[request.json["state"]]()} + return {"result": mapping[request.json["state"]]()} # type: ignore[index] def run_server() -> None: diff --git a/examples/tests/test_02_message_consumer.py b/examples/tests/test_02_message_consumer.py index b87b14a2e..f49c262ea 100644 --- a/examples/tests/test_02_message_consumer.py +++ b/examples/tests/test_02_message_consumer.py @@ -35,9 +35,10 @@ from unittest.mock import MagicMock import pytest -from examples.src.message import Handler from pact import MessageConsumer, MessagePact, Provider +from examples.src.message import Handler + if TYPE_CHECKING: from pathlib import Path @@ -116,7 +117,10 @@ def test_write_file(pact: MessagePact, handler: Handler) -> None: ) result = handler.process(msg) - handler.fs.write.assert_called_once_with("test.txt", "Hello world!") + handler.fs.write.assert_called_once_with( # type: ignore[attr-defined] + "test.txt", + "Hello world!", + ) assert result is None @@ -130,5 +134,5 @@ def test_read_file(pact: MessagePact, handler: Handler) -> None: ) result = handler.process(msg) - handler.fs.read.assert_called_once_with("test.txt") + handler.fs.read.assert_called_once_with("test.txt") # type: ignore[attr-defined] assert result == "Hello world!" diff --git a/pyproject.toml b/pyproject.toml index 5e475df5b..5b96caacd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -176,8 +176,28 @@ ignore = [ "ANN102", # `cls` must be typed ] +extend-exclude = [ + "tests/*.py", + "pact/*.py", +] + [tool.ruff.pyupgrade] keep-runtime-typing = true [tool.ruff.pydocstyle] convention = "google" + +################################################################################ +## Black Configuration +################################################################################ + +[tool.black] +target-version = ["py38"] +extend-exclude = '^/(pact|tests)/(?!v3).+\.py$' + +################################################################################ +## Mypy Configuration +################################################################################ + +[tool.mypy] +exclude = '^(pact|tests)/(?!v3).+\.py$' diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/v3/__init__.py b/tests/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py index 2e9ed652d..5894c0489 100644 --- a/tests/v3/test_http_interaction.py +++ b/tests/v3/test_http_interaction.py @@ -407,7 +407,11 @@ def test_with_body_invalid(pact: Pact) -> None: pact.upon_receiving("") .with_request("GET", "/") .will_respond_with(200) - .with_body(json.dumps({"request": True}), "application/json", "Invalid") + .with_body( + json.dumps({"request": True}), + "application/json", + "Invalid", # type: ignore[arg-type] + ) ) @@ -504,11 +508,13 @@ async def test_multipart_file_request(pact: Pact, temp_dir: Path) -> None: with pact.serve() as srv, aiohttp.MultipartWriter() as mpwriter: mpwriter.append( fpy.open("rb"), - {"Content-Type": "text/x-python"}, + # TODO: Remove type ignore once aio-libs/aiohttp#7741 is resolved + {"Content-Type": "text/x-python"}, # type: ignore[arg-type] ) mpwriter.append( fpng.open("rb"), - {"Content-Type": "image/png"}, + # TODO: Remove type ignore once aio-libs/aiohttp#7741 is resolved + {"Content-Type": "image/png"}, # type: ignore[arg-type] ) async with aiohttp.ClientSession(srv.url) as session, session.post( From ea2086945104aac2d5d3717554494c1fc192d16b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 23:40:53 +0000 Subject: [PATCH 0076/1376] chore(deps): update pre-commit hook psf/black to v23.10.1 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2a9f2db7..f2627d4f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: stages: [pre-push] - repo: https://github.com/psf/black - rev: 23.10.0 + rev: 23.10.1 hooks: - id: black # Exclude python files in pact/** and tests/**, except for the From a2d177ded29d37beda5e7c8ceec7c1bda30fdfa8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 23:40:57 +0000 Subject: [PATCH 0077/1376] chore(deps): update actions/checkout action to v4 --- .github/workflows/labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index be54b1169..80034ca4a 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Synchronize labels uses: EndBug/label-sync@v2 From 6911385062193b1e3cbac2e18d169460b6d539d3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 23:40:49 +0000 Subject: [PATCH 0078/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.2 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2627d4f8..402e1da72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.1 + rev: v0.1.2 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 678cd003070d227b73eb3e28a97e19dea4140e90 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 26 Oct 2023 13:37:44 +1100 Subject: [PATCH 0079/1376] chore(pre-commit): add mypy This commit adds mypy to the pre-commit hooks. This is executed through `hatch` as mypy needs to be able to find and parse dependencies in hatch's virtual environment. Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 402e1da72..6e24f44e8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,3 +61,15 @@ repos: hooks: - id: commitizen stages: [commit-msg] + + - repo: local + hooks: + # Mypy is difficult to run pre-commit's isolated environment as it needs + # to be able to find dependencies. + - id: mypy + name: mypy + entry: hatch run mypy + language: system + types: [python] + exclude: ^(pact|tests)/(?!v3/).*\.py$ + stages: [pre-push] From db6374a46ca636e8068330fcedced420b36f62cf Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 26 Oct 2023 13:45:11 +1100 Subject: [PATCH 0080/1376] chore(ffi): add typing The C extension module `_ffi` is now minimally typed with `pyi` files. It provides typing for the `ffi` class which holds a number of utility functions such as `ffi.string`, `ffi.cast`, `ffi.new`, etc. The `lib` class is also annotated within the `pyi` file, but this merely means that the type checking does not complain about calls to `lib.pactffi_*` functions. As part of this commit, the `StringResult` has been refactored to ensure that it works better with the type checker. Signed-off-by: JP-Ellis --- pact/v3/_ffi.pyi | 6 ++++++ pact/v3/ffi.py | 54 +++++++++++++++++++++++++----------------------- 2 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 pact/v3/_ffi.pyi diff --git a/pact/v3/_ffi.pyi b/pact/v3/_ffi.pyi new file mode 100644 index 000000000..897259dca --- /dev/null +++ b/pact/v3/_ffi.pyi @@ -0,0 +1,6 @@ +import ctypes + +import cffi + +lib: ctypes.CDLL +ffi: cffi.FFI diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index e1426feec..c2cf57599 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -82,6 +82,7 @@ from __future__ import annotations import gc +import typing import warnings from enum import Enum from typing import TYPE_CHECKING, List @@ -89,6 +90,7 @@ from ._ffi import ffi, lib # type: ignore[import] if TYPE_CHECKING: + import cffi from pathlib import Path # The follow types are classes defined in the Rust code. Ultimately, a Python @@ -530,35 +532,27 @@ def __repr__(self) -> str: return f"PactSpecification.{self.name}" -class _StringResult(Enum): +class StringResult: """ - String Result. - - [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/mock_server/enum.StringResult.html) + String result. """ - FAILED = lib.StringResult_Failed - OK = lib.StringResult_Ok - - def __str__(self) -> str: - """ - Informal string representation of the String Result. + class _StringResult(Enum): """ - return self.name + Internal enum from Pact FFI. - def __repr__(self) -> str: - """ - Information-rich string representation of the String Result. + [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/mock_server/enum.StringResult.html) """ - return f"_StringResultEnum.{self.name}" + FAILED = lib.StringResult_Failed + OK = lib.StringResult_Ok -class StringResult: - """ - String result. - """ + class _StringResultCData: + tag: int + ok: cffi.FFI.CData + failed: cffi.FFI.CData - def __init__(self, cdata: ffi.CData) -> None: + def __init__(self, cdata: cffi.FFI.CData) -> None: """ Initialise a new String Result. @@ -569,7 +563,7 @@ def __init__(self, cdata: ffi.CData) -> None: if ffi.typeof(cdata).cname != "struct StringResult": msg = f"cdata must be a struct StringResult, got {ffi.typeof(cdata).cname}" raise TypeError(msg) - self._cdata: ffi.CData = cdata + self._cdata = typing.cast(StringResult._StringResultCData, cdata) def __str__(self) -> str: """ @@ -588,14 +582,14 @@ def is_failed(self) -> bool: """ Whether the result is an error. """ - return self._cdata.tag == _StringResult.FAILED.value + return self._cdata.tag == StringResult._StringResult.FAILED.value @property def is_ok(self) -> bool: """ Whether the result is ok. """ - return self._cdata.tag == _StringResult.OK.value + return self._cdata.tag == StringResult._StringResult.OK.value @property def text(self) -> str: @@ -603,7 +597,10 @@ def text(self) -> str: The text of the result. """ # The specific `.ok` or `.failed` does not matter. - return ffi.string(self._cdata.ok).decode("utf-8") + s = ffi.string(self._cdata.ok) + if isinstance(s, bytes): + return s.decode("utf-8") + return s def raise_exception(self) -> None: """ @@ -625,7 +622,10 @@ def version() -> str: Returns: The version of the pact_ffi library as a string, in the form of `x.y.z`. """ - return ffi.string(lib.pactffi_version()).decode("utf-8") + v = ffi.string(lib.pactffi_version()) + if isinstance(v, bytes): + return v.decode("utf-8") + return v def init(log_env_var: str) -> None: @@ -845,7 +845,9 @@ def get_error_message(length: int = 1024) -> str | None: if ret >= 0: # While the documentation says that the return value is the number of bytes # written, the actually return value is always 0 on success. - if msg := ffi.string(buffer).decode("utf-8"): + if msg := ffi.string(buffer): + if isinstance(msg, bytes): + return msg.decode("utf-8") return msg return None if ret == -1: From 97dfcb9f2db3f119618cc178d1d4f2eb5868fb52 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 26 Oct 2023 14:40:00 +1100 Subject: [PATCH 0081/1376] feat(ffi): add OwnedString class A number of FFI functions return strings that are owned by the library and must be freed manually. Unfortunately, returning a `str` from a function would result in a memory leak, as the Python runtime would not know to free the string. This commit adds an `OwnedString` class that wraps a `str` and a function that frees the string. The `__del__` method of the class calls the free function, ensuring that the string is freed when the object is garbage collected. Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 112 +++++++++++++++++++++++++++++++++---------- tests/v3/test_ffi.py | 16 +++++++ 2 files changed, 104 insertions(+), 24 deletions(-) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index c2cf57599..fd190ee1b 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -92,6 +92,7 @@ if TYPE_CHECKING: import cffi from pathlib import Path + from typing_extensions import Self # The follow types are classes defined in the Rust code. Ultimately, a Python # alternative should be implemented, but for now, the follow lines only serve @@ -613,6 +614,75 @@ def raise_exception(self) -> None: raise RuntimeError(self.text) +class OwnedString(str): + """ + A string that owns its own memory. + + This is used to ensure that the memory is freed when the string is + destroyed. + + As this is subclassed from `str`, it can be used in place of a normal string + in most cases. + """ + + def __new__(cls, ptr: cffi.FFI.CData) -> Self: + """ + Create a new Owned String. + + As this is a subclass of the immutable type `str`, we need to override + the `__new__` method to ensure that the string is initialised correctly. + """ + s = ffi.string(ptr) + return super().__new__(cls, s if isinstance(s, str) else s.decode("utf-8")) + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Owned String. + + Args: + ptr: + CFFI data structure. + """ + self._ptr = ptr + s = ffi.string(ptr) + self._string = s if isinstance(s, str) else s.decode("utf-8") + + def __str__(self) -> str: + """ + String representation of the Owned String. + """ + return self._string + + def __repr__(self) -> str: + """ + Debugging string representation of the Owned String. + """ + return f"" + + def __del__(self) -> None: + """ + Destructor for the Owned String. + """ + string_delete(self) + + def __eq__(self, other: object) -> bool: + """ + Equality comparison. + + Args: + other: + The object to compare to. + + Returns: + Whether the two objects are equal. + """ + if isinstance(other, OwnedString): + return self._ptr == other._ptr + if isinstance(other, str): + return self._string == other + return super().__eq__(other) + + def version() -> str: """ Return the version of the pact_ffi library. @@ -3000,7 +3070,7 @@ def message_delete(message: Message) -> None: raise NotImplementedError -def message_get_contents(message: Message) -> str: +def message_get_contents(message: Message) -> OwnedString | None: """ Get the contents of a `Message` in string form. @@ -3112,7 +3182,7 @@ def message_set_contents_bin( raise NotImplementedError -def message_get_description(message: Message) -> str: +def message_get_description(message: Message) -> OwnedString: r""" Get a copy of the description. @@ -4196,20 +4266,14 @@ def sync_message_get_provider_state_iter( raise NotImplementedError -def string_delete(string: str) -> None: +def string_delete(string: OwnedString) -> None: """ Delete a string previously returned by this FFI. [Rust `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_string_delete) - - It is explicitly allowed to pass a null pointer to this function; in that - case the function will do nothing. - - # Safety Passing an invalid pointer, or one that was not returned by a FFI - function can result in undefined behaviour. """ - raise NotImplementedError + lib.pactffi_string_delete(string._ptr) def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: @@ -4253,7 +4317,7 @@ def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: raise NotImplementedError -def get_tls_ca_certificate() -> str: +def get_tls_ca_certificate() -> OwnedString: """ Fetch the CA Certificate used to generate the self-signed certificate. @@ -4267,7 +4331,7 @@ def get_tls_ca_certificate() -> str: An empty string indicates an error reading the pem file. """ - raise NotImplementedError + return OwnedString(lib.pactffi_get_tls_ca_certificate()) def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) -> int: @@ -5624,7 +5688,7 @@ def message_with_metadata(message_handle: MessageHandle, key: str, value: str) - raise NotImplementedError -def message_reify(message_handle: MessageHandle) -> str: +def message_reify(message_handle: MessageHandle) -> OwnedString: """ Reifies the given message. @@ -6320,7 +6384,7 @@ def verifier_cli_args() -> str: raise NotImplementedError -def verifier_logs(handle: VerifierHandle) -> str: +def verifier_logs(handle: VerifierHandle) -> OwnedString: """ Extracts the logs for the verification run. @@ -6337,7 +6401,7 @@ def verifier_logs(handle: VerifierHandle) -> str: raise NotImplementedError -def verifier_logs_for_provider(provider_name: str) -> str: +def verifier_logs_for_provider(provider_name: str) -> OwnedString: """ Extracts the logs for the verification run for the provider name. @@ -6354,7 +6418,7 @@ def verifier_logs_for_provider(provider_name: str) -> str: raise NotImplementedError -def verifier_output(handle: VerifierHandle, strip_ansi: int) -> str: +def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: """ Extracts the standard output for the verification run. @@ -6373,7 +6437,7 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> str: raise NotImplementedError -def verifier_json(handle: VerifierHandle) -> str: +def verifier_json(handle: VerifierHandle) -> OwnedString: """ Extracts the verification result as a JSON document. @@ -6498,7 +6562,7 @@ def matches_string_value( expected_value: str, actual_value: str, cascaded: int, -) -> str: +) -> OwnedString: """ Determines if the string value matches the given matching rule. @@ -6529,7 +6593,7 @@ def matches_u64_value( expected_value: int, actual_value: int, cascaded: int, -) -> str: +) -> OwnedString: """ Determines if the unsigned integer value matches the given matching rule. @@ -6559,7 +6623,7 @@ def matches_i64_value( expected_value: int, actual_value: int, cascaded: int, -) -> str: +) -> OwnedString: """ Determines if the signed integer value matches the given matching rule. @@ -6589,7 +6653,7 @@ def matches_f64_value( expected_value: float, actual_value: float, cascaded: int, -) -> str: +) -> OwnedString: """ Determines if the floating point value matches the given matching rule. @@ -6619,7 +6683,7 @@ def matches_bool_value( expected_value: int, actual_value: int, cascaded: int, -) -> str: +) -> OwnedString: """ Determines if the boolean value matches the given matching rule. @@ -6651,7 +6715,7 @@ def matches_binary_value( # noqa: PLR0913 actual_value: str, actual_value_len: int, cascaded: int, -) -> str: +) -> OwnedString: """ Determines if the binary value matches the given matching rule. @@ -6686,7 +6750,7 @@ def matches_json_value( expected_value: str, actual_value: str, cascaded: int, -) -> str: +) -> OwnedString: """ Determines if the JSON value matches the given matching rule. diff --git a/tests/v3/test_ffi.py b/tests/v3/test_ffi.py index f899144db..912411c53 100644 --- a/tests/v3/test_ffi.py +++ b/tests/v3/test_ffi.py @@ -51,3 +51,19 @@ def test_get_error_message() -> None: ret: int = ffi.lib.pactffi_validate_datetime(invalid_utf8, invalid_utf8) assert ret == 2 assert ffi.get_error_message() == "error parsing value as UTF-8" + + +def test_owned_string() -> None: + string = ffi.get_tls_ca_certificate() + assert isinstance(string, str) + assert len(string) > 0 + assert str(string) == string + assert repr(string).startswith("") + assert string.startswith("-----BEGIN CERTIFICATE-----") + assert string.endswith( + ( + "-----END CERTIFICATE-----\n", + "-----END CERTIFICATE-----\r\n", + ) + ) From 7c19e9b4e6c2e4eb02f67737387729087e8a814a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 27 Oct 2023 09:49:17 +1100 Subject: [PATCH 0082/1376] chore(labels): fix incorrect label alias Signed-off-by: JP-Ellis --- .github/labels.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/labels.yml b/.github/labels.yml index f8183813c..8b9ca9ab2 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -5,8 +5,6 @@ - name: area:v2 description: Relating to v2 code color: "C2E0C6" - aliases: - - documentation - name: area:examples description: Relating to the examples From 6899b506147f5a870ac78812bce6250edfcb1c5a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 20:22:21 +0000 Subject: [PATCH 0083/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.3 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e24f44e8..22042cfcf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.2 + rev: v0.1.3 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From edbfe8bffb65231bb2414571a1909d71eeda1536 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 27 Oct 2023 15:35:47 +1100 Subject: [PATCH 0084/1376] chore(deps): pin dev tools To ensure that the dev environment is more reproducible. Renovate should be good to make sure we keep these up to date. Signed-off-by: JP-Ellis --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5b96caacd..efac4a60d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ pact-verifier = "pact.cli.verify:main" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. types = [ - "mypy ~= 1.6.0", + "mypy == 1.6.0", "types-cffi ~= 1.0", "types-requests ~= 2.0", ] @@ -76,8 +76,8 @@ test = [ dev = [ "pact-python[types]", "pact-python[test]", - "black ~= 23.10.0", - "ruff ~= 0.1.0", + "black == 23.10.1", + "ruff == 0.1.3", ] ################################################################################ From 04ed4c714205114ebf7681b8c248883aea25fad7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 04:42:30 +0000 Subject: [PATCH 0085/1376] chore(deps): update dependency types/mypy to v1.6.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index efac4a60d..8ac8097c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ pact-verifier = "pact.cli.verify:main" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. types = [ - "mypy == 1.6.0", + "mypy ==1.6.1", "types-cffi ~= 1.0", "types-requests ~= 2.0", ] From 9a5c975e59000164e1e6f8660ad3a83376307485 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 27 Oct 2023 15:45:53 +1100 Subject: [PATCH 0086/1376] chore: exclude python 3.12 At this stage, Python 3.12 is not supported. There is a separate PR already resolving this, but it requires an update to a dependency. Signed-off-by: JP-Ellis --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8ac8097c1..244f24533 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ "Topic :: Software Development :: Testing", ] -requires-python = ">=3.8,<4.0" +requires-python = ">=3.8, <3.12" # Dependencies of Pact Python should be specified using the broadest range # compatible version unless: From e9e2ff615078fa245d4e0af4233db89df9b00378 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 30 Oct 2023 14:24:39 +1100 Subject: [PATCH 0087/1376] chore: fix wheel builds This commit introduces a few fixes were uncovered by the previous commit. Specifically: - Explicitly test the FFI - Cleanup the hatch build script and created a new `UnsupportedPlatformError` exception. - Update the wheel inclusions. Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 5 --- hatch_build.py | 81 ++++++++++++++++++++++++------------- pyproject.toml | 38 ++++++++++++----- 3 files changed, 81 insertions(+), 43 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aa05b14ac..4a18c3d82 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,11 +17,6 @@ concurrency: env: STABLE_PYTHON_VERSION: "3.11" CIBW_BUILD_FRONTEND: build - CIBW_TEST_COMMAND: > - python -c - "from pact import EachLike; - assert EachLike(1).generate() == {'json_class': 'Pact::ArrayLike', 'contents': 1, 'min': 1} - " jobs: build-x86_64: diff --git a/hatch_build.py b/hatch_build.py index 01b9d122e..c289baffd 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -36,6 +36,20 @@ PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{machine}.{ext}" +class UnsupportedPlatformError(RuntimeError): + """Raised when the current platform is not supported.""" + + def __init__(self, platform: str) -> None: + """ + Initialize the exception. + + Args: + platform: The unsupported platform. + """ + self.platform = platform + super().__init__(f"Unsupported platform {platform}") + + class PactBuildHook(BuildHookInterface[Any]): """Custom hook to download Pact binaries.""" @@ -66,8 +80,24 @@ def initialize( build_data["infer_tag"] = True build_data["pure_python"] = False - self.pact_bin_install(PACT_BIN_VERSION) - self.pact_lib_install(PACT_LIB_VERSION) + binaries_included = False + try: + self.pact_bin_install(PACT_BIN_VERSION) + binaries_included = True + except UnsupportedPlatformError as err: + msg = f"Pact binaries on not available for {err.platform}." + warnings.warn(msg, RuntimeWarning, stacklevel=2) + + try: + self.pact_lib_install(PACT_LIB_VERSION) + binaries_included = True + except UnsupportedPlatformError as err: + msg = f"Pact library is not available for {err.platform}" + warnings.warn(msg, RuntimeWarning, stacklevel=2) + + if not binaries_included: + msg = "Wheel does not include any binaries. Aborting." + raise UnsupportedPlatformError(msg) def pact_bin_install(self, version: str) -> None: """ @@ -84,7 +114,7 @@ def pact_bin_install(self, version: str) -> None: artifact = self._download(url) self._pact_bin_extract(artifact) - def _pact_bin_url(self, version: str) -> str | None: # noqa: PLR0911 + def _pact_bin_url(self, version: str) -> str | None: """ Generate the download URL for the Pact binaries. @@ -109,9 +139,7 @@ def _pact_bin_url(self, version: str) -> str | None: # noqa: PLR0911 elif platform.endswith("x86_64"): machine = "x86_64" else: - msg = f"Unknown macOS machine {platform}" - warnings.warn(msg, RuntimeWarning, stacklevel=2) - return None + raise UnsupportedPlatformError(platform) return PACT_BIN_URL.format( version=version, os=os, @@ -127,9 +155,7 @@ def _pact_bin_url(self, version: str) -> str | None: # noqa: PLR0911 elif platform.endswith(("x86", "win32")): machine = "x86" else: - msg = f"Unknown Windows machine {platform}" - warnings.warn(msg, RuntimeWarning, stacklevel=2) - return None + raise UnsupportedPlatformError(platform) return PACT_BIN_URL.format( version=version, os=os, @@ -137,16 +163,14 @@ def _pact_bin_url(self, version: str) -> str | None: # noqa: PLR0911 ext="zip", ) - if "linux" in platform and "musl" not in platform: + if "manylinux" 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}" - warnings.warn(msg, RuntimeWarning, stacklevel=2) - return None + raise UnsupportedPlatformError(platform) return PACT_BIN_URL.format( version=version, os=os, @@ -154,9 +178,7 @@ def _pact_bin_url(self, version: str) -> str | None: # noqa: PLR0911 ext="tar.gz", ) - msg = f"Unknown platform {platform}" - warnings.warn(msg, RuntimeWarning, stacklevel=2) - return None + raise UnsupportedPlatformError(platform) def _pact_bin_extract(self, artifact: Path) -> None: """ @@ -224,8 +246,7 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 elif platform.endswith("x86_64"): machine = "x86_64" else: - msg = f"Unknown macOS machine {platform}" - raise ValueError(msg) + raise UnsupportedPlatformError(platform) return PACT_LIB_URL.format( prefix="lib", version=version, @@ -240,8 +261,7 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 if platform.endswith("amd64"): machine = "x86_64" else: - msg = f"Unknown Windows machine {platform}" - raise ValueError(msg) + raise UnsupportedPlatformError(platform) return PACT_LIB_URL.format( prefix="", version=version, @@ -250,13 +270,12 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 ext="lib.gz", ) - if "linux" in platform and "musl" in platform: + if "musllinux" in platform: os = "linux" if platform.endswith("x86_64"): machine = "x86_64-musl" else: - msg = f"Unknown MUSL Linux machine {platform}" - raise ValueError(msg) + raise UnsupportedPlatformError(platform) return PACT_LIB_URL.format( prefix="lib", version=version, @@ -265,15 +284,14 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 ext="a.gz", ) - if "linux" in platform: + if "manylinux" in platform: os = "linux" if platform.endswith("x86_64"): machine = "x86_64" elif platform.endswith("aarch64"): machine = "aarch64" else: - msg = f"Unknown Linux machine {platform}" - raise ValueError(msg) + raise UnsupportedPlatformError(platform) return PACT_LIB_URL.format( prefix="lib", @@ -283,8 +301,7 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 ext="a.gz", ) - msg = f"Unknown platform {platform}" - raise ValueError(msg) + raise UnsupportedPlatformError(platform) def _pact_lib_extract(self, artifact: Path) -> None: """ @@ -296,6 +313,9 @@ def _pact_lib_extract(self, artifact: Path) -> None: Args: artifact: The URL to download the Pact binaries from. """ + # Pypy does not guarantee that the directory exists. + self.tmpdir.mkdir(parents=True, exist_ok=True) + if not str(artifact).endswith(".gz"): msg = f"Unknown artifact type {artifact}" raise ValueError(msg) @@ -320,6 +340,9 @@ def _pact_lib_header(self, url: str) -> list[str]: Args: url: The URL pointing to the Pact library artifact. """ + # Pypy does not guarantee that the directory exists. + self.tmpdir.mkdir(parents=True, exist_ok=True) + url = url.rsplit("/", 1)[0] + "/pact.h" artifact = self._download(url) includes: list[str] = [] @@ -387,7 +410,7 @@ def _pact_lib_cffi(self, includes: list[str]) -> None: libraries=["pact_ffi", *extra_libs], library_dirs=[str(self.tmpdir)], ) - output = ffibuilder.compile(verbose=True, tmpdir=str(self.tmpdir)) + output = Path(ffibuilder.compile(verbose=True, tmpdir=str(self.tmpdir))) shutil.copy(output, ROOT_DIR / "pact" / "v3") def _download(self, url: str) -> Path: diff --git a/pyproject.toml b/pyproject.toml index 244f24533..f2af5a0ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ requires-python = ">=3.8, <3.12" # - A specific feature is required in a new minor release # - A minor version address vulnerability which directly impacts Pact Python dependencies = [ + "cffi ~= 1.0", "click ~= 8.0", "fastapi ~= 0.0", "psutil ~= 5.0", @@ -92,12 +93,12 @@ build-backend = "hatchling.build" path = "pact/__version__.py" [tool.hatch.build] -include = ["pact/**/*.py", "*.md", "LICENSE"] +include = ["**/py.typed", "**/*.md", "LICENSE", "pact/**/*.py", "pact/**/*.pyi"] [tool.hatch.build.targets.wheel] # Ignore the data files in the wheel as their contents are already included # in the package. -artifacts = ["pact/bin/*", "pact/lib/*"] +artifacts = ["pact/bin/*", "pact/lib/*", "pact/v3/_ffi.*"] [tool.hatch.build.targets.wheel.hooks.custom] @@ -155,9 +156,9 @@ filterwarnings = [ [tool.coverage.report] exclude_lines = [ "if __name__ == .__main__.:", # Ignore non-runnable code - "if TYPE_CHECKING:", # Ignore typing - "raise NotImplementedError", # Ignore defensive assertions - "@(abc\\.)?abstractmethod", # Ignore abstract methods + "if TYPE_CHECKING:", # Ignore typing + "raise NotImplementedError", # Ignore defensive assertions + "@(abc\\.)?abstractmethod", # Ignore abstract methods ] ################################################################################ @@ -176,10 +177,7 @@ ignore = [ "ANN102", # `cls` must be typed ] -extend-exclude = [ - "tests/*.py", - "pact/*.py", -] +extend-exclude = ["tests/*.py", "pact/*.py"] [tool.ruff.pyupgrade] keep-runtime-typing = true @@ -201,3 +199,25 @@ extend-exclude = '^/(pact|tests)/(?!v3).+\.py$' [tool.mypy] exclude = '^(pact|tests)/(?!v3).+\.py$' + +################################################################################ +## CI Build Wheel +################################################################################ +[tool.cibuildwheel] +test-command = """ +python -c \ +"from pact import EachLike; \ +assert \ + EachLike(1).generate() \ + == {'json_class': 'Pact::ArrayLike', 'contents': 1, 'min': 1}; \ +import pact.v3.ffi; \ +assert isinstance(pact.v3.ffi.version(), str);\"""" + +[tool.cibuildwheel.macos] +# The repair tool unfortunately did not like the bundled Ruby distributable. +# TODO: Check whether delocate-wheel can be configured. +repair-wheel-command = "" + +[tool.cibuildwheel.windows] +# Skipping pypy, see giampaolo/psutil#2325 +skip = "pp*" From dc655f64d634c6c0a2d8879c539b2b3212761ab3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 30 Oct 2023 14:45:12 +1100 Subject: [PATCH 0088/1376] fix(deps): add yarl dependency The `yarl` dependency was initially only used in the test suite, but a previous commit made use of yarl's `URL` class and forgot to promote `yarl` to a main dependency of Pact Python. Signed-off-by: JP-Ellis --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f2af5a0ba..6a97c39fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ dependencies = [ "six ~= 1.0", "typing-extensions ~= 4.0 ; python_version < '3.10'", "uvicorn ~= 0.0", + "yarl ~= 1.0", ] [project.urls] @@ -72,7 +73,6 @@ test = [ "pytest-asyncio ~= 0.0", "pytest-cov ~= 4.0", "testcontainers ~= 3.0", - "yarl ~= 1.0", ] dev = [ "pact-python[types]", From aef10f26b8e40067ef79147186f68025d13f8f5e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 26 Oct 2023 20:47:00 +1100 Subject: [PATCH 0089/1376] feat(v3): implement Pact Handle methods This commit implements the methods for the Pact Handle class which correspond to the methods of the library which take `PactHandle` as an argument. Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 392 ++++++++++++++++++++++++++++++++---------- pact/v3/pact.py | 164 +++++++++++++++++- tests/v3/test_pact.py | 88 ++++++++++ 3 files changed, 545 insertions(+), 99 deletions(-) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index fd190ee1b..6b84d3aac 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -260,6 +260,7 @@ def __del__(self) -> None: """ Destructor for the Pact Handle. """ + cleanup_plugins(self) free_pact_handle(self) def __str__(self) -> str: @@ -329,19 +330,207 @@ class PactInteraction: class PactInteractionIterator: - ... + """ + Iterator over a Pact's interactions. + + Interactions encompasses all types of interactions, including HTTP + interactions and messages. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Pact Interaction Iterator. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct PactInteractionIterator *": + msg = ( + "ptr must be a struct PactInteractionIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "PactInteractionIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"PactInteractionIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Interaction Iterator. + """ + pact_interaction_iter_delete(self) + + def __next__(self) -> PactInteraction: + """ + Get the next interaction from the iterator. + """ + raise NotImplementedError class PactMessageIterator: - ... + """ + Iterator over a Pact's asynchronous messages. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Pact Message Iterator. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct PactMessageIterator *": + msg = ( + f"ptr must be a struct PactMessageIterator, got {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "PactMessageIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"PactMessageIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Message Iterator. + """ + pact_message_iter_delete(self) + + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> Message: + """ + Get the next message from the iterator. + """ + raise NotImplementedError class PactSyncHttpIterator: - ... + """ + Iterator over a Pact's synchronous HTTP interactions. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Pact Synchronous HTTP Iterator. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct PactSyncHttpIterator *": + msg = ( + "ptr must be a struct PactSyncHttpIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "PactSyncHttpIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"PactSyncHttpIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Synchronous HTTP Iterator. + """ + pact_sync_http_iter_delete(self) + + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> SynchronousHttp: + """ + Get the next message from the iterator. + """ + raise NotImplementedError class PactSyncMessageIterator: - ... + """ + Iterator over a Pact's synchronous messages. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Pact Synchronous Message Iterator. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct PactSyncMessageIterator *": + msg = ( + "ptr must be a struct PactSyncMessageIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "PactSyncMessageIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"PactSyncMessageIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Synchronous Message Iterator. + """ + pact_sync_message_iter_delete(self) + + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> SynchronousMessage: + """ + Get the next message from the iterator. + """ + raise NotImplementedError class Provider: @@ -2798,7 +2987,7 @@ def pact_message_iter_delete(iter: PactMessageIterator) -> None: [Rust `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_message_iter_delete) """ - raise NotImplementedError + lib.pactffi_pact_message_iter_delete(iter._ptr) def pact_message_iter_next(iter: PactMessageIterator) -> Message: @@ -2862,7 +3051,7 @@ def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: [Rust `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) """ - raise NotImplementedError + lib.pactffi_pact_sync_message_iter_delete(iter._ptr) def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: @@ -2900,7 +3089,7 @@ def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: [Rust `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) """ - raise NotImplementedError + lib.pactffi_pact_sync_http_iter_delete(iter._ptr) def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction: @@ -2938,7 +3127,7 @@ def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: [Rust `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_iter_delete) """ - raise NotImplementedError + lib.pactffi_pact_interaction_iter_delete(iter._ptr) def matching_rule_to_json(rule: MatchingRule) -> str: @@ -5095,28 +5284,32 @@ def with_query_parameter_v2( raise RuntimeError(msg) -def with_specification(pact: PactHandle, version: PactSpecification) -> bool: +def with_specification(pact: PactHandle, version: PactSpecification) -> None: """ Sets the specification version for a given Pact model. - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started) or the version is invalid. - [Rust `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_specification) - * `pact` - Handle to a Pact model - * `version` - the spec version to use + Args: + pact: + Handle to a Pact model. + + version: + The spec version to use. """ - raise NotImplementedError + success: bool = lib.pactffi_with_specification(pact._ref, version.value) + if not success: + msg = f"Failed to set Pact specification for {pact}" + raise RuntimeError(msg) def with_pact_metadata( pact: PactHandle, - namespace_: str, + namespace: str, name: str, value: str, -) -> bool: +) -> None: """ Sets the additional metadata on the Pact file. @@ -5127,12 +5320,28 @@ def with_pact_metadata( [Rust `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_pact_metadata) - * `pact` - Handle to a Pact model - * `namespace` - the top level metadat key to set any key values on - * `name` - the key to set - * `value` - the value to set + Args: + pact: + Handle to a Pact model + + namespace: + The top level metadat key to set any key values on + + name: + The key to set + + value: + The value to set """ - raise NotImplementedError + success: bool = lib.pactffi_with_pact_metadata( + pact._ref, + namespace.encode("utf-8"), + name.encode("utf-8"), + value.encode("utf-8"), + ) + if not success: + msg = f"Failed to set Pact metadata for {pact} with {namespace}.{name}={value}" + raise RuntimeError(msg) def with_header_v2( @@ -5499,9 +5708,6 @@ def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: r""" Get an iterator over all the messages of the Pact. - The returned iterator needs to be freed with - `pactffi_pact_message_iter_delete`. - [Rust `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_handle_get_message_iter) @@ -5516,7 +5722,7 @@ def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: This function may fail if any of the Rust strings contain embedded null ('\0') bytes. """ - raise NotImplementedError + return PactMessageIterator(lib.pactffi_pact_handle_get_message_iter(pact._ref)) def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterator: @@ -5540,7 +5746,9 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat This function may fail if any of the Rust strings contain embedded null ('\0') bytes. """ - raise NotImplementedError + return PactSyncMessageIterator( + lib.pactffi_pact_handle_get_sync_message_iter(pact._ref) + ) def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: @@ -5564,7 +5772,7 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: This function may fail if any of the Rust strings contain embedded null ('\0') bytes. """ - raise NotImplementedError + return PactSyncHttpIterator(lib.pactffi_pact_handle_get_sync_http_iter(pact._ref)) def new_message_pact(consumer_name: str, provider_name: str) -> MessagePactHandle: @@ -5760,54 +5968,46 @@ def with_message_pact_metadata( raise NotImplementedError -def pact_handle_write_file(pact: PactHandle, directory: str, *, overwrite: bool) -> int: +def pact_handle_write_file( + pact: PactHandle, + directory: Path | str | None, + *, + overwrite: bool, +) -> None: """ External interface to write out the pact file. - This function should be called if all the consumer tests have passed. The - directory to write the file to is passed as the second parameter. If a NULL - pointer is passed, the current working directory is used. - [Rust `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_handle_write_file) - If overwrite is true, the file will be overwritten with the contents of the - current pact. Otherwise, it will be merged with any existing pact file. - - Returns 0 if the pact file was successfully written. Returns a positive code - if the file can not be written or the function panics. - - # Safety - - The directory parameter must either be NULL or point to a valid NULL - terminated string. - - # Errors - - Errors are returned as positive values. - - | Error | Description | - |-------|-------------| - | 1 | The function panicked. | - | 2 | The pact file was not able to be written. | - | 3 | The pact for the given handle was not found. | - """ - raise NotImplementedError - - -def new_async_message(pact: PactHandle, description: str) -> MessageHandle: - """ - Creates a new V4 asynchronous message and returns a handle to it. + This function should be called if all the consumer tests have passed. - [Rust - `pactffi_new_async_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_async_message) - - * `description` - The message description. It needs to be unique for each - Message. + Args: + directory: + The directory to write the file to. If `None`, the current working + directory is used. - Returns a new `MessageHandle`. + overwrite: + If `True`, the file will be overwritten with the contents of the + current pact. Otherwise, it will be merged with any existing pact + file. """ - raise NotImplementedError + ret: int = lib.pactffi_pact_handle_write_file( + pact._ref, + str(directory).encode("utf-8") if directory else ffi.NULL, + overwrite, + ) + if ret == 0: + return + if ret == 1: + msg = f"The function panicked while writing {pact} to {directory}." + elif ret == 2: + msg = f"The pact file was not able to be written for {pact}." + elif ret == 3: + msg = f"The pact for {pact} was not found." + else: + msg = f"Unknown error writing {pact} to {directory}." + raise RuntimeError(msg) def free_pact_handle(pact: PactHandle) -> None: @@ -6452,41 +6652,47 @@ def verifier_json(handle: VerifierHandle) -> OwnedString: raise NotImplementedError -def using_plugin(pact: PactHandle, plugin_name: str, plugin_version: str) -> int: +def using_plugin( + pact: PactHandle, + plugin_name: str, + plugin_version: str | None, +) -> None: """ Add a plugin to be used by the test. The plugin needs to be installed correctly for this function to work. - [Rust - `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_using_plugin) - - * `plugin_name` is the name of the plugin to load. - * `plugin_version` is the version of the plugin to load. It is optional, and - can be NULL. - - Returns zero on success, and a positive integer value on failure. - Note that plugins run as separate processes, so will need to be cleaned up - afterwards by calling `pactffi_cleanup_plugins` otherwise you will have - plugin processes left running. - - # Safety - - `plugin_name` must be a valid pointer to a NULL terminated string. - `plugin_version` may be null, and if not NULL must also be a valid pointer - to a NULL terminated string. Invalid pointers will result in undefined - behaviour. + afterwards by calling [`cleanup_plugins`][pact.v3.ffi.cleanup_plugins] + otherwise you will have plugin processes left running. - # Errors + [Rust + `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_using_plugin) - * `1` - A general panic was caught. - * `2` - Failed to load the plugin. - * `3` - Pact Handle is not valid. + Args: + plugin_name: + Name of the plugin to use. - When an error errors, LAST_ERROR will contain the error message. + plugin_version: + Version of the plugin to use. If `None`, the latest version will be + used. """ - raise NotImplementedError + ret: int = lib.pactffi_using_plugin( + pact._ref, + plugin_name.encode("utf-8"), + plugin_version.encode("utf-8") if plugin_version is not None else ffi.NULL, + ) + if ret == 0: + return + if ret == 1: + msg = f"A general panic was caught: {get_error_message()}" + elif ret == 2: + msg = f"Failed to load the plugin {plugin_name}." + elif ret == 3: + msg = f"The Pact handle {pact} is invalid." + else: + msg = f"There was an unknown error loading the plugin {plugin_name}." + raise RuntimeError(msg) def cleanup_plugins(pact: PactHandle) -> None: @@ -6499,7 +6705,7 @@ def cleanup_plugins(pact: PactHandle) -> None: [Rust `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_cleanup_plugins) """ - raise NotImplementedError + lib.pactffi_cleanup_plugins(pact._ref) def interaction_contents( diff --git a/pact/v3/pact.py b/pact/v3/pact.py index b4dfe3af9..19222d5b2 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -956,18 +956,77 @@ def provider(self) -> str: """ return self._provider - @overload - def upon_receiving( + def with_specification( self, - description: str, - ) -> HttpInteraction: - ... + version: str | pact.v3.ffi.PactSpecification, + ) -> Self: + """ + Set the Pact specification version. + + The Pact specification version indicates the features which are + supported by the Pact, and certain default behaviours. + + Args: + version: + Pact specification version. The can be either a string or a + [`PactSpecification`][pact.v3.ffi.PactSpecification] instance. + + The version string is case insensitive and has an optional `v` + prefix. + """ + if isinstance(version, str): + version = version.upper().replace(".", "_") + if version.startswith("V"): + version = pact.v3.ffi.PactSpecification[version] + else: + version = pact.v3.ffi.PactSpecification["V" + version] + pact.v3.ffi.with_specification(self._handle, version) + return self + + def using_plugin(self, name: str, version: str | None = None) -> Self: + """ + Add a plugin to be used by the test. + + Plugins extend the functionality of Pact. + + Args: + name: + Name of the plugin. + + version: + Version of the plugin. This is optional and can be `None`. + """ + pact.v3.ffi.using_plugin(self._handle, name, version) + return self + + def with_metadata( + self, + namespace: str, + metadata: dict[str, str], + ) -> Self: + """ + Set additional metadata for the Pact. + + A common use for this function is to add information about the client + library (name, version, hash, etc.) to the Pact. + + Args: + namespace: + Namespace for the metadata. This is used to group the metadata + together. + + metadata: + Key-value pairs of metadata to add to the Pact. + """ + for k, v in metadata.items(): + pact.v3.ffi.with_pact_metadata(self._handle, namespace, k, v) + return self @overload def upon_receiving( self, description: str, - interaction: Literal["HTTP"], + interaction: Literal["HTTP"] = ..., ) -> HttpInteraction: ... @@ -1058,6 +1117,99 @@ def serve( transport_config, ) + def messages(self) -> pact.v3.ffi.PactMessageIterator: + """ + Iterate over the messages in the Pact. + + This function returns an iterator over the messages in the Pact. This + is useful for validating the Pact against the provider. + + ```python + pact = Pact("consumer", "provider") + with pact.serve() as srv: + for message in pact.messages(): + # Validate the message against the provider. + ... + ``` + + Note that the Pact must be written to a file before the messages can be + iterated over. This is because the messages are not stored in memory, + but rather are streamed directly from the file. + """ + return pact.v3.ffi.pact_handle_get_message_iter(self._handle) + + @overload + def interactions(self, type: Literal["HTTP"]) -> pact.v3.ffi.PactSyncHttpIterator: + ... + + @overload + def interactions( + self, type: Literal["Sync"] + ) -> pact.v3.ffi.PactSyncMessageIterator: + ... + + @overload + def interactions(self, type: Literal["Async"]) -> pact.v3.ffi.PactMessageIterator: + ... + + def interactions( + self, type: str = "HTTP" + ) -> ( + pact.v3.ffi.PactSyncHttpIterator + | pact.v3.ffi.PactSyncMessageIterator + | pact.v3.ffi.PactMessageIterator + ): + """ + Return an iterator over the Pact's interactions. + + The type is used to specify the kind of interactions that will be + iterated over. If `"All"` is specified (the default), then all + interactions will be iterated over. + """ + # TODO: The FFI does not have a way to iterate over all interactions, unless + # you have a pointer to the pact. See + # pact-foundation/pact-reference#333. + # if type == "All": + # return pact.v3.ffi.pact_model_interaction_iterator(self._handle) + if type == "HTTP": + return pact.v3.ffi.pact_handle_get_sync_http_iter(self._handle) + if type == "Sync": + return pact.v3.ffi.pact_handle_get_sync_message_iter(self._handle) + if type == "Async": + return pact.v3.ffi.pact_handle_get_message_iter(self._handle) + msg = f"Unknown interaction type: {type}" + raise ValueError(msg) + + def write_file( + self, + directory: Path | str = Path.cwd(), + *, + overwrite: bool = False, + ): + """ + Write out the pact to a file. + + This function should be called once all of the consumer tests have been + run. It writes the Pact to a file, which can then be used to validate + the provider. + + Args: + directory: + The directory to write the pact to. If the directory does not + exist, it will be created. The filename will be + automatically generated from the underlying Pact. + + overwrite: + If set to True, the file will be overwritten if it already + exists. Otherwise, the contents of the file will be merged with + the existing file. + """ + pact.v3.ffi.pact_handle_write_file( + self._handle, + directory, + overwrite=overwrite, + ) + class PactServer: """ diff --git a/tests/v3/test_pact.py b/tests/v3/test_pact.py index 6f24c04a6..4759be615 100644 --- a/tests/v3/test_pact.py +++ b/tests/v3/test_pact.py @@ -3,6 +3,9 @@ """ from __future__ import annotations +import json +from pathlib import Path +from typing import Literal import pytest from pact.v3 import Pact @@ -19,6 +22,9 @@ def pact() -> Pact: def test_init(pact: Pact) -> None: assert pact.consumer == "consumer" assert pact.provider == "provider" + assert str(pact) == "consumer -> provider" + assert repr(pact).startswith("") def test_empty_consumer() -> None: @@ -41,3 +47,85 @@ def test_serve(pact: Pact) -> None: assert srv.url.path == "/" assert srv / "foo" == srv.url / "foo" assert str(srv / "foo") == f"http://localhost:{srv.port}/foo" + + +@pytest.mark.skip(reason="TODO: implement") +def test_using_plugin(pact: Pact) -> None: + pact.using_plugin("core/transport/http") + + +def test_metadata(pact: Pact) -> None: + pact.with_metadata("test", {"version": "1.2.3", "hash": "abcdef"}) + + +def test_invalid_interaction(pact: Pact) -> None: + with pytest.raises(ValueError, match="Invalid interaction type: .*"): + pact.upon_receiving("a basic request", "Invalid") # type: ignore[call-overload] + + +@pytest.mark.parametrize( + "interaction_type", + [ + "HTTP", + "Sync", + "Async", + ], +) +def test_interactions_iter( + pact: Pact, + interaction_type: Literal[ + "HTTP", + "Sync", + "Async", + ], +) -> None: + interactions = pact.interactions(interaction_type) + assert interactions is not None + for _interaction in interactions: + # This should be an empty list and therefore the error should never be + # raised. + raise RuntimeError("Should not be reached") + else: + print("Ok") + + +def test_messages(pact: Pact) -> None: + messages = pact.messages() + assert messages is not None + for _message in messages: + # This should be an empty list and therefore the error should never be + # raised. + raise RuntimeError("Should not be reached") + else: + print("Ok") + + +def test_write_file(pact: Pact, temp_dir: Path) -> None: + pact.write_file(temp_dir) + outfile = temp_dir / "consumer-provider.json" + assert outfile.exists() + assert outfile.is_file() + + data = json.load(outfile.open("r")) + assert data["consumer"]["name"] == "consumer" + assert data["provider"]["name"] == "provider" + assert len(data["interactions"]) == 0 + + +@pytest.mark.parametrize( + "version", + [ + "1", + "1.1", + "2", + "3", + "4", + "V1", + "V1.1", + "V2", + "V3", + "V4", + ], +) +def test_specification(pact: Pact, version: str) -> None: + pact.with_specification(version) From 887d5e5241080d378f6f38f180002bf7450a3f35 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 31 Oct 2023 11:26:35 +1100 Subject: [PATCH 0090/1376] fix(v3): add __next__ implementation The various Pact iterators are empty at this stage as the returned classes are not implemented. As a result, this merely tests that the FFI `*_next` functions return a null pointer which raises a `StopIteration` exception in Python. Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 102 ++++++++++++------------------------------------- 1 file changed, 24 insertions(+), 78 deletions(-) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 6b84d3aac..b33b30c0e 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -375,7 +375,7 @@ def __next__(self) -> PactInteraction: """ Get the next interaction from the iterator. """ - raise NotImplementedError + return pact_interaction_iter_next(self) class PactMessageIterator: @@ -426,7 +426,7 @@ def __next__(self) -> Message: """ Get the next message from the iterator. """ - raise NotImplementedError + return pact_message_iter_next(self) class PactSyncHttpIterator: @@ -478,7 +478,7 @@ def __next__(self) -> SynchronousHttp: """ Get the next message from the iterator. """ - raise NotImplementedError + return pact_sync_http_iter_next(self) class PactSyncMessageIterator: @@ -530,7 +530,7 @@ def __next__(self) -> SynchronousMessage: """ Get the next message from the iterator. """ - raise NotImplementedError + return pact_sync_message_iter_next(self) class Provider: @@ -2994,54 +2994,28 @@ def pact_message_iter_next(iter: PactMessageIterator) -> Message: """ Get the next message from the message pact. - As the messages returned are owned by the iterator, they do not need to be - deleted but will be cleaned up when the iterator is deleted. - [Rust `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_message_iter_next) - - Will return a NULL pointer when the iterator has advanced past the end of - the list. - - # Safety - - This function is safe. - - Deleting a message returned by the iterator can lead to undefined behaviour. - - # Error Handling - - This function will return a NULL pointer if passed a NULL pointer or if an - error occurs. """ - raise NotImplementedError + ptr = lib.pactffi_pact_message_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + raise NotImplementedError() + return Message(ptr) def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMessage: """ Get the next synchronous request/response message from the V4 pact. - As the messages returned are owned by the iterator, they do not need to be - deleted but will be cleaned up when the iterator is deleted. - [Rust `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_sync_message_iter_next) - - Will return a NULL pointer when the iterator has advanced past the end of - the list. - - # Safety - - This function is safe. - - Deleting a message returned by the iterator can lead to undefined behaviour. - - # Error Handling - - This function will return a NULL pointer if passed a NULL pointer or if an - error occurs. """ - raise NotImplementedError + ptr = lib.pactffi_pact_sync_message_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + raise NotImplementedError() + return SynchronousMessage(ptr) def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: @@ -3058,28 +3032,14 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: """ Get the next synchronous HTTP request/response interaction from the V4 pact. - As the interactions returned are owned by the iterator, they do not need to - be deleted but will be cleaned up when the iterator is deleted. - [Rust `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_sync_http_iter_next) - - Will return a NULL pointer when the iterator has advanced past the end of - the list. - - # Safety - - This function is safe. - - Deleting an interaction returned by the iterator can lead to undefined - behaviour. - - # Error Handling - - This function will return a NULL pointer if passed a NULL pointer or if an - error occurs. """ - raise NotImplementedError + ptr = lib.pactffi_pact_sync_http_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + raise NotImplementedError() + return SynchronousHttp(ptr) def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: @@ -3096,28 +3056,14 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction """ Get the next interaction from the pact. - As the interactions returned are owned by the iterator, they do not need to - be deleted but will be cleaned up when the iterator is deleted. - [Rust `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_iter_next) - - Will return a NULL pointer when the iterator has advanced past the end of - the list. - - # Safety - - This function is safe. - - Deleting an interaction returned by the iterator can lead to undefined - behaviour. - - # Error Handling - - This function will return a NULL pointer if passed a NULL pointer or if an - error occurs. """ - raise NotImplementedError + ptr = lib.pactffi_pact_interaction_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + raise NotImplementedError() + return PactInteraction(ptr) def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: From 03462ca0d2c7a7c342aaa9955a72fe344ae06626 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 31 Oct 2023 12:05:55 +1100 Subject: [PATCH 0091/1376] fix(example): unknown action Fix an issue with an 'unknown action' error being raised due to the branch not returning from the function. Signed-off-by: JP-Ellis --- examples/src/message.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/src/message.py b/examples/src/message.py index 977c343c7..cab137ec0 100644 --- a/examples/src/message.py +++ b/examples/src/message.py @@ -61,10 +61,11 @@ def process(self, event: Dict[str, Any]) -> Union[str, None]: if event["action"] == "WRITE": self.fs.write(event["path"], event.get("contents", "")) + return None if event["action"] == "READ": return self.fs.read(event["path"]) - msg = "Invalid action." + msg = f"Invalid action: {event['action']!r}" raise ValueError(msg) @staticmethod From e942149e7227607548060476fe4e18ba77040596 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 3 Nov 2023 15:46:37 +1100 Subject: [PATCH 0092/1376] chore(ci): revise pypi publishing Firstly, this commits adds the building of a source distribution. When a Python package is installed, it is typically installed from a binary wheel and will fallback to the source distribution if it cannot find a matching wheel. Additionally, I have created a new GitHub environment called `pypi` which stores a token authorising publication to Pact Python (and only Pact Python). The token is associated with my account, and will hopefully be replaced by trusted publishing soon (which bypasses tokens entirely). Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 44 ++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a18c3d82..f2d5cd65d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,37 @@ env: CIBW_BUILD_FRONTEND: build jobs: + build-sdit: + name: Build source distribution + + if: github.event_name == 'push' || ! github.event.pull_request.draft + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + # Fetch all tags + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.STABLE_PYTHON_VERSION }} + + - name: Install hatch + run: pip install --upgrade hatch + + - name: Create source distribution + run: | + hatch build --target sdist + + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: wheels + path: ./dist/*.tar.* + if-no-files-found: error + build-x86_64: name: Build wheels on ${{ matrix.os }} (x86, 64-bit) @@ -123,20 +154,23 @@ jobs: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') runs-on: ubuntu-latest - environment: Upload Python Package + environment: pypi needs: [check] + permissions: + # Required for trusted publishing + id-token: write + steps: - uses: actions/download-artifact@v3 with: name: wheels - path: wheelhouse + path: wheels - name: Push build artifacts to PyPI uses: pypa/gh-action-pypi-publish@v1.8.10 with: skip-existing: true - user: ${{ secrets.PYPI_USERNAME }} - password: ${{ secrets.PYPI_PASSWORD }} - packages-dir: wheelhouse + password: ${{ secrets.PYPI_TOKEN }} + packages-dir: wheels From 07d99f2b98b82e2f11bbf7be2c49da0b78f8ee4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 21:47:37 +0000 Subject: [PATCH 0093/1376] chore(deps): update dependency dev/ruff to v0.1.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6a97c39fc..80b976959 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ dev = [ "pact-python[types]", "pact-python[test]", "black == 23.10.1", - "ruff == 0.1.3", + "ruff ==0.1.4", ] ################################################################################ From 9b7b33730478aa3cc9605ed951aaaf1dc476fd6f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 21:47:41 +0000 Subject: [PATCH 0094/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.4 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 22042cfcf..28987809f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.3 + rev: v0.1.4 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 1c89522542361559ac6809edc8f6f3edf465d27e Mon Sep 17 00:00:00 2001 From: Neringa Altanaite Date: Fri, 3 Nov 2023 15:30:10 +0100 Subject: [PATCH 0095/1376] fix(example): publish_verification_results typo The example has a typo and use `published_verification_results` instead of `publish_verification_results`. Once this was fixed, a new error was uncovered whereby the authentication with the broker failed which caused the publication of the verification results to fail. Co-authored-by: Neringa Altanaitename Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- examples/tests/test_01_provider_fastapi.py | 7 ++++++- examples/tests/test_01_provider_flask.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/tests/test_01_provider_fastapi.py index 03d8d382c..1d011ed7f 100644 --- a/examples/tests/test_01_provider_fastapi.py +++ b/examples/tests/test_01_provider_fastapi.py @@ -147,7 +147,12 @@ def test_against_broker(broker: URL, verifier: Verifier) -> None: """ code, _ = verifier.verify_with_broker( broker_url=str(broker), - published_verification_results=True, + # Despite the auth being set in the broker URL, we still need to pass + # the username and password to the verify_with_broker method. + broker_username=broker.user, + broker_password=broker.password, + publish_version="0.0.0", + publish_verification_results=True, provider_states_setup_url=str(PROVIDER_URL / "_pact" / "provider_states"), ) diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py index 05323246e..20a84ee98 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/tests/test_01_provider_flask.py @@ -135,7 +135,12 @@ def test_against_broker(broker: URL, verifier: Verifier) -> None: """ code, _ = verifier.verify_with_broker( broker_url=str(broker), - published_verification_results=True, + # Despite the auth being set in the broker URL, we still need to pass + # the username and password to the verify_with_broker method. + broker_username=broker.user, + broker_password=broker.password, + publish_version="0.0.0", + publish_verification_results=True, provider_states_setup_url=str(PROVIDER_URL / "_pact" / "provider_states"), ) From 9d3bc76c085bd149ed6ef48513729122de028d5a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 6 Nov 2023 13:52:11 +1100 Subject: [PATCH 0096/1376] fix(example): publish message pact Building on the work from @neringaalt, I have updated the message pact example so as to publish the verification of the pact back to the pact broker. Signed-off-by: JP-Ellis --- examples/tests/test_03_message_provider.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/tests/test_03_message_provider.py b/examples/tests/test_03_message_provider.py index bf75e736a..9a9ff3f7d 100644 --- a/examples/tests/test_03_message_provider.py +++ b/examples/tests/test_03_message_provider.py @@ -65,4 +65,12 @@ def test_verify(broker: URL) -> None: ) with provider: - provider.verify_with_broker(broker_url=str(broker)) + provider.verify_with_broker( + broker_url=str(broker), + # Despite the auth being set in the broker URL, we still need to pass + # the username and password to the verify_with_broker method. + broker_username=broker.user, + broker_password=broker.password, + publish_version="0.0.0", + publish_verification_results=True, + ) From 7b9b1d3c4e6b437499e2436d5e6c0ae2ea9fe794 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 3 Nov 2023 09:15:12 +1100 Subject: [PATCH 0097/1376] chore(tests): reduce log verbosity Signed-off-by: JP-Ellis --- tests/v3/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/v3/conftest.py b/tests/v3/conftest.py index d509cba19..eb69d16fa 100644 --- a/tests/v3/conftest.py +++ b/tests/v3/conftest.py @@ -16,4 +16,4 @@ def _setup_pact_logging() -> None: """ from pact.v3 import ffi - ffi.log_to_stderr("DEBUG") + ffi.log_to_stderr("INFO") From d1724bfca8a7e1ff00881dea95e3a3c8bee3acde Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 3 Nov 2023 14:46:23 +1100 Subject: [PATCH 0098/1376] chore: fix ruff lints Due to an upstream bug in `ruff`, all Python files were inadvertently ignored instead of just the `v2` code. As a result, lint errors have been accummulating. Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 29 +++++++++++------- pact/v3/pact.py | 46 +++++++++++++++++----------- pyproject.toml | 39 +++++++++++++++++++++++- tests/v3/__init__.py | 3 ++ tests/v3/test_ffi.py | 2 +- tests/v3/test_http_interaction.py | 50 ++++++++----------------------- tests/v3/test_pact.py | 17 ++++++----- 7 files changed, 110 insertions(+), 76 deletions(-) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index b33b30c0e..88d0f0e57 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -90,8 +90,9 @@ from ._ffi import ffi, lib # type: ignore[import] if TYPE_CHECKING: - import cffi from pathlib import Path + + import cffi from typing_extensions import Self # The follow types are classes defined in the Rust code. Ultimately, a Python @@ -814,6 +815,8 @@ class OwnedString(str): in most cases. """ + __slots__ = ("_ptr", "_string") + def __new__(cls, ptr: cffi.FFI.CData) -> Self: """ Create a new Owned String. @@ -3000,7 +3003,7 @@ def pact_message_iter_next(iter: PactMessageIterator) -> Message: ptr = lib.pactffi_pact_message_iter_next(iter._ptr) if ptr == ffi.NULL: raise StopIteration - raise NotImplementedError() + raise NotImplementedError return Message(ptr) @@ -3014,7 +3017,7 @@ def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMes ptr = lib.pactffi_pact_sync_message_iter_next(iter._ptr) if ptr == ffi.NULL: raise StopIteration - raise NotImplementedError() + raise NotImplementedError return SynchronousMessage(ptr) @@ -3038,7 +3041,7 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: ptr = lib.pactffi_pact_sync_http_iter_next(iter._ptr) if ptr == ffi.NULL: raise StopIteration - raise NotImplementedError() + raise NotImplementedError return SynchronousHttp(ptr) @@ -3062,7 +3065,7 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction ptr = lib.pactffi_pact_interaction_iter_next(iter._ptr) if ptr == ffi.NULL: raise StopIteration - raise NotImplementedError() + raise NotImplementedError return PactInteraction(ptr) @@ -5693,7 +5696,7 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat ('\0') bytes. """ return PactSyncMessageIterator( - lib.pactffi_pact_handle_get_sync_message_iter(pact._ref) + lib.pactffi_pact_handle_get_sync_message_iter(pact._ref), ) @@ -5929,6 +5932,9 @@ def pact_handle_write_file( This function should be called if all the consumer tests have passed. Args: + pact: + Handle to a Pact model. + directory: The directory to write the file to. If `None`, the current working directory is used. @@ -5947,9 +5953,9 @@ def pact_handle_write_file( return if ret == 1: msg = f"The function panicked while writing {pact} to {directory}." - elif ret == 2: + elif ret == 2: # noqa: PLR2004 msg = f"The pact file was not able to be written for {pact}." - elif ret == 3: + elif ret == 3: # noqa: PLR2004 msg = f"The pact for {pact} was not found." else: msg = f"Unknown error writing {pact} to {directory}." @@ -6616,6 +6622,9 @@ def using_plugin( `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_using_plugin) Args: + pact: + Handle to a Pact model. + plugin_name: Name of the plugin to use. @@ -6632,9 +6641,9 @@ def using_plugin( return if ret == 1: msg = f"A general panic was caught: {get_error_message()}" - elif ret == 2: + elif ret == 2: # noqa: PLR2004 msg = f"Failed to load the plugin {plugin_name}." - elif ret == 3: + elif ret == 3: # noqa: PLR2004 msg = f"The Pact handle {pact} is invalid." else: msg = f"There was an unknown error loading the plugin {plugin_name}." diff --git a/pact/v3/pact.py b/pact/v3/pact.py index 19222d5b2..a2536ca54 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -1108,6 +1108,13 @@ def serve( transport_config: Configuration for the transport. This is specific to the transport being used and should be a JSON string. + + raises: Whether to raise an exception if there are mismatches + between the Pact and the server. If set to `False`, then the + mismatches must be handled manually. + + Returns: + A [`PactServer`][pact.v3.pact.PactServer] instance. """ return PactServer( self._handle, @@ -1139,21 +1146,23 @@ def messages(self) -> pact.v3.ffi.PactMessageIterator: return pact.v3.ffi.pact_handle_get_message_iter(self._handle) @overload - def interactions(self, type: Literal["HTTP"]) -> pact.v3.ffi.PactSyncHttpIterator: + def interactions(self, kind: Literal["HTTP"]) -> pact.v3.ffi.PactSyncHttpIterator: ... @overload def interactions( - self, type: Literal["Sync"] + self, + kind: Literal["Sync"], ) -> pact.v3.ffi.PactSyncMessageIterator: ... @overload - def interactions(self, type: Literal["Async"]) -> pact.v3.ffi.PactMessageIterator: + def interactions(self, kind: Literal["Async"]) -> pact.v3.ffi.PactMessageIterator: ... def interactions( - self, type: str = "HTTP" + self, + kind: str = "HTTP", ) -> ( pact.v3.ffi.PactSyncHttpIterator | pact.v3.ffi.PactSyncMessageIterator @@ -1162,30 +1171,26 @@ def interactions( """ Return an iterator over the Pact's interactions. - The type is used to specify the kind of interactions that will be - iterated over. If `"All"` is specified (the default), then all - interactions will be iterated over. + The kind is used to specify the type of interactions that will be + iterated over. """ - # TODO: The FFI does not have a way to iterate over all interactions, unless - # you have a pointer to the pact. See - # pact-foundation/pact-reference#333. - # if type == "All": - # return pact.v3.ffi.pact_model_interaction_iterator(self._handle) - if type == "HTTP": + # TODO(JP-Ellis): Add an iterator for `All` interactions. + # https://github.com/pact-foundation/pact-python/issues/451 + if kind == "HTTP": return pact.v3.ffi.pact_handle_get_sync_http_iter(self._handle) - if type == "Sync": + if kind == "Sync": return pact.v3.ffi.pact_handle_get_sync_message_iter(self._handle) - if type == "Async": + if kind == "Async": return pact.v3.ffi.pact_handle_get_message_iter(self._handle) - msg = f"Unknown interaction type: {type}" + msg = f"Unknown interaction type: {kind}" raise ValueError(msg) def write_file( self, - directory: Path | str = Path.cwd(), + directory: Path | str | None = None, *, overwrite: bool = False, - ): + ) -> None: """ Write out the pact to a file. @@ -1204,6 +1209,8 @@ def write_file( exists. Otherwise, the contents of the file will be merged with the existing file. """ + if directory is None: + directory = Path.cwd() pact.v3.ffi.pact_handle_write_file( self._handle, directory, @@ -1259,6 +1266,9 @@ def __init__( # noqa: PLR0913 transport_config: Configuration for the transport. This is specific to the transport being used and should be a JSON string. + + raises: Whether or not to raise an exception if the server is not + matched upon exit. """ self._host = host self._port = port diff --git a/pyproject.toml b/pyproject.toml index 80b976959..d2b18c90d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -175,9 +175,46 @@ ignore = [ "D212", # Multi-line docstring summary must start at the first line "ANN101", # `self` must be typed "ANN102", # `cls` must be typed + "FIX002", # Forbid TODO in comments ] -extend-exclude = ["tests/*.py", "pact/*.py"] +# TODO: Remove the explicity extend-exclude once astral-sh/ruff#6262 is fixed. +# https://github.com/pact-foundation/pact-python/issues/458 +extend-exclude = [ + # "pact/*.py", + # "pact/cli/*.py", + # "tests/*.py", + # "tests/cli/*.py", + "pact/__init__.py", + "pact/__version__.py", + "pact/broker.py", + "pact/cli/*.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", + "tests/__init__.py", + "tests/cli/*.py", + "tests/conftest.py", + "tests/test_broker.py", + "tests/test_constants.py", + "tests/test_consumer.py", + "tests/test_http_proxy.py", + "tests/test_matchers.py", + "tests/test_message_consumer.py", + "tests/test_message_pact.py", + "tests/test_message_provider.py", + "tests/test_pact.py", + "tests/test_verifier.py", + "tests/test_verify_wrapper.py", +] [tool.ruff.pyupgrade] keep-runtime-typing = true diff --git a/tests/v3/__init__.py b/tests/v3/__init__.py index e69de29bb..bcfbefdf7 100644 --- a/tests/v3/__init__.py +++ b/tests/v3/__init__.py @@ -0,0 +1,3 @@ +""" +Pact Python v3 tests. +""" diff --git a/tests/v3/test_ffi.py b/tests/v3/test_ffi.py index 912411c53..53e00361d 100644 --- a/tests/v3/test_ffi.py +++ b/tests/v3/test_ffi.py @@ -65,5 +65,5 @@ def test_owned_string() -> None: ( "-----END CERTIFICATE-----\n", "-----END CERTIFICATE-----\r\n", - ) + ), ) diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py index 5894c0489..fa5d6cbe0 100644 --- a/tests/v3/test_http_interaction.py +++ b/tests/v3/test_http_interaction.py @@ -105,11 +105,7 @@ async def test_with_header_request( ) with pact.serve() as srv: async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - headers=headers, - ) as resp: + async with session.request("GET", "/", headers=headers) as resp: assert resp.status == 200 @@ -134,10 +130,7 @@ async def test_with_header_response( ) with pact.serve() as srv: async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - ) as resp: + async with session.request("GET", "/") as resp: assert resp.status == 200 response_headers = [(h.lower(), v) for h, v in resp.headers.items()] for header, value in headers: @@ -182,11 +175,7 @@ async def test_set_header_request( ) with pact.serve() as srv: async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - headers=headers, - ) as resp: + async with session.request("GET", "/", headers=headers) as resp: assert resp.status == 200 @@ -205,11 +194,7 @@ async def test_set_header_request_repeat( ) with pact.serve() as srv: async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - headers=headers, - ) as resp: + async with session.request("GET", "/", headers=headers) as resp: assert resp.status == 500 @@ -233,10 +218,7 @@ async def test_set_header_response( ) with pact.serve() as srv: async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - ) as resp: + async with session.request("GET", "/") as resp: assert resp.status == 200 response_headers = [(h.lower(), v) for h, v in resp.headers.items()] for header, value in headers: @@ -258,11 +240,7 @@ async def test_set_header_response_repeat( ) with pact.serve() as srv: async with aiohttp.ClientSession(srv.url) as session: - async with session.request( - "GET", - "/", - headers=headers, - ) as resp: + async with session.request("GET", "/", headers=headers) as resp: assert resp.status == 200 response_headers = [(h.lower(), v) for h, v in resp.headers.items()] assert ("x-test", "2") in response_headers @@ -309,10 +287,7 @@ async def test_with_query_parameter_request( with pact.serve() as srv: async with aiohttp.ClientSession(srv.url) as session: url = srv.url.with_query(query) - async with session.request( - "GET", - url.path_qs, - ) as resp: + async with session.request("GET", url.path_qs) as resp: assert resp.status == 200 @@ -327,10 +302,7 @@ async def test_with_query_parameter_dict(pact: Pact) -> None: with pact.serve() as srv: async with aiohttp.ClientSession(srv.url) as session: url = srv.url.with_query({"test": "true", "foo": "bar"}) - async with session.request( - "GET", - url.path_qs, - ) as resp: + async with session.request("GET", url.path_qs) as resp: assert resp.status == 200 @@ -508,12 +480,14 @@ async def test_multipart_file_request(pact: Pact, temp_dir: Path) -> None: with pact.serve() as srv, aiohttp.MultipartWriter() as mpwriter: mpwriter.append( fpy.open("rb"), - # TODO: Remove type ignore once aio-libs/aiohttp#7741 is resolved + # TODO(JP-Ellis): Remove type ignore once aio-libs/aiohttp#7741 is resolved + # https://github.com/pact-foundation/pact-python/issues/450 {"Content-Type": "text/x-python"}, # type: ignore[arg-type] ) mpwriter.append( fpng.open("rb"), - # TODO: Remove type ignore once aio-libs/aiohttp#7741 is resolved + # TODO(JP-Ellis): Remove type ignore once aio-libs/aiohttp#7741 is resolved + # https://github.com/pact-foundation/pact-python/issues/450 {"Content-Type": "image/png"}, # type: ignore[arg-type] ) diff --git a/tests/v3/test_pact.py b/tests/v3/test_pact.py index 4759be615..64fd520a6 100644 --- a/tests/v3/test_pact.py +++ b/tests/v3/test_pact.py @@ -3,13 +3,16 @@ """ from __future__ import annotations + import json -from pathlib import Path -from typing import Literal +from typing import TYPE_CHECKING, Literal import pytest from pact.v3 import Pact +if TYPE_CHECKING: + from pathlib import Path + @pytest.fixture() def pact() -> Pact: @@ -84,9 +87,8 @@ def test_interactions_iter( for _interaction in interactions: # This should be an empty list and therefore the error should never be # raised. - raise RuntimeError("Should not be reached") - else: - print("Ok") + msg = "Should not be reached" + raise RuntimeError(msg) def test_messages(pact: Pact) -> None: @@ -95,9 +97,8 @@ def test_messages(pact: Pact) -> None: for _message in messages: # This should be an empty list and therefore the error should never be # raised. - raise RuntimeError("Should not be reached") - else: - print("Ok") + msg = "Should not be reached" + raise RuntimeError(msg) def test_write_file(pact: Pact, temp_dir: Path) -> None: From b0de84b1b00b9ce5b71ce2b5bce335092525b524 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 06:36:50 +0000 Subject: [PATCH 0099/1376] chore(deps): update pre-commit hook psf/black to v23.11.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28987809f..be530c70c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: stages: [pre-push] - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black # Exclude python files in pact/** and tests/**, except for the From 01fd6f0605721179f6f58a6c3cc26a5596adeea9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 06:36:46 +0000 Subject: [PATCH 0100/1376] chore(deps): update dependency dev/black to v23.11.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d2b18c90d..dd240a8a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ test = [ dev = [ "pact-python[types]", "pact-python[test]", - "black == 23.10.1", + "black ==23.11.0", "ruff ==0.1.4", ] From 5678b7a94af55c0e5dea5115a07fe90e1827e8d7 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 9 Nov 2023 09:33:01 +1100 Subject: [PATCH 0101/1376] fix/workflow-permissions --- .github/workflows/smartbear-issue-label-added.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/smartbear-issue-label-added.yml b/.github/workflows/smartbear-issue-label-added.yml index 8b68fed74..5407a64ec 100644 --- a/.github/workflows/smartbear-issue-label-added.yml +++ b/.github/workflows/smartbear-issue-label-added.yml @@ -8,4 +8,6 @@ on: jobs: call-workflow: uses: pact-foundation/.github/.github/workflows/smartbear-issue-label-added.yml@master + permissions: + issues: write secrets: inherit From e50edff0d042bae688acebf9e28ed270a3d3c127 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 9 Nov 2023 00:33:57 +0000 Subject: [PATCH 0102/1376] chore(deps): update dependency dev/ruff to v0.1.5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dd240a8a5..821f15c93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ dev = [ "pact-python[types]", "pact-python[test]", "black ==23.11.0", - "ruff ==0.1.4", + "ruff ==0.1.5", ] ################################################################################ From 607d28df8602b49d7ba814f16384de3f4b0fd3b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 9 Nov 2023 00:34:00 +0000 Subject: [PATCH 0103/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.5 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be530c70c..2b09d2c97 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.4 + rev: v0.1.5 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 87938cc39c361b02ffc52be10a69c7794d262222 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Nov 2023 16:02:13 +0000 Subject: [PATCH 0104/1376] chore(deps): update dependency types/mypy to v1.7.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 821f15c93..b42a579f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ pact-verifier = "pact.cli.verify:main" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. types = [ - "mypy ==1.6.1", + "mypy ==1.7.0", "types-cffi ~= 1.0", "types-requests ~= 2.0", ] From bf205fbf4e2e52ea8800bc0b0870467b51ba359b Mon Sep 17 00:00:00 2001 From: Filips Nastins Date: Sat, 11 Nov 2023 09:12:06 +0100 Subject: [PATCH 0105/1376] docs(readme): fix links to examples --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6f046fe56..a4640c5e5 100644 --- a/README.md +++ b/README.md @@ -464,9 +464,9 @@ The parameters for this differ slightly in naming from their CLI equivalents: You can see more details in the examples -- [Message Provider Verifier Test](https://github.com/pact-foundation/pact-python/tree/master/examples/message/tests/provider/test_message_provider.py) -- [Flask Provider Verifier Test](https://github.com/pact-foundation/pact-python/tree/master/examples/flask_provider/tests/provider/test_provider.py) -- [FastAPI Provider Verifier Test](https://github.com/pact-foundation/pact-python/tree/master/examples/fastapi_provider/tests/provider/test_provider.py) +- [Message Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_03_message_provider.py) +- [Flask Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_01_provider_flask.py) +- [FastAPI Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_01_provider_fastapi.py) ### Provider States From cb58c8d463e3c83436b33d89badb148e82c9faa3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:07:21 +0000 Subject: [PATCH 0106/1376] chore(deps): update pre-commit hook pre-commit/mirrors-prettier to v3.1.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b09d2c97..117b3cbbd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: - id: check-json5 - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v3.1.0 hooks: - id: prettier stages: [pre-push] From aa1567752eef6aceae91e8fdfa95a21b2a7739d7 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 2 Nov 2023 15:21:38 +1100 Subject: [PATCH 0107/1376] feat(v3): add mock server mismatches Add two methods to the Pact Server which allow for the verification of interactions. By default, if there are any mismatches, the server will raise an exception on exit. It is possible to bypass this if the failures are expected (such as in unit testing). Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 37 ++++++++-------- pact/v3/pact.py | 70 +++++++++++++++++++++++++++++-- tests/v3/test_http_interaction.py | 20 ++++++--- 3 files changed, 100 insertions(+), 27 deletions(-) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 88d0f0e57..5beacd231 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -82,10 +82,11 @@ from __future__ import annotations import gc +import json import typing import warnings from enum import Enum -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Any, List from ._ffi import ffi, lib # type: ignore[import] @@ -4581,42 +4582,42 @@ def create_mock_server_for_transport( raise RuntimeError(msg) -def mock_server_matched(mock_server_port: int) -> bool: +def mock_server_matched(mock_server_handle: PactServerHandle) -> bool: """ External interface to check if a mock server has matched all its requests. - The port number is passed in, and if all requests have been matched, true is - returned. False is returned if there is no mock server on the given port, or + If all requests have been matched, `true` is returned. `false` is returned if any request has not been successfully matched, or the method panics. [Rust `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mock_server_matched) """ - raise NotImplementedError + return lib.pactffi_mock_server_matched(mock_server_handle._ref) -def mock_server_mismatches(mock_server_port: int) -> str: +def mock_server_mismatches( + mock_server_handle: PactServerHandle, +) -> list[dict[str, Any]]: """ External interface to get all the mismatches from a mock server. - The port number of the mock server is passed in, and a pointer to a C string - with the mismatches in JSON format is returned. - [Rust `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mock_server_mismatches) - **NOTE:** The JSON string for the result is allocated on the heap, and will - have to be freed once the code using the mock server is complete. The - [`cleanup_mock_server`](fn.cleanup_mock_server.html) function is provided - for this purpose. - # Errors - If there is no mock server with the provided port number, or the function - panics, a NULL pointer will be returned. Don't try to dereference it, it - will not end well for you. + Raises: + RuntimeError: If there is no mock server with the provided port number, + or the function panics. """ - raise NotImplementedError + ptr = lib.pactffi_mock_server_mismatches(mock_server_handle._ref) + if ptr == ffi.NULL: + msg = f"No mock server found with port {mock_server_handle}." + raise RuntimeError(msg) + string = ffi.string(ptr) + if isinstance(string, bytes): + string = string.decode("utf-8") + return json.loads(string) def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: diff --git a/pact/v3/pact.py b/pact/v3/pact.py index a2536ca54..ac0325c13 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -1075,12 +1075,14 @@ def upon_receiving( msg = f"Invalid interaction type: {interaction}" raise ValueError(msg) - def serve( + def serve( # noqa: PLR0913 self, addr: str = "localhost", port: int = 0, transport: str = "http", transport_config: str | None = None, + *, + raises: bool = True, ) -> PactServer: """ Return a mock server for the Pact. @@ -1122,6 +1124,7 @@ def serve( port, transport, transport_config, + raises=raises, ) def messages(self) -> pact.v3.ffi.PactMessageIterator: @@ -1218,6 +1221,30 @@ def write_file( ) +class MismatchesError(Exception): + """ + Exception raised when there are mismatches between the Pact and the server. + """ + + def __init__(self, mismatches: list[dict[str, Any]]) -> None: + """ + Initialise a new MismatchesError. + + Args: + mismatches: + Mismatches between the Pact and the server. + """ + super().__init__(f"Mismatched interaction (count: {len(mismatches)})") + self._mismatches = mismatches + + @property + def mismatches(self) -> list[dict[str, Any]]: + """ + Mismatches between the Pact and the server. + """ + return self._mismatches + + class PactServer: """ Pact Server. @@ -1234,6 +1261,8 @@ def __init__( # noqa: PLR0913 port: int = 0, transport: str = "HTTP", transport_config: str | None = None, + *, + raises: bool = True, ) -> None: """ Initialise a new Pact Server. @@ -1267,8 +1296,8 @@ def __init__( # noqa: PLR0913 Configuration for the transport. This is specific to the transport being used and should be a JSON string. - raises: Whether or not to raise an exception if the server is not - matched upon exit. + raises: Whether or not to raise an exception if the server + is not matched upon exit. """ self._host = host self._port = port @@ -1276,6 +1305,7 @@ def __init__( # noqa: PLR0913 self._transport_config = transport_config self._pact_handle = pact_handle self._handle: None | pact.v3.ffi.PactServerHandle = None + self._raises = raises @property def port(self) -> int: @@ -1310,6 +1340,31 @@ def url(self) -> URL: """ return URL(str(self)) + @property + def matched(self) -> bool: + """ + Whether or not the server has been matched. + + This is `True` if the server has been matched, and `False` otherwise. + """ + if not self._handle: + msg = "The server is not running." + raise RuntimeError(msg) + return pact.v3.ffi.mock_server_matched(self._handle) + + @property + def mismatches(self) -> list[dict[str, Any]]: + """ + Mismatches between the Pact and the server. + + This is a string containing the mismatches between the Pact and the + server. If there are no mismatches, then this is an empty string. + """ + if not self._handle: + msg = "The server is not running." + raise RuntimeError(msg) + return pact.v3.ffi.mock_server_mismatches(self._handle) + def __str__(self) -> str: """ URL for the server. @@ -1357,11 +1412,18 @@ def __exit__( ) -> None: """ Stop the server. + + Raises: + MismatchesError: + If the server has not been fully matched and the server is + configured to raise an exception. """ if self._handle: + if self._raises and not self.matched: + raise MismatchesError(self.mismatches) self._handle = None - def __truediv__(self, other: str) -> URL: + def __truediv__(self, other: str | object) -> URL: """ URL for the server. """ diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py index fa5d6cbe0..6abc85456 100644 --- a/tests/v3/test_http_interaction.py +++ b/tests/v3/test_http_interaction.py @@ -60,12 +60,16 @@ async def test_basic_request_method(pact: Pact, method: str) -> None: .with_request(method, "/") .will_respond_with(200) ) - with pact.serve() as srv: + with pact.serve(raises=False) as srv: async with aiohttp.ClientSession(srv.url) as session: for m in ALL_HTTP_METHODS: async with session.request(m, "/") as resp: assert resp.status == (200 if m == method else 500) + # As we are making unexpected requests, we should have mismatches + for mismatch in srv.mismatches: + assert mismatch["type"] == "request-not-found" + @pytest.mark.parametrize( "status", @@ -192,10 +196,16 @@ async def test_set_header_request_repeat( .set_headers(headers) .will_respond_with(200) ) - with pact.serve() as srv: - async with aiohttp.ClientSession(srv.url) as session: - async with session.request("GET", "/", headers=headers) as resp: - assert resp.status == 500 + with pact.serve(raises=False) as srv: + async with aiohttp.ClientSession(srv.url) as session, session.request( + "GET", + "/", + headers=headers, + ) as resp: + assert resp.status == 500 + + assert len(srv.mismatches) == 1 + assert srv.mismatches[0]["type"] == "request-mismatch" @pytest.mark.parametrize( From 0be4d15a1c586cf6adf2c4c2667866e985b269bf Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 6 Nov 2023 17:58:05 +1100 Subject: [PATCH 0108/1376] chore(tests): add compatibility suite as submodule I have debated whether to include a submodule or a subtree. Given that we purely want to use the compatibility suite as a reference and not modify it, I thought it best to used a submodule. A subtree provides better compatibility when the files need to subsequently be changed, but has the downside of being more difficult to update. Signed-off-by: JP-Ellis --- .gitmodules | 3 +++ tests/v3/compatibility-suite/definition | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 tests/v3/compatibility-suite/definition diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..0242118f4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "compatibility-suite"] + path = tests/v3/compatibility-suite/definition + url = ../pact-compatibility-suite.git diff --git a/tests/v3/compatibility-suite/definition b/tests/v3/compatibility-suite/definition new file mode 160000 index 000000000..d22d4667c --- /dev/null +++ b/tests/v3/compatibility-suite/definition @@ -0,0 +1 @@ +Subproject commit d22d4667c0bda76d408676044cb33db834e7167e From 7ef6904e4574c27b9faa2c1b466df5e18b42cf47 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 3 Nov 2023 12:47:04 +1100 Subject: [PATCH 0109/1376] feat(v3): implement server log methods Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 18 ++++++++++++------ pact/v3/pact.py | 18 ++++++++++++++++++ tests/v3/test_pact.py | 5 +++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 5beacd231..448ee5cf7 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -4699,21 +4699,27 @@ def write_pact_file( raise RuntimeError(msg) -def mock_server_logs(mock_server_port: int) -> str: +def mock_server_logs(mock_server_handle: PactServerHandle) -> str: """ Fetch the logs for the mock server. This needs the memory buffer log sink to be setup before the mock server is - started. Returned string will be freed with the `cleanup_mock_server` - function call. + started. [Rust `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mock_server_logs) - Will return a NULL pointer if the logs for the mock server can not be - retrieved. + Raises: + RuntimeError: If the logs for the mock server can not be retrieved. """ - raise NotImplementedError + ptr = lib.pactffi_mock_server_logs(mock_server_handle._ref) + if ptr == ffi.NULL: + msg = f"Unable to obtain logs from {mock_server_handle!r}" + raise RuntimeError(msg) + string = ffi.string(ptr) + if isinstance(string, bytes): + string = string.decode("utf-8") + return string def generate_datetime_string(format: str) -> StringResult: diff --git a/pact/v3/pact.py b/pact/v3/pact.py index ac0325c13..78bfb3511 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -1365,6 +1365,24 @@ def mismatches(self) -> list[dict[str, Any]]: raise RuntimeError(msg) return pact.v3.ffi.mock_server_mismatches(self._handle) + @property + def logs(self) -> str | None: + """ + Logs from the server. + + This is a string containing the logs from the server. If there are no + logs, then this is `None`. For this to be populated, the logging must + be configured to make use of the internal buffer. + """ + if not self._handle: + msg = "The server is not running." + raise RuntimeError(msg) + + try: + return pact.v3.ffi.mock_server_logs(self._handle) + except RuntimeError: + return None + def __str__(self) -> str: """ URL for the server. diff --git a/tests/v3/test_pact.py b/tests/v3/test_pact.py index 64fd520a6..e919e760a 100644 --- a/tests/v3/test_pact.py +++ b/tests/v3/test_pact.py @@ -130,3 +130,8 @@ def test_write_file(pact: Pact, temp_dir: Path) -> None: ) def test_specification(pact: Pact, version: str) -> None: pact.with_specification(version) + + +def test_server_log(pact: Pact) -> None: + with pact.serve() as srv: + assert srv.logs is not None From 88d6e128b78efbddd6c199496c2729dff431a838 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 10 Nov 2023 15:06:34 +1100 Subject: [PATCH 0110/1376] chore(ruff): disable TD002 TD002 is a rule pertaining to TODO items and enforces the assignment of an actor/owner for the task. As this is an open source project, I don't think this is particularly relevant, and it should hopefully suffice to have the URL of the related issue. Signed-off-by: JP-Ellis --- pact/v3/pact.py | 2 +- pyproject.toml | 1 + tests/v3/test_http_interaction.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pact/v3/pact.py b/pact/v3/pact.py index 78bfb3511..b6ed86f3b 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -1177,7 +1177,7 @@ def interactions( The kind is used to specify the type of interactions that will be iterated over. """ - # TODO(JP-Ellis): Add an iterator for `All` interactions. + # TODO: Add an iterator for `All` interactions. # https://github.com/pact-foundation/pact-python/issues/451 if kind == "HTTP": return pact.v3.ffi.pact_handle_get_sync_http_iter(self._handle) diff --git a/pyproject.toml b/pyproject.toml index b42a579f5..5bb4ac982 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -176,6 +176,7 @@ ignore = [ "ANN101", # `self` must be typed "ANN102", # `cls` must be typed "FIX002", # Forbid TODO in comments + "TD002", # Assign someone to 'TODO' comments ] # TODO: Remove the explicity extend-exclude once astral-sh/ruff#6262 is fixed. diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py index 6abc85456..51589f985 100644 --- a/tests/v3/test_http_interaction.py +++ b/tests/v3/test_http_interaction.py @@ -490,13 +490,13 @@ async def test_multipart_file_request(pact: Pact, temp_dir: Path) -> None: with pact.serve() as srv, aiohttp.MultipartWriter() as mpwriter: mpwriter.append( fpy.open("rb"), - # TODO(JP-Ellis): Remove type ignore once aio-libs/aiohttp#7741 is resolved + # TODO: Remove type ignore once aio-libs/aiohttp#7741 is resolved # https://github.com/pact-foundation/pact-python/issues/450 {"Content-Type": "text/x-python"}, # type: ignore[arg-type] ) mpwriter.append( fpng.open("rb"), - # TODO(JP-Ellis): Remove type ignore once aio-libs/aiohttp#7741 is resolved + # TODO: Remove type ignore once aio-libs/aiohttp#7741 is resolved # https://github.com/pact-foundation/pact-python/issues/450 {"Content-Type": "image/png"}, # type: ignore[arg-type] ) From 7db2bfdb19f4b276a3c090e81fef414432d78e4a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 10 Nov 2023 15:13:43 +1100 Subject: [PATCH 0111/1376] chore: allow None content type In order to more closely match the behaviour of the FFI, where the FFI allows a `NULL` pointer, the Python wrapper allows a `None` to be passed. This is change is also reflect in the methods of the user-facing Pact class. Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 22 +++++++++------------- pact/v3/pact.py | 8 ++++---- tests/v3/test_http_interaction.py | 2 +- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 448ee5cf7..3392977d9 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -4558,11 +4558,7 @@ def create_mock_server_for_transport( addr.encode("utf-8"), port, transport.encode("utf-8"), - ( - transport_config.encode("utf-8") - if transport_config is not None - else ffi.NULL - ), + (transport_config.encode("utf-8") if transport_config else ffi.NULL), ) if ret > 0: return PactServerHandle(ret) @@ -5459,7 +5455,7 @@ def response_status(interaction: InteractionHandle, status: int) -> None: def with_body( interaction: InteractionHandle, part: InteractionPart, - content_type: str, + content_type: str | None, body: str | None, ) -> None: """ @@ -5495,8 +5491,8 @@ def with_body( success: bool = lib.pactffi_with_body( interaction._ref, part.value, - content_type.encode("utf-8"), - body.encode("utf-8") if body is not None else None, + content_type.encode("utf-8") if content_type else ffi.NULL, + body.encode("utf-8") if body else None, ) if not success: msg = f"Unable to set body for {interaction}." @@ -5506,7 +5502,7 @@ def with_body( def with_binary_file( interaction: InteractionHandle, part: InteractionPart, - content_type: str, + content_type: str | None, body: bytes | None, ) -> None: """ @@ -5549,7 +5545,7 @@ def with_binary_file( success: bool = lib.pactffi_with_binary_file( interaction._ref, part.value, - content_type.encode("utf-8"), + content_type.encode("utf-8") if content_type else ffi.NULL, body if body else ffi.NULL, len(body) if body else 0, ) @@ -5561,7 +5557,7 @@ def with_binary_file( def with_multipart_file_v2( # noqa: PLR0913 interaction: InteractionHandle, part: InteractionPart, - content_type: str, + content_type: str | None, file: Path | None, part_name: str, boundary: str | None, @@ -5604,7 +5600,7 @@ def with_multipart_file_v2( # noqa: PLR0913 lib.pactffi_with_multipart_file_v2( interaction._ref, part.value, - content_type.encode("utf-8"), + content_type.encode("utf-8") if content_type else ffi.NULL, str(file).encode("utf-8") if file else ffi.NULL, part_name.encode("utf-8"), boundary.encode("utf-8") if boundary else ffi.NULL, @@ -6642,7 +6638,7 @@ def using_plugin( ret: int = lib.pactffi_using_plugin( pact._ref, plugin_name.encode("utf-8"), - plugin_version.encode("utf-8") if plugin_version is not None else ffi.NULL, + plugin_version.encode("utf-8") if plugin_version else ffi.NULL, ) if ret == 0: return diff --git a/pact/v3/pact.py b/pact/v3/pact.py index b6ed86f3b..7fc6427d6 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -233,7 +233,7 @@ def given( def with_body( self, body: str | None = None, - content_type: str = "text/plain", + content_type: str | None = None, part: Literal["Request", "Response"] | None = None, ) -> Self: """ @@ -267,7 +267,7 @@ def with_body( def with_binary_file( self, body: bytes | None, - content_type: str = "application/octet-stream", + content_type: str | None = None, part: Literal["Request", "Response"] | None = None, ) -> Self: """ @@ -305,7 +305,7 @@ def with_multipart_file( # noqa: PLR0913 self, part_name: str, path: Path | None, - content_type: str = "application/octet-stream", + content_type: str | None = None, part: Literal["Request", "Response"] | None = None, boundary: str | None = None, ) -> Self: @@ -343,7 +343,7 @@ def test_name( def with_plugin_contents( self, contents: dict[str, Any] | str, - content_type: str = "text/plain", + content_type: str, part: Literal["Request", "Response"] | None = None, ) -> Self: """ diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py index 51589f985..2b545e29d 100644 --- a/tests/v3/test_http_interaction.py +++ b/tests/v3/test_http_interaction.py @@ -527,7 +527,7 @@ async def test_name(pact: Pact) -> None: async def test_with_plugin(pact: Pact) -> None: ( pact.upon_receiving("a basic request with a plugin") - .with_plugin_contents("{}") + .with_plugin_contents("{}", "application/json") .will_respond_with(200) ) with pact.serve() as srv: From d9c1e804604314ceda91a0e6a1413493a397ae28 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 10 Nov 2023 17:49:01 +1100 Subject: [PATCH 0112/1376] fix(v3): rename `with_binary_file` To help ensure consistency with the `with_body`, I have renamed the `with_binary_file` to `with_binary_body`, as the former implies that it expects a path to a file, as opposed to some byte array. Signed-off-by: JP-Ellis --- pact/v3/pact.py | 4 ++-- tests/v3/test_http_interaction.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pact/v3/pact.py b/pact/v3/pact.py index 7fc6427d6..4d8f7bc74 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -264,14 +264,14 @@ def with_body( ) return self - def with_binary_file( + def with_binary_body( self, body: bytes | None, content_type: str | None = None, part: Literal["Request", "Response"] | None = None, ) -> Self: """ - Adds a binary file to the request or response. + Adds a binary body to the request or response. Note that for HTTP interactions, this function will overwrite the body if it has been set using diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py index 2b545e29d..51a6134d0 100644 --- a/tests/v3/test_http_interaction.py +++ b/tests/v3/test_http_interaction.py @@ -435,7 +435,7 @@ async def test_binary_file_request(pact: Pact) -> None: ( pact.upon_receiving("a basic request with a binary file") .with_request("POST", "/") - .with_binary_file(payload, "application/octet-stream") + .with_binary_body(payload, "application/octet-stream") .will_respond_with(200) ) with pact.serve() as srv: @@ -456,7 +456,7 @@ async def test_binary_file_response(pact: Pact) -> None: pact.upon_receiving("a basic request with a binary file response") .with_request("GET", "/") .will_respond_with(200) - .with_binary_file(payload, "application/bytes") + .with_binary_body(payload, "application/bytes") ) with pact.serve() as srv: async with aiohttp.ClientSession(srv.url) as session: From aaf246dd6291cce6284992525d88d271fe5b02d7 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 3 Nov 2023 09:16:20 +1100 Subject: [PATCH 0113/1376] chore(tests): implement consumer v1 feature Implement the compability test suite as defined in the V1 consumer feature file from the Pact Compability Suite. Signed-off-by: JP-Ellis Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 2 +- pyproject.toml | 6 + .../compatibility-suite/test_v1_consumer.py | 922 ++++++++++++++++++ tests/v3/compatibility-suite/util.py | 301 ++++++ 4 files changed, 1230 insertions(+), 1 deletion(-) create mode 100644 tests/v3/compatibility-suite/test_v1_consumer.py create mode 100644 tests/v3/compatibility-suite/util.py diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 3392977d9..4d085aaca 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -4670,7 +4670,7 @@ def write_pact_file( """ ret: int = lib.pactffi_write_pact_file( mock_server_handle._ref, - directory, + str(directory).encode("utf-8"), overwrite, ) if ret == 0: diff --git a/pyproject.toml b/pyproject.toml index 5bb4ac982..004290dea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ test = [ "mock ~= 5.0", "pytest ~= 7.0", "pytest-asyncio ~= 0.0", + "pytest-bdd ~= 7.0", "pytest-cov ~= 4.0", "testcontainers ~= 3.0", ] @@ -149,6 +150,11 @@ filterwarnings = [ "ignore::PendingDeprecationWarning:tests", ] +markers = [ + # Markers for the compatibility suite + "consumer", +] + ################################################################################ ## Coverage ################################################################################ diff --git a/tests/v3/compatibility-suite/test_v1_consumer.py b/tests/v3/compatibility-suite/test_v1_consumer.py new file mode 100644 index 000000000..10c9989b6 --- /dev/null +++ b/tests/v3/compatibility-suite/test_v1_consumer.py @@ -0,0 +1,922 @@ +"""Basic HTTP consumer feature tests.""" + +from __future__ import annotations + +import json +import logging +import re +from typing import TYPE_CHECKING, Any, Generator + +import pytest +import requests +from pact.v3 import Pact +from pytest_bdd import given, parsers, scenario, then, when +from yarl import URL + +from .util import ( # type: ignore[import-untyped] + FIXTURES_ROOT, + InteractionDefinition, + string_to_int, + truncate, +) + +if TYPE_CHECKING: + from pathlib import Path + + from pact.v3.pact import PactServer + +logger = logging.getLogger(__name__) + +################################################################################ +## Scenario +################################################################################ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "When all requests are made to the mock server", +) +def test_when_all_requests_are_made_to_the_mock_server() -> None: + """ + When all requests are made to the mock server. + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "When not all requests are made to the mock server", +) +def test_when_not_all_requests_are_made_to_the_mock_server() -> None: + """ + When not all requests are made to the mock server. + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "When an unexpected request is made to the mock server", +) +def test_when_an_unexpected_request_is_made_to_the_mock_server() -> None: + """ + When an unexpected request is made to the mock server. + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with query parameters", +) +def test_request_with_query_parameters() -> None: + """ + Request with query parameters. + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with invalid query parameters", +) +def test_request_with_invalid_query_parameters() -> None: + """ + Request with invalid query parameters. + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with invalid path", +) +def test_request_with_invalid_path() -> None: + """ + Request with invalid path. + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with invalid method", +) +def test_request_with_invalid_method() -> None: + """ + Request with invalid method. + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with headers", +) +def test_request_with_headers() -> None: + """ + Request with headers. + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with invalid headers", +) +def test_request_with_invalid_headers() -> None: + """ + Request with invalid headers. + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with body", +) +def test_request_with_body() -> None: + """ + Request with body. + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with invalid body", +) +def test_request_with_invalid_body() -> None: + """ + Request with invalid body. + """ + + +# TODO: Enable this test when the upstream issue is resolved: +# https://github.com/pact-foundation/pact-compatibility-suite/issues/3 +@pytest.mark.skip("Waiting on upstream fix") +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with the incorrect type of body contents", +) +def test_request_with_the_incorrect_type_of_body_contents() -> None: + """ + Request with the incorrect type of body contents. + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with plain text body (positive case)", +) +def test_request_with_plain_text_body_positive_case() -> None: + """ + Request with plain text body (positive case). + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with plain text body (negative case)", +) +def test_request_with_plain_text_body_negative_case() -> None: + """ + Request with plain text body (negative case). + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with JSON body (positive case)", +) +def test_request_with_json_body_positive_case() -> None: + """ + Request with JSON body (positive case). + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with JSON body (negative case)", +) +def test_request_with_json_body_negative_case() -> None: + """ + Request with JSON body (negative case). + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with XML body (positive case)", +) +def test_request_with_xml_body_positive_case() -> None: + """ + Request with XML body (positive case). + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with XML body (negative case)", +) +def test_request_with_xml_body_negative_case() -> None: + """ + Request with XML body (negative case). + """ + + +# TODO: Enable this test when the upstream issue is resolved: +# https://github.com/pact-foundation/pact-reference/issues/336 +@pytest.mark.skip("Waiting on upstream fix") +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with a binary body (positive case)", +) +def test_request_with_a_binary_body_positive_case() -> None: + """ + Request with a binary body (positive case). + """ + + +# TODO: Enable this test when the upstream issue is resolved: +# https://github.com/pact-foundation/pact-reference/issues/336 +@pytest.mark.skip("Waiting on upstream fix") +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with a binary body (negative case)", +) +def test_request_with_a_binary_body_negative_case() -> None: + """ + Request with a binary body (negative case). + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with a form post body (positive case)", +) +def test_request_with_a_form_post_body_positive_case() -> None: + """ + Request with a form post body (positive case). + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with a form post body (negative case)", +) +def test_request_with_a_form_post_body_negative_case() -> None: + """ + Request with a form post body (negative case). + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with a multipart body (positive case)", +) +def test_request_with_a_multipart_body_positive_case() -> None: + """ + Request with a multipart body (positive case). + """ + + +@scenario( + "definition/features/V1/http_consumer.feature", + "Request with a multipart body (negative case)", +) +def test_request_with_a_multipart_body_negative_case() -> None: + """ + Request with a multipart body (negative case). + """ + + +################################################################################ +## Given +################################################################################ + + +@given( + parsers.parse("the following HTTP interactions have been defined:\n{content}"), + target_fixture="interaction_definitions", +) +def the_following_http_interactions_have_been_defined( + content: str, +) -> dict[int, InteractionDefinition]: + """ + Parse the HTTP interactions table into a dictionary. + + The table columns are expected to be: + + - No + - method + - path + - query + - headers + - body + - response + - response content + - response body + + The first row is ignored, as it is assumed to be the column headers. The + order of the columns is similarly ignored. + """ + rows = [ + list(map(str.strip, row.split("|")))[1:-1] + for row in content.split("\n") + if row.strip() + ] + + # Check that the table is well-formed + assert len(rows[0]) == 9 + assert rows[0][0] == "No" + + # Parse the table into a more useful format + interactions: dict[int, InteractionDefinition] = {} + for row in rows[1:]: + interactions[int(row[0])] = InteractionDefinition(**dict(zip(rows[0], row))) + return interactions + + +################################################################################ +## When +################################################################################ + + +@when( + parsers.re( + r"the mock server is started" + r" with interactions?" + r' "?(?P((\d+)(,\s)?)+)"?', + ), + converters={"ids": lambda s: list(map(int, s.split(",")))}, + target_fixture="srv", +) +def the_mock_server_is_started_with_interactions( + ids: list[int], + interaction_definitions: dict[int, InteractionDefinition], +) -> Generator[PactServer, Any, None]: + """The mock server is started with interactions.""" + pact = Pact("consumer", "provider") + pact.with_specification("V1") + for iid in ids: + definition = interaction_definitions[iid] + logging.info("Adding interaction %s", iid) + + interaction = pact.upon_receiving(f"interactions {iid}") + logging.info("-> with_request(%s, %s)", definition.method, definition.path) + interaction.with_request(definition.method, definition.path) + + if definition.query: + query = URL.build(query_string=definition.query).query + logging.info("-> with_query_parameters(%s)", query.items()) + interaction.with_query_parameters(query.items()) + + if definition.headers: + logging.info("-> with_headers(%s)", definition.headers.items()) + interaction.with_headers(definition.headers.items()) + + if definition.body: + if definition.body.string: + logging.info( + "-> with_body(%s, %s)", + truncate(definition.body.string), + definition.body.mime_type, + ) + interaction.with_body( + definition.body.string, + definition.body.mime_type, + ) + else: + logging.info( + "-> with_binary_file(%s, %s)", + truncate(definition.body.bytes), + definition.body.mime_type, + ) + interaction.with_binary_body( + definition.body.bytes, + definition.body.mime_type, + ) + + logging.info("-> will_respond_with(%s)", definition.response) + interaction.will_respond_with(definition.response) + + if definition.response_content: + if definition.response_body is None: + msg = "Expected response body along with response content type" + raise ValueError(msg) + + if definition.response_body.string: + logging.info( + "-> with_body(%s, %s)", + truncate(definition.response_body.string), + definition.response_content, + ) + interaction.with_body( + definition.response_body.string, + definition.response_content, + ) + else: + logging.info( + "-> with_binary_file(%s, %s)", + truncate(definition.response_body.bytes), + definition.response_content, + ) + interaction.with_binary_body( + definition.response_body.bytes, + definition.response_content, + ) + + with pact.serve(raises=False) as srv: + yield srv + + +@when( + parsers.re( + r"request (?P\d+) is made to the mock server", + ), + converters={"request_id": int}, + target_fixture="response", +) +def request_n_is_made_to_the_mock_server( + interaction_definitions: dict[int, InteractionDefinition], + request_id: int, + srv: PactServer, +) -> requests.Response: + """ + Request n is made to the mock server. + """ + definition = interaction_definitions[request_id] + if ( + definition.body + and definition.body.mime_type + and "Content-Type" not in definition.headers + ): + definition.headers.add("Content-Type", definition.body.mime_type) + + return requests.request( + definition.method, + str(srv.url.with_path(definition.path)), + params=URL.build(query_string=definition.query).query + if definition.query + else None, + headers=definition.headers if definition.headers else None, # type: ignore[arg-type] + data=definition.body.bytes if definition.body else None, + ) + + +@when( + parsers.re( + r"request (?P\d+) is made to the mock server" + r" with the following changes?:\n(?P.*)", + re.DOTALL, + ), + converters={"request_id": int}, + target_fixture="response", +) +def request_n_is_made_to_the_mock_server_with_the_following_changes( + interaction_definitions: dict[int, InteractionDefinition], + request_id: int, + content: str, + srv: PactServer, +) -> requests.Response: + """ + Request n is made to the mock server with changes. + + The content is a markdown table with a subset of the columns defining the + definition (as in the given step). + """ + definition = interaction_definitions[request_id] + rows = [ + list(map(str.strip, row.split("|")))[1:-1] + for row in content.split("\n") + if row.strip() + ] + assert len(rows) == 2, "Expected two rows in the table" + updates = dict(zip(rows[0], rows[1])) + definition.update(**updates) + + if ( + definition.body + and definition.body.mime_type + and "Content-Type" not in definition.headers + ): + definition.headers.add("Content-Type", definition.body.mime_type) + + return requests.request( + definition.method, + str(srv.url.with_path(definition.path)), + params=URL.build(query_string=definition.query).query + if definition.query + else None, + headers=definition.headers if definition.headers else None, # type: ignore[arg-type] + data=definition.body.bytes if definition.body else None, + ) + + +################################################################################ +## Then +################################################################################ + + +@then( + parsers.re( + r"a (?P\d+) (success|error) response is returned", + ), + converters={"code": int}, +) +def a_response_is_returned( + response: requests.Response, + code: int, + srv: PactServer, +) -> None: + """ + A response is returned. + """ + logging.info("Request Information:") + logging.info("-> Method: %s", response.request.method) + logging.info("-> URL: %s", response.request.url) + logging.info( + "-> Headers: %s", + json.dumps( + dict(**response.request.headers), + indent=2, + ), + ) + logging.info( + "-> Body: %s", + truncate(response.request.body) if response.request.body else None, + ) + logging.info("Mismatches:\n%s", json.dumps(srv.mismatches, indent=2)) + assert response.status_code == code + + +@then( + parsers.re( + r'the payload will contain the "(?P[^"]+)" JSON document', + ), +) +def the_payload_will_contain_the_json_document( + response: requests.Response, + file: str, +) -> None: + """ + The payload will contain the JSON document. + """ + path = FIXTURES_ROOT / f"{file}.json" + assert response.json() == json.loads(path.read_text()) + + +@then( + parsers.re( + r'the content type will be set as "(?P[^"]+)"', + ), +) +def the_content_type_will_be_set_as( + response: requests.Response, + content_type: str, +) -> None: + assert response.headers["Content-Type"] == content_type + + +@when("the pact test is done") +def the_pact_test_is_done() -> None: + """ + The pact test is done. + """ + + +@then( + parsers.re(r"the mock server status will (?P(NOT )?)be OK"), + converters={"negated": lambda s: s == "NOT "}, +) +def the_mock_server_status_will_be( + srv: PactServer, + negated: bool, # noqa: FBT001 +) -> None: + """ + The mock server status will be. + """ + assert srv.matched is not negated + + +@then( + parsers.re( + r"the mock server status will be" + r" an expected but not received error" + r" for interaction \{(?P\d+)\}", + ), + converters={"n": int}, +) +def the_mock_server_status_will_be_an_expected_but_not_received_error_for_interaction_n( + srv: PactServer, + n: int, + interaction_definitions: dict[int, InteractionDefinition], +) -> None: + """ + The mock server status will be an expected but not received error for interaction n. + """ + assert srv.matched is False + assert len(srv.mismatches) > 0 + + for mismatch in srv.mismatches: + if ( + mismatch["method"] == interaction_definitions[n].method + and mismatch["path"] == interaction_definitions[n].path + and mismatch["type"] == "missing-request" + ): + return + pytest.fail("Expected mismatch not found") + + +@then( + parsers.re( + r"the mock server status will be" + r' an unexpected "(?P[^"]+)" request received error' + r" for interaction \{(?P\d+)\}", + ), + converters={"n": int}, +) +def the_mock_server_status_will_be_an_unexpected_request_received_for_interaction_n( + srv: PactServer, + method: str, + n: int, + interaction_definitions: dict[int, InteractionDefinition], +) -> None: + """ + The mock server status will be an expected but not received error for interaction n. + """ + assert srv.matched is False + assert len(srv.mismatches) > 0 + + for mismatch in srv.mismatches: + if ( + mismatch["method"] == interaction_definitions[n].method + and mismatch["request"]["method"] == method + and mismatch["path"] == interaction_definitions[n].path + and mismatch["type"] == "request-not-found" + ): + return + pytest.fail("Expected mismatch not found") + + +@then( + parsers.re( + r"the mock server status will be" + r' an unexpected "(?P[^"]+)" request received error' + r' for path "(?P[^"]+)"', + ), + converters={"n": int}, +) +def the_mock_server_status_will_be_an_unexpected_request_received_for_path( + srv: PactServer, + method: str, + path: str, +) -> None: + """ + The mock server status will be an expected but not received error for interaction n. + """ + assert srv.matched is False + assert len(srv.mismatches) > 0 + + for mismatch in srv.mismatches: + if ( + mismatch["request"]["method"] == method + and mismatch["path"] == path + and mismatch["type"] == "request-not-found" + ): + return + pytest.fail("Expected mismatch not found") + + +@then("the mock server status will be mismatches") +def the_mock_server_status_will_be_mismatches( + srv: PactServer, +) -> None: + """ + The mock server status will be mismatches. + """ + assert srv.matched is False + assert len(srv.mismatches) > 0 + + +@then( + parsers.re( + r'the mismatches will contain a "(?P[^"]+)" mismatch' + r' with error "(?P[^"]+)"', + ), +) +def the_mismatches_will_contain_a_mismatch_with_the_error( + srv: PactServer, + mismatch_type: str, + error: str, +) -> None: + """ + The mismatches will contain a mismatch with the error. + """ + if mismatch_type == "query": + mismatch_type = "QueryMismatch" + elif mismatch_type == "header": + mismatch_type = "HeaderMismatch" + elif mismatch_type == "body": + mismatch_type = "BodyMismatch" + elif mismatch_type == "body-content-type": + mismatch_type = "BodyTypeMismatch" + else: + msg = f"Unexpected mismatch type: {mismatch_type}" + raise ValueError(msg) + + logger.info("Expecting mismatch: %s", mismatch_type) + logger.info("With error: %s", error) + for mismatch in srv.mismatches: + for sub_mismatch in mismatch["mismatches"]: + if ( + error in sub_mismatch["mismatch"] + and sub_mismatch["type"] == mismatch_type + ): + return + pytest.fail("Expected mismatch not found") + + +@then( + parsers.re( + r'the mismatches will contain a "(?P[^"]+)" mismatch' + r' with path "(?P[^"]+)"' + r' with error "(?P[^"]+)"', + ), +) +def the_mismatches_will_contain_a_mismatch_with_path_with_the_error( + srv: PactServer, + mismatch_type: str, + path: str, + error: str, +) -> None: + """ + The mismatches will contain a mismatch with the error. + """ + mismatch_type = "BodyMismatch" if mismatch_type == "body" else mismatch_type + for mismatch in srv.mismatches: + for sub_mismatch in mismatch["mismatches"]: + if ( + sub_mismatch["mismatch"] == error + and sub_mismatch["type"] == mismatch_type + and sub_mismatch["path"] == path + ): + return + pytest.fail("Expected mismatch not found") + + +@then( + parsers.re( + r"the mock server will (?P(NOT )?)write out" + r" a Pact file for the interactions? when done", + ), + converters={"negated": lambda s: s == "NOT "}, + target_fixture="pact_file", +) +def the_mock_server_will_write_out_a_pact_file_for_the_interaction_when_done( + srv: PactServer, + temp_dir: Path, + negated: bool, # noqa: FBT001 +) -> dict[str, Any] | None: + """ + The mock server will write out a Pact file for the interaction when done. + """ + if not negated: + srv.write_file(temp_dir) + output = temp_dir / "consumer-provider.json" + assert output.is_file() + return json.load(output.open()) + return None + + +@then( + parsers.re(r"the pact file will contain \{(?P\d+)\} interactions?"), + converters={"n": int}, +) +def the_pact_file_will_contain_n_interactions( + pact_file: dict[str, Any], + n: int, +) -> None: + """ + The pact file will contain n interactions. + """ + assert len(pact_file["interactions"]) == n + + +@then( + parsers.re( + r"the \{(?P\w+)\} interaction response" + r' will contain the "(?P[^"]+)" document', + ), + converters={"n": string_to_int}, +) +def the_nth_interaction_will_contain_the_document( + pact_file: dict[str, Any], + n: int, + file: str, +) -> None: + """ + The nth interaction response will contain the document. + """ + file_path = FIXTURES_ROOT / file + if file.endswith(".json"): + assert pact_file["interactions"][n - 1]["response"]["body"] == json.load( + file_path.open(), + ) + + +@then( + parsers.re( + r'the \{(?P\w+)\} interaction request will be for a "(?P[A-Z]+)"', + ), + converters={"n": string_to_int}, +) +def the_nth_interaction_request_will_be_for_method( + pact_file: dict[str, Any], + n: int, + method: str, +) -> None: + """ + The nth interaction request will be for a method. + """ + assert pact_file["interactions"][n - 1]["request"]["method"] == method + + +@then( + parsers.re( + r"the \{(?P\w+)\} interaction request" + r' query parameters will be "(?P[^"]+)"', + ), + converters={"n": string_to_int}, +) +def the_nth_interaction_request_query_parameters_will_be( + pact_file: dict[str, Any], + n: int, + query: str, +) -> None: + """ + The nth interaction request query parameters will be. + """ + assert query == pact_file["interactions"][n - 1]["request"]["query"] + + +@then( + parsers.re( + r"the \{(?P\w+)\} interaction request" + r' will contain the header "(?P[^"]+)"' + r' with value "(?P[^"]+)"', + ), + converters={"n": string_to_int}, +) +def the_nth_interaction_request_will_contain_the_header( + pact_file: dict[str, Any], + n: int, + key: str, + value: str, +) -> None: + """ + The nth interaction request will contain the header. + """ + expected = {key: value} + actual = pact_file["interactions"][n - 1]["request"]["headers"] + assert expected.keys() == actual.keys() + for key in expected: + assert expected[key] == actual[key] or [expected[key]] == actual[key] + + +@then( + parsers.re( + r"the \{(?P\w+)\} interaction request" + r' content type will be "(?P[^"]+)"', + ), + converters={"n": string_to_int}, +) +def the_nth_interaction_request_content_type_will_be( + pact_file: dict[str, Any], + n: int, + content_type: str, +) -> None: + """ + The nth interaction request will contain the header. + """ + assert ( + pact_file["interactions"][n - 1]["request"]["headers"]["Content-Type"] + == content_type + ) + + +@then( + parsers.re( + r"the \{(?P\w+)\} interaction request" + r' will contain the "(?P[^"]+)" document', + ), + converters={"n": string_to_int}, +) +def the_nth_interaction_request_will_contain_the_document( + pact_file: dict[str, Any], + n: int, + file: str, +) -> None: + """ + The nth interaction request will contain the document. + """ + file_path = FIXTURES_ROOT / file + if file.endswith(".json"): + assert pact_file["interactions"][n - 1]["request"]["body"] == json.load( + file_path.open(), + ) + else: + assert ( + pact_file["interactions"][n - 1]["request"]["body"] == file_path.read_text() + ) diff --git a/tests/v3/compatibility-suite/util.py b/tests/v3/compatibility-suite/util.py new file mode 100644 index 000000000..53be2902c --- /dev/null +++ b/tests/v3/compatibility-suite/util.py @@ -0,0 +1,301 @@ +""" +Utility functions to help with testing. +""" + +from __future__ import annotations + +import contextlib +import hashlib +import logging +import typing +from pathlib import Path +from xml.etree import ElementTree + +from multidict import MultiDict + +logger = logging.getLogger(__name__) +SUITE_ROOT = Path(__file__).parent / "definition" +FIXTURES_ROOT = SUITE_ROOT / "fixtures" + + +def string_to_int(word: str) -> int: + """ + Convert a word to an integer. + + The word can be a number, or a word representing a number. + + Args: + word: The word to convert. + + Returns: + The integer value of the word. + + Raises: + ValueError: If the word cannot be converted to an integer. + """ + try: + return int(word) + except ValueError: + pass + + try: + return { + "first": 1, + "second": 2, + "third": 3, + "fourth": 4, + "fifth": 5, + "sixth": 6, + "seventh": 7, + "eighth": 8, + "ninth": 9, + "1st": 1, + "2nd": 2, + "3rd": 3, + "4th": 4, + "5th": 5, + "6th": 6, + "7th": 7, + "8th": 8, + "9th": 9, + }[word] + except KeyError: + pass + + msg = f"Unable to convert {word!r} to an integer" + raise ValueError(msg) + + +def truncate(data: str | bytes) -> str: + """ + Truncate a large string or bytes object. + + This is useful for printing large strings or bytes objects in tests. + """ + if len(data) <= 32: + if isinstance(data, str): + return f"{data!r}" + return data.decode("utf-8", "backslashreplace") + + length = len(data) + if isinstance(data, str): + checksum = hashlib.sha256(data.encode()).hexdigest() + return ( + '"' + + data[:6] + + "⋯" + + data[-6:] + + '"' + + f" ({length} bytes, sha256={checksum[:7]})" + ) + + checksum = hashlib.sha256(data).hexdigest() + return ( + 'b"' + + data[:8].decode("utf-8", "backslashreplace") + + "⋯" + + data[-8:].decode("utf-8", "backslashreplace") + + '"' + + f" ({length} bytes, sha256={checksum[:7]})" + ) + + +class InteractionDefinition: + """ + Interaction definition. + + This is a dictionary that represents a single interaction. It is used to + parse the HTTP interactions table into a more useful format. + """ + + class Body: + """ + Interaction body. + + The interaction body can be one of: + + - A file + - An arbitrary string + - A JSON document + - An XML document + """ + + def __init__(self, data: str) -> None: + """ + Instantiate the interaction body. + """ + self.string: str | None = None + self.bytes: bytes | None = None + self.mime_type: str | None = None + + if data.startswith("file: ") and data.endswith("-body.xml"): + self.parse_fixture(FIXTURES_ROOT / data[6:]) + return + + if data.startswith("file: "): + self.parse_file(FIXTURES_ROOT / data[6:]) + return + + if data.startswith("JSON: "): + self.string = data[6:] + self.bytes = self.string.encode("utf-8") + self.mime_type = "application/json" + return + + if data.startswith("XML: "): + self.string = data[5:] + self.bytes = self.string.encode("utf-8") + self.mime_type = "application/xml" + return + + self.bytes = data.encode("utf-8") + self.string = data + self.mime_type = "text/plain" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return "".format( + ", ".join(truncate(f"{k}={v!r}") for k, v in vars(self).items()), + ) + + def parse_fixture(self, fixture: Path) -> None: + """ + Parse a fixture file. + + This is used to parse the fixture files that contain additional + metadata about the body (such as the content type). + """ + etree = ElementTree.parse(fixture) # noqa: S314 + root = etree.getroot() + if not root or root.tag != "body": + msg = "Invalid XML fixture document" + raise ValueError(msg) + + contents = root.find("contents") + content_type = root.find("contentType") + if contents is None: + msg = "Invalid XML fixture document: no contents" + raise ValueError(msg) + if content_type is None: + msg = "Invalid XML fixture document: no contentType" + raise ValueError(msg) + self.string = typing.cast(str, contents.text) + + if eol := contents.attrib.get("eol", None): + if eol == "CRLF": + self.string = self.string.replace("\r\n", "\n") + self.string = self.string.replace("\n", "\r\n") + elif eol == "LF": + self.string = self.string.replace("\r\n", "\n") + + self.bytes = self.string.encode("utf-8") + self.mime_type = content_type.text + + def parse_file(self, file: Path) -> None: + """ + Load the contents of a file. + + The mime type is inferred from the file extension, and the contents + are loaded as a byte array, and optionally as a string. + """ + self.bytes = file.read_bytes() + with contextlib.suppress(UnicodeDecodeError): + self.string = file.read_text() + + if file.suffix == ".xml": + self.mime_type = "application/xml" + elif file.suffix == ".json": + self.mime_type = "application/json" + elif file.suffix == ".jpg": + self.mime_type = "image/jpeg" + elif file.suffix == ".pdf": + self.mime_type = "application/pdf" + else: + msg = "Unknown file type" + raise ValueError(msg) + + def __init__(self, **kwargs: str) -> None: + """Initialise the interaction definition.""" + self.id: int | None = None + self.method: str = kwargs.pop("method") + self.path: str = kwargs.pop("path") + self.response: int = int(kwargs.pop("response")) + self.query: str | None = None + self.headers: MultiDict[str] = MultiDict() + self.body: InteractionDefinition.Body | None = None + self.response_content: str | None = None + self.response_body: InteractionDefinition.Body | None = None + self.update(**kwargs) + + def update(self, **kwargs: str) -> None: # noqa: C901 + """ + Update the interaction definition. + + This is a convenience method that allows the interaction definition to + be updated with new values. + """ + if interaction_id := kwargs.pop("No", None): + self.id = int(interaction_id) + if method := kwargs.pop("method", None): + self.method = method + if path := kwargs.pop("path", None): + self.path = path + if query := kwargs.pop("query", None): + self.query = query + if headers := kwargs.pop("headers", None): + self.headers = InteractionDefinition.parse_headers(headers) + if body := kwargs.pop("body", None): + # When updating the body, we _only_ update the body content, not + # the content type. + orig_content_type = self.body.mime_type if self.body else None + self.body = InteractionDefinition.Body(body) + self.body.mime_type = orig_content_type or self.body.mime_type + if content_type := ( + kwargs.pop("content_type", None) or kwargs.pop("content type", None) + ): + if self.body is None: + self.body = InteractionDefinition.Body("") + self.body.mime_type = content_type + if response := kwargs.pop("response", None): + self.response = int(response) + if response_content := ( + kwargs.pop("response_content", None) or kwargs.pop("response content", None) + ): + self.response_content = response_content + if response_body := ( + kwargs.pop("response_body", None) or kwargs.pop("response body", None) + ): + self.response_body = InteractionDefinition.Body(response_body) + + if len(kwargs) > 0: + msg = f"Unexpected arguments: {kwargs.keys()}" + raise TypeError(msg) + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return "".format( + ", ".join(f"{k}={v!r}" for k, v in vars(self).items()), + ) + + @staticmethod + def parse_headers(headers: str) -> MultiDict[str]: + """ + Parse the headers. + + The headers are in the format: + + ```text + 'X-A: 1', 'X-B: 2', 'X-A: 3' + ``` + + As headers can be repeated, the result is a MultiDict. + """ + kvs: list[tuple[str, str]] = [] + for header in headers.split(", "): + k, v = header.strip("'").split(": ") + kvs.append((k, v)) + return MultiDict(kvs) From f3d940da3965b4e7b69c9c34080e8039d7ce1cc1 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 13 Nov 2023 07:53:53 +1100 Subject: [PATCH 0114/1376] fix(v3): incorrect arg order A recent change to the `content_type` type uncovered an accidental error in the order of the arguments. Signed-off-by: JP-Ellis --- pact/v3/pact.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pact/v3/pact.py b/pact/v3/pact.py index 4d8f7bc74..7ef64e5b3 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -317,9 +317,9 @@ def with_multipart_file( # noqa: PLR0913 pact.v3.ffi.with_multipart_file_v2( self._handle, self._parse_interaction_part(part), - part_name, - path, content_type, + path, + part_name, boundary, ) return self From b6ecf35b329bf7147c63f14c27f11397aecaf0ee Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 13 Nov 2023 09:30:35 +1100 Subject: [PATCH 0115/1376] chore(ci): checkout submodules As I have opted to use a submodule for the compatibility suite, it needs to be checked out explicitly when running tests. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6785a55d..e63ff5e85 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,6 +38,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: true - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 From 5f9a11f5eefe74e26fee9ef2592b374adeedc3bd Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 15 Nov 2023 16:11:58 +1100 Subject: [PATCH 0116/1376] chore(ci): fix examples testing There are two hatch scripts which call pytest, `hatch run test` and `hatch run example`. The scripts allow for additional arguments which get passed along to `pytest` with an optional default. This was misconfigured for the examples: \## Before `hatch run examples` becomes `pytest run examples/` `hatch run examples --broker-url=...` becomes `pytest run --broker-url=...` \## Now `hatch run examples` becomes `pytest run examples/` `hatch run examples --broker-url=...` becomes `pytest run examples/ --broker-url=...` Signed-off-by: JP-Ellis --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 004290dea..b65562869 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,7 @@ extra-dependencies = ["hatchling", "packaging", "requests", "cffi"] [tool.hatch.envs.default.scripts] lint = ["black --check --diff {args:.}", "ruff {args:.}", "mypy {args:.}"] test = "pytest {args:tests/}" -example = "pytest {args:examples/}" +example = "pytest examples/ {args}" all = ["lint", "test", "example"] # Test environment for running unit tests. This automatically tests against all @@ -129,7 +129,7 @@ python = ["3.8", "3.9", "3.10", "3.11"] [tool.hatch.envs.test.scripts] test = "pytest {args:tests/}" -example = "pytest {args:examples/}" +example = "pytest examples/ {args}" all = ["test", "example"] ################################################################################ From 4aa33deec1025a480d28ba81c133308402fb2e52 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 15 Nov 2023 16:30:17 +1100 Subject: [PATCH 0117/1376] chore(ci): clone submodules in Cirrus Signed-off-by: JP-Ellis --- .cirrus.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.cirrus.yml b/.cirrus.yml index a52b4f0d5..f6ba0c61e 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -11,6 +11,7 @@ TEST_TEMPLATE: &TEST_TEMPLATE linux_arm64_task: env: PATH: ${HOME}/.local/bin:${PATH} + CIRRUS_CLONE_SUBMODULES: "true" matrix: - IMAGE: "python:3.8-slim" - IMAGE: "python:3.9-slim" @@ -30,6 +31,7 @@ macosx_arm64_task: image: ghcr.io/cirruslabs/macos-ventura-base:latest env: PATH: ${HOME}/.local/bin:${HOME}/.pyenv/shims:${PATH} + CIRRUS_CLONE_SUBMODULES: "true" matrix: - PYTHON: "3.8" - PYTHON: "3.9" From edd6856e7f6056370646a93b6f52241be44d6b87 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 15 Nov 2023 16:42:45 +1100 Subject: [PATCH 0118/1376] docs: add git submodule init With the addition of a git submodule for the compatibility suite, the contributing docs needed to be updated to ensure the additional step is documented. Signed-off-by: JP-Ellis --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b6ca493c..5f1de75e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,7 +99,7 @@ You can also try using the new [github.dev](https://github.dev/pact-foundation/p pipx install hatch ``` -3. After cloning the repository, run `hatch shell` in the root of the repository. This will install all dependencies in a Python virtual environment and then ensure that the virtual environment is activated. +3. After cloning the repository, run `hatch shell` in the root of the repository. This will install all dependencies in a Python virtual environment and then ensure that the virtual environment is activated. You will also need to run `git submodule init` if you want to run tests, as Pact Python makes use of the Pact Compability Suite. 4. To run tests, run `hatch run test` to make sure the test suite is working. You should also make sure the example works by running `hatch run example`. For the examples, you will have to make sure that you have Docker (or a suitable alternative) installed and running. From 1763eb3426d5b4f4f0d8e4bef950a843292a1690 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 15 Nov 2023 16:43:32 +1100 Subject: [PATCH 0119/1376] chore(tests): automatic submodule init In order to reduce the burden for new contributors, I have added a pytest fixture which checks that the submodule has been created. If it is not, it will try and run `git submodule init`. Signed-off-by: JP-Ellis --- tests/v3/compatibility-suite/__init__.py | 3 +++ tests/v3/compatibility-suite/conftest.py | 30 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 tests/v3/compatibility-suite/__init__.py create mode 100644 tests/v3/compatibility-suite/conftest.py diff --git a/tests/v3/compatibility-suite/__init__.py b/tests/v3/compatibility-suite/__init__.py new file mode 100644 index 000000000..16228019e --- /dev/null +++ b/tests/v3/compatibility-suite/__init__.py @@ -0,0 +1,3 @@ +""" +Compatibility suite tests. +""" diff --git a/tests/v3/compatibility-suite/conftest.py b/tests/v3/compatibility-suite/conftest.py new file mode 100644 index 000000000..46d3d33cb --- /dev/null +++ b/tests/v3/compatibility-suite/conftest.py @@ -0,0 +1,30 @@ +""" +Pytest configuration. + +As the compatibility suite makes use of a submodule, we need to make sure the +submodule has been initialized before running the tests. +""" + +import shutil +import subprocess +from pathlib import Path + +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def _submodule_init() -> None: + """Initialize the submodule.""" + # Locate the git execute + submodule_dir = Path(__file__).parent / "definition" + if submodule_dir.is_dir(): + return + + git_exec = shutil.which("git") + if git_exec is None: + msg = ( + "Submodule not initialized and git executable not found." + " Please initialize the submodule with `git submodule init`." + ) + raise RuntimeError(msg) + subprocess.check_call([git_exec, "submodule", "init"]) # noqa: S603 From e49ced91903d6b4daec4de37b28f42b8a04fdb8f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 15 Nov 2023 17:12:02 +1100 Subject: [PATCH 0120/1376] chore: fix lints Signed-off-by: JP-Ellis --- .gitmodules | 2 +- .../__init__.py | 0 .../conftest.py | 0 .../definition | 0 .../test_v1_consumer.py | 12 +++++++++--- .../util.py | 0 6 files changed, 10 insertions(+), 4 deletions(-) rename tests/v3/{compatibility-suite => compatiblity_suite}/__init__.py (100%) rename tests/v3/{compatibility-suite => compatiblity_suite}/conftest.py (100%) rename tests/v3/{compatibility-suite => compatiblity_suite}/definition (100%) rename tests/v3/{compatibility-suite => compatiblity_suite}/test_v1_consumer.py (98%) rename tests/v3/{compatibility-suite => compatiblity_suite}/util.py (100%) diff --git a/.gitmodules b/.gitmodules index 0242118f4..fa1a87278 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "compatibility-suite"] - path = tests/v3/compatibility-suite/definition + path = tests/v3/compatiblity_suite/definition url = ../pact-compatibility-suite.git diff --git a/tests/v3/compatibility-suite/__init__.py b/tests/v3/compatiblity_suite/__init__.py similarity index 100% rename from tests/v3/compatibility-suite/__init__.py rename to tests/v3/compatiblity_suite/__init__.py diff --git a/tests/v3/compatibility-suite/conftest.py b/tests/v3/compatiblity_suite/conftest.py similarity index 100% rename from tests/v3/compatibility-suite/conftest.py rename to tests/v3/compatiblity_suite/conftest.py diff --git a/tests/v3/compatibility-suite/definition b/tests/v3/compatiblity_suite/definition similarity index 100% rename from tests/v3/compatibility-suite/definition rename to tests/v3/compatiblity_suite/definition diff --git a/tests/v3/compatibility-suite/test_v1_consumer.py b/tests/v3/compatiblity_suite/test_v1_consumer.py similarity index 98% rename from tests/v3/compatibility-suite/test_v1_consumer.py rename to tests/v3/compatiblity_suite/test_v1_consumer.py index 10c9989b6..9807f67cf 100644 --- a/tests/v3/compatibility-suite/test_v1_consumer.py +++ b/tests/v3/compatiblity_suite/test_v1_consumer.py @@ -342,7 +342,7 @@ def the_following_http_interactions_have_been_defined( converters={"ids": lambda s: list(map(int, s.split(",")))}, target_fixture="srv", ) -def the_mock_server_is_started_with_interactions( +def the_mock_server_is_started_with_interactions( # noqa: C901 ids: list[int], interaction_definitions: dict[int, InteractionDefinition], ) -> Generator[PactServer, Any, None]: @@ -377,7 +377,7 @@ def the_mock_server_is_started_with_interactions( definition.body.string, definition.body.mime_type, ) - else: + elif definition.body.bytes: logging.info( "-> with_binary_file(%s, %s)", truncate(definition.body.bytes), @@ -387,6 +387,9 @@ def the_mock_server_is_started_with_interactions( definition.body.bytes, definition.body.mime_type, ) + else: + msg = "Unexpected body definition" + raise RuntimeError(msg) logging.info("-> will_respond_with(%s)", definition.response) interaction.will_respond_with(definition.response) @@ -406,7 +409,7 @@ def the_mock_server_is_started_with_interactions( definition.response_body.string, definition.response_content, ) - else: + elif definition.response_body.bytes: logging.info( "-> with_binary_file(%s, %s)", truncate(definition.response_body.bytes), @@ -416,6 +419,9 @@ def the_mock_server_is_started_with_interactions( definition.response_body.bytes, definition.response_content, ) + else: + msg = "Unexpected body definition" + raise RuntimeError(msg) with pact.serve(raises=False) as srv: yield srv diff --git a/tests/v3/compatibility-suite/util.py b/tests/v3/compatiblity_suite/util.py similarity index 100% rename from tests/v3/compatibility-suite/util.py rename to tests/v3/compatiblity_suite/util.py From 32ded0037a77b7fa25280889c62b47e53a6337d6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 16 Nov 2023 09:34:09 +1100 Subject: [PATCH 0121/1376] chore: update submodule Additionally, enable renovate's support for git submodules. This is still in beta and therefore has to be enabled manually. Signed-off-by: JP-Ellis --- .github/renovate.json | 3 +++ tests/v3/compatiblity_suite/definition | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 41784f53e..5116c9514 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -4,6 +4,9 @@ "pre-commit": { "enabled": true }, + "git-submodules": { + "enabled": true + }, "prHourlyLimit": 0, "prConcurrentLimit": 0 } diff --git a/tests/v3/compatiblity_suite/definition b/tests/v3/compatiblity_suite/definition index d22d4667c..db548451c 160000 --- a/tests/v3/compatiblity_suite/definition +++ b/tests/v3/compatiblity_suite/definition @@ -1 +1 @@ -Subproject commit d22d4667c0bda76d408676044cb33db834e7167e +Subproject commit db548451c9a7515ba2adb11cb4c140fb1363f00b From 24d9642f64aef4df50613b02107669c5d5a53908 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 22:21:07 +0000 Subject: [PATCH 0122/1376] chore(deps): update dependency dev/ruff to v0.1.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b65562869..e026884fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ dev = [ "pact-python[types]", "pact-python[test]", "black ==23.11.0", - "ruff ==0.1.5", + "ruff ==0.1.6", ] ################################################################################ From 636cc35422d4ce0f3cd85025a270ccfd426d5b00 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 22:21:10 +0000 Subject: [PATCH 0123/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.6 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 117b3cbbd..1a4eb510f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.5 + rev: v0.1.6 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 8a81106a7bc97f8527eacbe4b57243e77afcf548 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 13 Oct 2023 11:06:23 +1100 Subject: [PATCH 0124/1376] feat: add python 3.12 support Update the build scripts to test and target Python 3.12 released on 2 October 2023. As the dependency on distutils has been removed already, there are no other changes required. Signed-off-by: JP-Ellis --- .cirrus.yml | 2 ++ .github/workflows/build.yml | 2 +- .github/workflows/test.yml | 6 +++--- pyproject.toml | 23 ++++++++++++++++++----- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index f6ba0c61e..7ad9e9082 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -17,6 +17,7 @@ linux_arm64_task: - IMAGE: "python:3.9-slim" - IMAGE: "python:3.10-slim" - IMAGE: "python:3.11-slim" + - IMAGE: "python:3.12-slim" arm_container: image: $IMAGE install_script: @@ -37,6 +38,7 @@ macosx_arm64_task: - PYTHON: "3.9" - PYTHON: "3.10" - PYTHON: "3.11" + - PYTHON: "3.12" install_script: - brew update - brew install pyenv diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f2d5cd65d..40a20f578 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true env: - STABLE_PYTHON_VERSION: "3.11" + STABLE_PYTHON_VERSION: "3.12" CIBW_BUILD_FRONTEND: build jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e63ff5e85..8ed5e29b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ concurrency: cancel-in-progress: true env: - STABLE_PYTHON_VERSION: "3.11" + STABLE_PYTHON_VERSION: "3.12" PYTEST_ADDOPTS: --color=yes jobs: @@ -28,12 +28,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] experimental: [false] include: - # Run tests against the next Python version, but no need for the full list of OSes. os: ubuntu-latest - python-version: "3.12-dev" + python-version: "3.13.0-alpha.0 - 3.13" experimental: true steps: diff --git a/pyproject.toml b/pyproject.toml index e026884fe..44bc2ed97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Testing", ] -requires-python = ">=3.8, <3.12" +requires-python = ">=3.8" # Dependencies of Pact Python should be specified using the broadest range # compatible version unless: @@ -87,7 +88,13 @@ dev = [ ################################################################################ [build-system] -requires = ["hatchling", "packaging", "requests", "cffi"] +requires = [ + "hatchling", + "packaging", + "requests", + "cffi", + "setuptools ; python_version >= '3.12'", +] build-backend = "hatchling.build" [tool.hatch.version] @@ -110,8 +117,14 @@ artifacts = ["pact/bin/*", "pact/lib/*", "pact/v3/_ffi.*"] # Install dev dependencies in the default environment to simplify the developer # workflow. [tool.hatch.envs.default] -features = ["dev"] -extra-dependencies = ["hatchling", "packaging", "requests", "cffi"] +features = ["dev"] +extra-dependencies = [ + "hatchling", + "packaging", + "requests", + "cffi", + "setuptools ; python_version >= '3.12'", +] [tool.hatch.envs.default.scripts] lint = ["black --check --diff {args:.}", "ruff {args:.}", "mypy {args:.}"] @@ -125,7 +138,7 @@ all = ["lint", "test", "example"] features = ["test"] [[tool.hatch.envs.test.matrix]] -python = ["3.8", "3.9", "3.10", "3.11"] +python = ["3.8", "3.9", "3.10", "3.11", "3.12"] [tool.hatch.envs.test.scripts] test = "pytest {args:tests/}" From e6771a58830aada13bbd68039c2ffb2447731959 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 22 Nov 2023 13:33:32 +1100 Subject: [PATCH 0125/1376] chore(ci): set hatch to be verbose This will help with debugging any errors that might occur during the venv setup. Signed-off-by: JP-Ellis --- .cirrus.yml | 1 + .github/workflows/build.yml | 1 + .github/workflows/test.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.cirrus.yml b/.cirrus.yml index 7ad9e9082..cbd26fdc3 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -12,6 +12,7 @@ linux_arm64_task: env: PATH: ${HOME}/.local/bin:${PATH} CIRRUS_CLONE_SUBMODULES: "true" + HATCH_VERBOSE: 1 matrix: - IMAGE: "python:3.8-slim" - IMAGE: "python:3.9-slim" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 40a20f578..284125bab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,7 @@ concurrency: env: STABLE_PYTHON_VERSION: "3.12" + HATCH_VERBOSE: 1 CIBW_BUILD_FRONTEND: build jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8ed5e29b1..91f6a3508 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,7 @@ concurrency: env: STABLE_PYTHON_VERSION: "3.12" PYTEST_ADDOPTS: --color=yes + HATCH_VERBOSE: 1 jobs: test: From 318d541cf9a20569a3d977208095439d926e6e1a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 22 Nov 2023 13:42:28 +1100 Subject: [PATCH 0126/1376] chore(ci): add test conclusion step We require the test matrix to succeed before a PR can be merged. Unfortunately, the rule is rather complex as it has to test that each element of the test matrix completed successfully. By adding another step which runs only of the matrix succeeded, we can check that this ran, as opposed to each element of the matrix. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 91f6a3508..838cde4a7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,6 +60,16 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} + test-conlusion: + name: Test matrix complete + + runs-on: ubuntu-latest + needs: + - test + + steps: + - run: echo "Test matrix completed successfully." + example: name: Example From 8c3b267117d6faf5f8315f98d07bf0d60411e04f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:46:04 +0000 Subject: [PATCH 0127/1376] chore(deps): update dependency types/mypy to v1.7.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 44bc2ed97..1d2c68ed7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ pact-verifier = "pact.cli.verify:main" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. types = [ - "mypy ==1.7.0", + "mypy ==1.7.1", "types-cffi ~= 1.0", "types-requests ~= 2.0", ] From e9fd7bafe2c35bd36d081f64cc2462b9804c39af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:02:08 +0000 Subject: [PATCH 0128/1376] chore(deps): update pypa/gh-action-pypi-publish action to v1.8.11 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 284125bab..61f214b7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -170,7 +170,7 @@ jobs: path: wheels - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.10 + uses: pypa/gh-action-pypi-publish@v1.8.11 with: skip-existing: true password: ${{ secrets.PYPI_TOKEN }} From 9eeb5acdddabca5ad38cbe22c563c461db9e192b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 Dec 2023 16:36:27 +0000 Subject: [PATCH 0129/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.13.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a4eb510f..255d5e3a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: stages: [pre-push] - repo: https://github.com/commitizen-tools/commitizen - rev: v3.12.0 + rev: v3.13.0 hooks: - id: commitizen stages: [commit-msg] From a8431f37ef1f862e4aa0c2785c1e83d5eda119f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:24:44 +0000 Subject: [PATCH 0130/1376] chore(deps): update dependency dev/ruff to v0.1.7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1d2c68ed7..2835f49c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ dev = [ "pact-python[types]", "pact-python[test]", "black ==23.11.0", - "ruff ==0.1.6", + "ruff ==0.1.7", ] ################################################################################ From 17d59984c6a6b89abed091bf533d82da09f2f37c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:24:48 +0000 Subject: [PATCH 0131/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.7 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 255d5e3a2..1e4a53214 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 + rev: v0.1.7 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 1f5e14bcfa6449567d1e5bd3a915f4da16a12259 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 6 Dec 2023 12:47:54 +1100 Subject: [PATCH 0132/1376] chore(deps): use renovate best practices Signed-off-by: JP-Ellis --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 5116c9514..145fc10b7 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,6 +1,6 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base"], + "extends": ["config:best-practices"], "pre-commit": { "enabled": true }, From 136864a97c822087e164cc77ff82c245527ef6b6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 03:23:17 +0000 Subject: [PATCH 0133/1376] chore(deps): pin dependencies --- .github/workflows/build.yml | 30 +++++++++++++++--------------- .github/workflows/labels.yml | 4 ++-- .github/workflows/test.yml | 16 ++++++++-------- Dockerfile.ubuntu | 2 +- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 61f214b7b..4d12b542c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,13 +27,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: # Fetch all tags fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} @@ -45,7 +45,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: wheels path: ./dist/*.tar.* @@ -68,18 +68,18 @@ jobs: archs: AMD64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: # Fetch all tags fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@fff9ec32ed25a9c576750c91e06b410ed0c15db7 # v2.16.2 env: CIBW_ARCHS: ${{ matrix.archs }} - name: Upload wheels - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: wheels path: ./wheelhouse/*.whl @@ -103,24 +103,24 @@ jobs: archs: arm64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: # Fetch all tags fetch-depth: 0 - name: Set up QEMU if: matrix.os == 'ubuntu-latest' - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3 with: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@fff9ec32ed25a9c576750c91e06b410ed0c15db7 # v2.16.2 env: CIBW_ARCHS: ${{ matrix.archs }} - name: Upload wheels - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: wheels path: ./wheelhouse/*.whl @@ -136,13 +136,13 @@ jobs: - build-arm64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3 with: name: wheels path: wheelhouse @@ -164,13 +164,13 @@ jobs: id-token: write steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3 with: name: wheels path: wheels - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.11 + uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf # v1.8.11 with: skip-existing: true password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 80034ca4a..48d8876b1 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -20,10 +20,10 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Synchronize labels - uses: EndBug/label-sync@v2 + uses: EndBug/label-sync@da00f2c11fdb78e4fae44adac2fdd713778ea3e8 # v2 with: config-file: | https://raw.githubusercontent.com/pact-foundation/.github/master/.github/labels.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 838cde4a7..6a266d61b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,12 +38,12 @@ jobs: experimental: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: submodules: true - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 with: python-version: ${{ matrix.python-version }} @@ -56,7 +56,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -78,7 +78,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest + image: pactfoundation/pact-broker:latest@sha256:186205f0596fd4f4ce553876f6e846ae614db2b9d582f0391ec418d71e5e4473 ports: - "9292:9292" env: @@ -90,10 +90,10 @@ jobs: PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Set up Python 3 - uses: actions/setup-python@v4 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} @@ -122,10 +122,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index c841bc1cc..b3673a749 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ubuntu:22.04 +FROM ubuntu:22.04@sha256:8eab65df33a6de2844c9aefd19efe8ddb87b7df5e9185a4ab73af936225685bb ENV DEBIAN_FRONTEND=noninteractive ARG PYTHON_VERSION 3.9 From bb5c642d8248f81a8c7a1b8b85eddb58269496b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:06:30 +0000 Subject: [PATCH 0134/1376] chore(deps): update actions/setup-python action to v5 --- .github/workflows/build.yml | 4 ++-- .github/workflows/test.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d12b542c..c5e265d50 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} @@ -138,7 +138,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Setup Python - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6a266d61b..175ee25e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,7 @@ jobs: submodules: true - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: python-version: ${{ matrix.python-version }} @@ -93,7 +93,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Set up Python 3 - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} @@ -125,7 +125,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Set up Python - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} From ca135922ca71c036f0942d87bfe2b1427ad80c93 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 07:00:56 +0000 Subject: [PATCH 0135/1376] chore(deps): update pre-commit hook psf/black to v23.12.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e4a53214..39a267225 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: stages: [pre-push] - repo: https://github.com/psf/black - rev: 23.11.0 + rev: 23.12.0 hooks: - id: black # Exclude python files in pact/** and tests/**, except for the From 52a94a221d9cfc77ddbd8ff94336f83f09daf13d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 07:00:52 +0000 Subject: [PATCH 0136/1376] chore(deps): update dependency dev/black to v23.12.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2835f49c4..5b66352ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ test = [ dev = [ "pact-python[types]", "pact-python[test]", - "black ==23.11.0", + "black ==23.12.0", "ruff ==0.1.7", ] From b762e3710bc9c58c17cb4410751cfea3ff9df76d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 19:14:41 +0000 Subject: [PATCH 0137/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.8 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39a267225..4e27f296d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.7 + rev: v0.1.8 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 696b918b85482bfed5c3238f5701cdbe266b153c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 19:14:37 +0000 Subject: [PATCH 0138/1376] chore(deps): update dependency dev/ruff to v0.1.8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5b66352ec..4e665ede7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ dev = [ "pact-python[types]", "pact-python[test]", "black ==23.12.0", - "ruff ==0.1.7", + "ruff ==0.1.8", ] ################################################################################ From 8ca941651cb90e0bcf97c2bbf847160c332df46f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Dec 2023 10:21:32 +0000 Subject: [PATCH 0139/1376] chore(deps): update ubuntu:22.04 docker digest to 6042500 --- Dockerfile.ubuntu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index b3673a749..0bbf9742e 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ubuntu:22.04@sha256:8eab65df33a6de2844c9aefd19efe8ddb87b7df5e9185a4ab73af936225685bb +FROM ubuntu:22.04@sha256:6042500cf4b44023ea1894effe7890666b0c5c7871ed83a97c36c76ae560bb9b ENV DEBIAN_FRONTEND=noninteractive ARG PYTHON_VERSION 3.9 From 24accce3ed03efe4c873f8f294f11991199485e4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 21:26:29 +0000 Subject: [PATCH 0140/1376] chore(deps): update actions/download-artifact action to v4 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5e265d50..9a9879455 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -142,7 +142,7 @@ jobs: with: python-version: ${{ env.STABLE_PYTHON_VERSION }} - - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3 + - uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4 with: name: wheels path: wheelhouse @@ -164,7 +164,7 @@ jobs: id-token: write steps: - - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3 + - uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4 with: name: wheels path: wheels From 98a22b0e88a53f65156ca6021192471ff1da8983 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 21:26:33 +0000 Subject: [PATCH 0141/1376] chore(deps): update actions/upload-artifact action to v4 --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a9879455..49b48212a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4 with: name: wheels path: ./dist/*.tar.* @@ -79,7 +79,7 @@ jobs: CIBW_ARCHS: ${{ matrix.archs }} - name: Upload wheels - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4 with: name: wheels path: ./wheelhouse/*.whl @@ -120,7 +120,7 @@ jobs: CIBW_ARCHS: ${{ matrix.archs }} - name: Upload wheels - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4 with: name: wheels path: ./wheelhouse/*.whl From e9043065a6a559dcef4a58f4286ac8af3542731f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 18 Dec 2023 09:36:07 +1100 Subject: [PATCH 0142/1376] chore(ci): breaking changes with for artifacts The `v4` of download/upload artifacts have some breaking changes. In particular, all artifacts must have different names now (i.e., it is no longer possible to append to the artifact). Omitting the artifact name for the download will match all artifacts from the pipeline. Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49b48212a..425cdef00 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,9 +47,10 @@ jobs: - name: Upload sdist uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4 with: - name: wheels + name: wheels-sdist path: ./dist/*.tar.* if-no-files-found: error + compression-level: 0 build-x86_64: name: Build wheels on ${{ matrix.os }} (x86, 64-bit) @@ -81,9 +82,10 @@ jobs: - name: Upload wheels uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4 with: - name: wheels + name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl if-no-files-found: error + compression-level: 0 build-arm64: name: Build wheels on ${{ matrix.os }} (arm64) @@ -122,9 +124,10 @@ jobs: - name: Upload wheels uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4 with: - name: wheels + name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl if-no-files-found: error + compression-level: 0 check: name: Check wheels @@ -144,7 +147,6 @@ jobs: - uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4 with: - name: wheels path: wheelhouse - run: | @@ -166,7 +168,6 @@ jobs: steps: - uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4 with: - name: wheels path: wheels - name: Push build artifacts to PyPI From 603cfc881b9309cea7fca0a7a6f4cfb7107a483c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 16 Nov 2023 11:20:45 +1100 Subject: [PATCH 0143/1376] chore(ci): re-enable pypy builds on Windows With the resolution of the upstream issue with psutil builds on Windows and PyPy, we can re-enable the building of wheels for this target. Signed-off-by: JP-Ellis --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4e665ede7..39b483570 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -274,7 +274,3 @@ assert isinstance(pact.v3.ffi.version(), str);\"""" # The repair tool unfortunately did not like the bundled Ruby distributable. # TODO: Check whether delocate-wheel can be configured. repair-wheel-command = "" - -[tool.cibuildwheel.windows] -# Skipping pypy, see giampaolo/psutil#2325 -skip = "pp*" From 5d0a240f30d62f274a3a756600f84d7e07deded1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 00:49:24 +0000 Subject: [PATCH 0144/1376] chore(deps): update actions/download-artifact digest to f44cd7b --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 425cdef00..9daeced1a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,7 +145,7 @@ jobs: with: python-version: ${{ env.STABLE_PYTHON_VERSION }} - - uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4 + - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4 with: path: wheelhouse @@ -166,7 +166,7 @@ jobs: id-token: write steps: - - uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4 + - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4 with: path: wheels From e5acfde365147e4a7a27577a782a7a27abda70d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 19:03:21 +0000 Subject: [PATCH 0145/1376] chore(deps): update dependency dev/ruff to v0.1.9 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 39b483570..900d3d625 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ dev = [ "pact-python[types]", "pact-python[test]", "black ==23.12.0", - "ruff ==0.1.8", + "ruff ==0.1.9", ] ################################################################################ From fe7c01f0c16589196192f431f478d973953ddb08 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 19:03:25 +0000 Subject: [PATCH 0146/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.9 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4e27f296d..ad875c208 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.8 + rev: v0.1.9 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 69830fbae7af305ed3d246ba202c6fa49f5ae2d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 17:03:17 +0000 Subject: [PATCH 0147/1376] chore(deps): update dependency types/mypy to v1.8.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 900d3d625..76770bda5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ pact-verifier = "pact.cli.verify:main" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. types = [ - "mypy ==1.7.1", + "mypy ==1.8.0", "types-cffi ~= 1.0", "types-requests ~= 2.0", ] From 7b0694e6c678ce334f5e82cc597531c8a76b8d66 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 23 Dec 2023 00:29:43 +0000 Subject: [PATCH 0148/1376] chore(deps): update dependency dev/black to v23.12.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 76770bda5..887241331 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ test = [ dev = [ "pact-python[types]", "pact-python[test]", - "black ==23.12.0", + "black ==23.12.1", "ruff ==0.1.9", ] From 46521a1d4c3636762edff1da9a08d1d225b4f93a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 23 Dec 2023 00:29:46 +0000 Subject: [PATCH 0149/1376] chore(deps): update pre-commit hook psf/black to v23.12.1 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad875c208..e980bdda1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: stages: [pre-push] - repo: https://github.com/psf/black - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black # Exclude python files in pact/** and tests/**, except for the From 889e00f390ea06fd8f3e3a99b8ed8e9843dd7a12 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 22:34:32 +0000 Subject: [PATCH 0150/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.10 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e980bdda1..bfee6d045 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.9 + rev: v0.1.10 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From d8a74f955ae004a01d9c6958f33464794434183e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 23:01:14 +0000 Subject: [PATCH 0151/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.11 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bfee6d045..2ec9ce32b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.10 + rev: v0.1.11 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From a95e499b30cf91adce3be625e5fe23f914588303 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 23:25:38 +0000 Subject: [PATCH 0152/1376] chore(deps): update dependency dev/ruff to v0.1.11 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 887241331..978cd1922 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ dev = [ "pact-python[types]", "pact-python[test]", "black ==23.12.1", - "ruff ==0.1.9", + "ruff ==0.1.11", ] ################################################################################ From 6c0e4111c72ac9afb36b02be6a8371db58da2ee5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Jan 2024 04:13:04 +0000 Subject: [PATCH 0153/1376] chore(deps): update pactfoundation/pact-broker:latest docker digest to 9cdd475 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 175ee25e1..f0e1f2314 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,7 +78,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:186205f0596fd4f4ce553876f6e846ae614db2b9d582f0391ec418d71e5e4473 + image: pactfoundation/pact-broker:latest@sha256:9cdd475459ee608b8ab4c32ad72e7aa9236bc5072c123fe689bdc3f44e1ea8dc ports: - "9292:9292" env: From c6604df91c4ce3b414ee8b0d36032d86e62f1e45 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Jan 2024 18:05:38 +0000 Subject: [PATCH 0154/1376] chore(deps): update actions/download-artifact digest to 6b208ae --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9daeced1a..e2d9c32ce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,7 +145,7 @@ jobs: with: python-version: ${{ env.STABLE_PYTHON_VERSION }} - - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4 + - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4 with: path: wheelhouse @@ -166,7 +166,7 @@ jobs: id-token: write steps: - - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4 + - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4 with: path: wheels From 634e09d2ac45361e5893a6bc5e3abd908fc45ac9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 19:17:51 +0000 Subject: [PATCH 0155/1376] chore(deps): update actions/upload-artifact digest to 1eb3cb2 --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2d9c32ce..525e1312d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4 + uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4 with: name: wheels-sdist path: ./dist/*.tar.* @@ -80,7 +80,7 @@ jobs: CIBW_ARCHS: ${{ matrix.archs }} - name: Upload wheels - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4 + uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl @@ -122,7 +122,7 @@ jobs: CIBW_ARCHS: ${{ matrix.archs }} - name: Upload wheels - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4 + uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl From 233714c6554096497908ea23b9e933a437bc7625 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 17:06:30 +0000 Subject: [PATCH 0156/1376] chore(deps): update dependency dev/ruff to v0.1.13 Signed-off-by: JP-Ellis --- examples/src/consumer.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/src/consumer.py b/examples/src/consumer.py index 18819d4f7..42a249fe6 100644 --- a/examples/src/consumer.py +++ b/examples/src/consumer.py @@ -30,7 +30,7 @@ class User: """User data class.""" - id: int # noqa: A003 + id: int name: str created_on: datetime diff --git a/pyproject.toml b/pyproject.toml index 978cd1922..57e81421d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ dev = [ "pact-python[types]", "pact-python[test]", "black ==23.12.1", - "ruff ==0.1.11", + "ruff ==0.1.13", ] ################################################################################ From 53518e6f8442bbd9f467799a5e903887657f0ae8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 17:06:35 +0000 Subject: [PATCH 0157/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.13 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ec9ce32b..f3211a01b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.11 + rev: v0.1.13 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 5ed537923ddc8318ba0fbfc566e162464e85edf2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 01:52:00 +0000 Subject: [PATCH 0158/1376] chore(deps): update actions/upload-artifact digest to 694cdab --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 525e1312d..5eff9535b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4 with: name: wheels-sdist path: ./dist/*.tar.* @@ -80,7 +80,7 @@ jobs: CIBW_ARCHS: ${{ matrix.archs }} - name: Upload wheels - uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl @@ -122,7 +122,7 @@ jobs: CIBW_ARCHS: ${{ matrix.archs }} - name: Upload wheels - uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl From 1461ef77868193d66603580b8edf83cca82916a8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 20:22:50 +0000 Subject: [PATCH 0159/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.14 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f3211a01b..9f2f3ea00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.13 + rev: v0.1.14 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From a66c99ae967fcdbc715372208591ab4b7df0f827 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Jan 2024 10:18:23 +0000 Subject: [PATCH 0160/1376] chore(deps): update ubuntu:22.04 docker digest to e6173d4 --- Dockerfile.ubuntu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 0bbf9742e..7c31ccdec 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ubuntu:22.04@sha256:6042500cf4b44023ea1894effe7890666b0c5c7871ed83a97c36c76ae560bb9b +FROM ubuntu:22.04@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 ENV DEBIAN_FRONTEND=noninteractive ARG PYTHON_VERSION 3.9 From 6ed7b3767f105a16bc234e0eec98f98bb40ede9e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 23:09:15 +0000 Subject: [PATCH 0161/1376] chore(deps): update dependency dev/ruff to v0.1.14 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 57e81421d..9a12dce55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ dev = [ "pact-python[types]", "pact-python[test]", "black ==23.12.1", - "ruff ==0.1.13", + "ruff ==0.1.14", ] ################################################################################ From 6affa989910f01d350770b0fdcb073dd10d3033d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 18:38:05 +0000 Subject: [PATCH 0162/1376] chore(deps): update actions/upload-artifact digest to 26f96df --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5eff9535b..af101126a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4 with: name: wheels-sdist path: ./dist/*.tar.* @@ -80,7 +80,7 @@ jobs: CIBW_ARCHS: ${{ matrix.archs }} - name: Upload wheels - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl @@ -122,7 +122,7 @@ jobs: CIBW_ARCHS: ${{ matrix.archs }} - name: Upload wheels - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl From d0e70ca5c23b194e94531c660a6e8b9c3e840f56 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Jan 2024 21:05:40 +0000 Subject: [PATCH 0163/1376] chore(deps): update pypa/cibuildwheel action to v2.16.3 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af101126a..b43d7a78e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,7 +75,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@fff9ec32ed25a9c576750c91e06b410ed0c15db7 # v2.16.2 + uses: pypa/cibuildwheel@e250df5d5da8c45226a8de1a80e6bfbbf46f5e4b # v2.16.3 env: CIBW_ARCHS: ${{ matrix.archs }} @@ -117,7 +117,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@fff9ec32ed25a9c576750c91e06b410ed0c15db7 # v2.16.2 + uses: pypa/cibuildwheel@e250df5d5da8c45226a8de1a80e6bfbbf46f5e4b # v2.16.3 env: CIBW_ARCHS: ${{ matrix.archs }} From 8e38e3505b2fe1a1e01fb60ccc9955526f356d48 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Jan 2024 05:38:40 +0000 Subject: [PATCH 0164/1376] chore(deps): update pre-commit hook psf/black to v24 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f2f3ea00..53d7579b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: stages: [pre-push] - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 24.1.0 hooks: - id: black # Exclude python files in pact/** and tests/**, except for the From a7f4518b496ee365f710387b91389709a5ddee78 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:44:50 +0000 Subject: [PATCH 0165/1376] chore(deps): update codecov/codecov-action digest to 4fe8c5f --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0e1f2314..d79427c1d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,7 +56,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3 + uses: codecov/codecov-action@4fe8c5f003fae66aa5ebb77cfd3e7bfbbda0b6b0 # v3 with: token: ${{ secrets.CODECOV_TOKEN }} From c906211cda3449e70f60c59ae870eb6404c527c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 28 Jan 2024 07:50:16 +0000 Subject: [PATCH 0166/1376] chore(deps): update dependency dev/black to v24 Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 108 ++++++------------ pact/v3/pact.py | 33 +++--- pyproject.toml | 2 +- .../v3/compatiblity_suite/test_v1_consumer.py | 12 +- tests/v3/conftest.py | 1 - tests/v3/test_ffi.py | 1 + 6 files changed, 59 insertions(+), 98 deletions(-) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 4d085aaca..78c025dea 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -101,32 +101,25 @@ # to inform the type checker of the existence of these types. -class AsynchronousMessage: - ... +class AsynchronousMessage: ... -class Consumer: - ... +class Consumer: ... -class Generator: - ... +class Generator: ... -class GeneratorCategoryIterator: - ... +class GeneratorCategoryIterator: ... -class GeneratorKeyValuePair: - ... +class GeneratorKeyValuePair: ... -class HttpRequest: - ... +class HttpRequest: ... -class HttpResponse: - ... +class HttpResponse: ... class InteractionHandle: @@ -160,84 +153,64 @@ def __repr__(self) -> str: return f"InteractionHandle({self._ref!r})" -class MatchingRule: - ... +class MatchingRule: ... -class MatchingRuleCategoryIterator: - ... +class MatchingRuleCategoryIterator: ... -class MatchingRuleDefinitionResult: - ... +class MatchingRuleDefinitionResult: ... -class MatchingRuleIterator: - ... +class MatchingRuleIterator: ... -class MatchingRuleKeyValuePair: - ... +class MatchingRuleKeyValuePair: ... -class MatchingRuleResult: - ... +class MatchingRuleResult: ... -class Message: - ... +class Message: ... -class MessageContents: - ... +class MessageContents: ... -class MessageHandle: - ... +class MessageHandle: ... -class MessageMetadataIterator: - ... +class MessageMetadataIterator: ... -class MessageMetadataPair: - ... +class MessageMetadataPair: ... -class MessagePact: - ... +class MessagePact: ... -class MessagePactHandle: - ... +class MessagePactHandle: ... -class MessagePactMessageIterator: - ... +class MessagePactMessageIterator: ... -class MessagePactMetadataIterator: - ... +class MessagePactMetadataIterator: ... -class MessagePactMetadataTriple: - ... +class MessagePactMetadataTriple: ... -class Mismatch: - ... +class Mismatch: ... -class Mismatches: - ... +class Mismatches: ... -class MismatchesIterator: - ... +class MismatchesIterator: ... -class Pact: - ... +class Pact: ... class PactHandle: @@ -327,8 +300,7 @@ def port(self) -> int: return self._ref -class PactInteraction: - ... +class PactInteraction: ... class PactInteractionIterator: @@ -535,36 +507,28 @@ def __next__(self) -> SynchronousMessage: return pact_sync_message_iter_next(self) -class Provider: - ... +class Provider: ... -class ProviderState: - ... +class ProviderState: ... -class ProviderStateIterator: - ... +class ProviderStateIterator: ... -class ProviderStateParamIterator: - ... +class ProviderStateParamIterator: ... -class ProviderStateParamPair: - ... +class ProviderStateParamPair: ... -class SynchronousHttp: - ... +class SynchronousHttp: ... -class SynchronousMessage: - ... +class SynchronousMessage: ... -class VerifierHandle: - ... +class VerifierHandle: ... class ExpressionValueType(Enum): diff --git a/pact/v3/pact.py b/pact/v3/pact.py index 7ef64e5b3..bb655d2d3 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -107,16 +107,13 @@ def _parse_interaction_part( raise ValueError(msg) @overload - def given(self, state: str) -> Self: - ... + def given(self, state: str) -> Self: ... @overload - def given(self, state: str, *, name: str, value: str) -> Self: - ... + def given(self, state: str, *, name: str, value: str) -> Self: ... @overload - def given(self, state: str, *, parameters: dict[str, Any] | str) -> Self: - ... + def given(self, state: str, *, parameters: dict[str, Any] | str) -> Self: ... def given( self, @@ -1027,24 +1024,21 @@ def upon_receiving( self, description: str, interaction: Literal["HTTP"] = ..., - ) -> HttpInteraction: - ... + ) -> HttpInteraction: ... @overload def upon_receiving( self, description: str, interaction: Literal["Async"], - ) -> AsyncMessageInteraction: - ... + ) -> AsyncMessageInteraction: ... @overload def upon_receiving( self, description: str, interaction: Literal["Sync"], - ) -> SyncMessageInteraction: - ... + ) -> SyncMessageInteraction: ... def upon_receiving( self, @@ -1149,19 +1143,22 @@ def messages(self) -> pact.v3.ffi.PactMessageIterator: return pact.v3.ffi.pact_handle_get_message_iter(self._handle) @overload - def interactions(self, kind: Literal["HTTP"]) -> pact.v3.ffi.PactSyncHttpIterator: - ... + def interactions( + self, + kind: Literal["HTTP"], + ) -> pact.v3.ffi.PactSyncHttpIterator: ... @overload def interactions( self, kind: Literal["Sync"], - ) -> pact.v3.ffi.PactSyncMessageIterator: - ... + ) -> pact.v3.ffi.PactSyncMessageIterator: ... @overload - def interactions(self, kind: Literal["Async"]) -> pact.v3.ffi.PactMessageIterator: - ... + def interactions( + self, + kind: Literal["Async"], + ) -> pact.v3.ffi.PactMessageIterator: ... def interactions( self, diff --git a/pyproject.toml b/pyproject.toml index 9a12dce55..349424673 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ test = [ dev = [ "pact-python[types]", "pact-python[test]", - "black ==23.12.1", + "black ==24.1.1", "ruff ==0.1.14", ] diff --git a/tests/v3/compatiblity_suite/test_v1_consumer.py b/tests/v3/compatiblity_suite/test_v1_consumer.py index 9807f67cf..e402bb682 100644 --- a/tests/v3/compatiblity_suite/test_v1_consumer.py +++ b/tests/v3/compatiblity_suite/test_v1_consumer.py @@ -453,9 +453,9 @@ def request_n_is_made_to_the_mock_server( return requests.request( definition.method, str(srv.url.with_path(definition.path)), - params=URL.build(query_string=definition.query).query - if definition.query - else None, + params=( + URL.build(query_string=definition.query).query if definition.query else None + ), headers=definition.headers if definition.headers else None, # type: ignore[arg-type] data=definition.body.bytes if definition.body else None, ) @@ -502,9 +502,9 @@ def request_n_is_made_to_the_mock_server_with_the_following_changes( return requests.request( definition.method, str(srv.url.with_path(definition.path)), - params=URL.build(query_string=definition.query).query - if definition.query - else None, + params=( + URL.build(query_string=definition.query).query if definition.query else None + ), headers=definition.headers if definition.headers else None, # type: ignore[arg-type] data=definition.body.bytes if definition.body else None, ) diff --git a/tests/v3/conftest.py b/tests/v3/conftest.py index eb69d16fa..b75fc4792 100644 --- a/tests/v3/conftest.py +++ b/tests/v3/conftest.py @@ -5,7 +5,6 @@ directory. """ - import pytest diff --git a/tests/v3/test_ffi.py b/tests/v3/test_ffi.py index 53e00361d..496240e1c 100644 --- a/tests/v3/test_ffi.py +++ b/tests/v3/test_ffi.py @@ -5,6 +5,7 @@ They are not intended to test the Pact API itself, as that is handled by the client library. """ + import re import pytest From 3f7213d9df779ceb06895506306d57852560aeed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 28 Jan 2024 07:50:11 +0000 Subject: [PATCH 0167/1376] chore(deps): update pre-commit hook psf/black to v24.1.1 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 53d7579b4..8ba31962b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: stages: [pre-push] - repo: https://github.com/psf/black - rev: 24.1.0 + rev: 24.1.1 hooks: - id: black # Exclude python files in pact/** and tests/**, except for the From b3207c8416929448ee4b4f4d60be7a43a7202b63 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 28 Jan 2024 18:56:42 +0000 Subject: [PATCH 0168/1376] chore(deps): update pypa/cibuildwheel action to v2.16.4 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b43d7a78e..b9cde84f9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,7 +75,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@e250df5d5da8c45226a8de1a80e6bfbbf46f5e4b # v2.16.3 + uses: pypa/cibuildwheel@0b04ab1040366101259658b355777e4ff2d16f83 # v2.16.4 env: CIBW_ARCHS: ${{ matrix.archs }} @@ -117,7 +117,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@e250df5d5da8c45226a8de1a80e6bfbbf46f5e4b # v2.16.3 + uses: pypa/cibuildwheel@0b04ab1040366101259658b355777e4ff2d16f83 # v2.16.4 env: CIBW_ARCHS: ${{ matrix.archs }} From dc73777be0046232ec8a9c3c4cb84298253da05e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 29 Jan 2024 16:58:41 +1100 Subject: [PATCH 0169/1376] chore(dev): replace black with ruff Ruff has included (for a few months now) a formatter functionality. The formatting and linting are entirely distinct functionalities, though both provided by Ruff. My experience so far has been that Ruff's formatting is very fast, and mostly matches Black's formatting. Signed-off-by: JP-Ellis --- .cirrus.yml | 2 + .github/workflows/test.yml | 6 ++ .pre-commit-config.yaml | 10 +-- CONTRIBUTING.md | 4 +- Makefile | 2 + examples/tests/test_00_consumer.py | 2 +- examples/tests/test_01_provider_fastapi.py | 2 +- examples/tests/test_01_provider_flask.py | 2 +- examples/tests/test_02_message_consumer.py | 2 +- examples/tests/test_03_message_provider.py | 1 + pact/v3/__init__.py | 2 +- pact/v3/ffi.py | 6 +- pact/v3/pact.py | 31 ++++---- pyproject.toml | 70 ++++++++++--------- .../v3/compatiblity_suite/test_v1_consumer.py | 4 +- tests/v3/test_async_interaction.py | 1 + tests/v3/test_ffi.py | 1 + tests/v3/test_http_interaction.py | 1 + tests/v3/test_pact.py | 1 + tests/v3/test_sync_interaction.py | 1 + 20 files changed, 81 insertions(+), 70 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index cbd26fdc3..dbd648868 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -6,6 +6,8 @@ TEST_TEMPLATE: &TEST_TEMPLATE - python --version # TODO: Fix lints before enabling - echo hatch run lint + - echo hatch run typecheck + - echo hatch run format - hatch run test linux_arm64_task: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d79427c1d..0861ce59e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -134,3 +134,9 @@ jobs: - name: Lint run: hatch run lint + + - name: Typecheck + run: hatch run typecheck + + - name: Format + run: hatch run format diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ba31962b..63df0509a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,16 +45,8 @@ repos: # files in pact/v3/** and tests/v3/**. exclude: ^(pact|tests)/(?!v3/).*\.py$ args: [--fix, --exit-non-zero-on-fix] - stages: [pre-push] - - - repo: https://github.com/psf/black - rev: 24.1.1 - hooks: - - id: black - # Exclude python files in pact/** and tests/**, except for the - # files in pact/v3/** and tests/v3/**. + - id: ruff-format exclude: ^(pact|tests)/(?!v3/).*\.py$ - stages: [pre-push] - repo: https://github.com/commitizen-tools/commitizen rev: v3.13.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f1de75e0..47a5a07bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -109,11 +109,11 @@ You can also try using the new [github.dev](https://github.dev/pact-foundation/p - **Most important: Look around.** Match the style you see used in the rest of the project. This includes formatting, naming files, naming things in code, naming things in documentation, etc. - "Attractive" -- We do have Black (a formatter) and Ruff (a syntax linter) to catch most stylistic problems. If you are working locally, they should automatically fix some issues during git commits and push. +- We do have Ruff to catch most stylistic problems (both linting and formatting). If you are working locally, they should automatically fix some issues during git commits and push. Don't worry too much about styles in general—the maintainers will help you fix them as they review your code. -To help catch a lot of simple formatting or linting issues, you can run `hatch run lint` to run Black and Ruff. This process can also be automated by installing [`pre-commit`](https://pre-commit.com/) hooks: +To help catch a lot of simple formatting or linting issues, you can run `hatch run lint` to run the linter and `hatch run format` to format your code. This process can also be automated by installing [`pre-commit`](https://pre-commit.com/) hooks: ```sh pre-commit install diff --git a/Makefile b/Makefile index aa7981fb1..de2363d3a 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,8 @@ example: .PHONY: lint lint: hatch run lint + hatch run format + hatch run typecheck .PHONY: ci diff --git a/examples/tests/test_00_consumer.py b/examples/tests/test_00_consumer.py index 52f24a36a..a49f9664a 100644 --- a/examples/tests/test_00_consumer.py +++ b/examples/tests/test_00_consumer.py @@ -21,10 +21,10 @@ import pytest import requests -from pact import Consumer, Format, Like, Provider from yarl import URL from examples.src.consumer import User, UserConsumer +from pact import Consumer, Format, Like, Provider if TYPE_CHECKING: from pathlib import Path diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/tests/test_01_provider_fastapi.py index 1d011ed7f..7ccd3128a 100644 --- a/examples/tests/test_01_provider_fastapi.py +++ b/examples/tests/test_01_provider_fastapi.py @@ -30,11 +30,11 @@ import pytest import uvicorn -from pact import Verifier from pydantic import BaseModel from yarl import URL from examples.src.fastapi import app +from pact import Verifier PROVIDER_URL = URL("http://localhost:8080") diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py index 20a84ee98..ba5c39d43 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/tests/test_01_provider_flask.py @@ -30,10 +30,10 @@ import pytest from flask import request -from pact import Verifier from yarl import URL from examples.src.flask import app +from pact import Verifier PROVIDER_URL = URL("http://localhost:8080") diff --git a/examples/tests/test_02_message_consumer.py b/examples/tests/test_02_message_consumer.py index f49c262ea..0603b4b96 100644 --- a/examples/tests/test_02_message_consumer.py +++ b/examples/tests/test_02_message_consumer.py @@ -35,9 +35,9 @@ from unittest.mock import MagicMock import pytest -from pact import MessageConsumer, MessagePact, Provider from examples.src.message import Handler +from pact import MessageConsumer, MessagePact, Provider if TYPE_CHECKING: from pathlib import Path diff --git a/examples/tests/test_03_message_provider.py b/examples/tests/test_03_message_provider.py index 9a9ff3f7d..581bd2391 100644 --- a/examples/tests/test_03_message_provider.py +++ b/examples/tests/test_03_message_provider.py @@ -29,6 +29,7 @@ from typing import TYPE_CHECKING, Dict from flask import Flask + from pact import MessageProvider if TYPE_CHECKING: diff --git a/pact/v3/__init__.py b/pact/v3/__init__.py index cb0059d95..6edcda7b6 100644 --- a/pact/v3/__init__.py +++ b/pact/v3/__init__.py @@ -20,7 +20,7 @@ considered deprecated, and will be removed in a future release. """ -from .pact import Interaction, Pact +from pact.v3.pact import Interaction, Pact __all__ = [ "Pact", diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 78c025dea..c0ee4b55c 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -88,7 +88,7 @@ from enum import Enum from typing import TYPE_CHECKING, Any, List -from ._ffi import ffi, lib # type: ignore[import] +from pact.v3._ffi import ffi, lib # type: ignore[import] if TYPE_CHECKING: from pathlib import Path @@ -5146,7 +5146,7 @@ def with_query_parameter_v2( handle, "version", 0, - json.dumps({ "value": ["2", "3"] }) + json.dumps({"value": ["2", "3"]}), ) ``` @@ -5288,7 +5288,7 @@ def with_header_v2( part, "Accept-Version", 0, - json.dumps({ "value": ["2", "3"] }) + json.dumps({"value": ["2", "3"]}), ) ``` diff --git a/pact/v3/pact.py b/pact/v3/pact.py index bb655d2d3..495868441 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -144,8 +144,9 @@ def given( ```python ( - pact.upon_receiving("a request") - .given("a user exists", name="id", value="123") + pact.upon_receiving("a request").given( + "a user exists", name="id", value="123" + ) ) ``` @@ -156,11 +157,13 @@ def given( ```python ( - pact.upon_receiving("a request") - .given("a user exists", parameters={ - "id": "123", - "name": "John", - }) + pact.upon_receiving("a request").given( + "a user exists", + parameters={ + "id": "123", + "name": "John", + }, + ) ) ``` @@ -200,8 +203,9 @@ def given( ```python ( - pact.upon_receiving("a request") - .given("a user exists", name="value", value=parameters) + pact.upon_receiving("a request").given( + "a user exists", name="value", value=parameters + ) ) ``` @@ -503,8 +507,7 @@ def with_header( ```python ( - pact.upon_receiving("a request") - .with_header( + pact.upon_receiving("a request").with_header( "Accept-Version", json.dumps({ "value": "1.2.3", @@ -693,8 +696,7 @@ def with_query_parameter(self, name: str, value: str) -> Self: ```python ( - pact.upon_receiving("a request") - .with_query_parameter( + pact.upon_receiving("a request").with_query_parameter( "name", json.dumps({ "value": ["John", "Mary"], @@ -709,8 +711,7 @@ def with_query_parameter(self, name: str, value: str) -> Self: ```python ( - pact.upon_receiving("a request") - .with_query_parameter( + pact.upon_receiving("a request").with_query_parameter( "version", json.dumps({ "value": "1.2.3", diff --git a/pyproject.toml b/pyproject.toml index 349424673..d1fe60f1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,12 +76,7 @@ test = [ "pytest-cov ~= 4.0", "testcontainers ~= 3.0", ] -dev = [ - "pact-python[types]", - "pact-python[test]", - "black ==24.1.1", - "ruff ==0.1.14", -] +dev = ["pact-python[types]", "pact-python[test]", "ruff==0.1.14"] ################################################################################ ## Hatch Build Configuration @@ -127,10 +122,14 @@ extra-dependencies = [ ] [tool.hatch.envs.default.scripts] -lint = ["black --check --diff {args:.}", "ruff {args:.}", "mypy {args:.}"] -test = "pytest {args:tests/}" -example = "pytest examples/ {args}" -all = ["lint", "test", "example"] +lint = "ruff check --show-source --show-fixes {args}" +typecheck = "mypy {args:.}" +format = "ruff format --diff {args}" +test = "pytest tests/ {args}" +example = "pytest examples/ {args}" +all = ["format", "lint", "typecheck", "test", "example"] +docs = ["mkdocs serve {args}"] +docs-build = ["mkdocs build {args}"] # Test environment for running unit tests. This automatically tests against all # supported Python versions. @@ -140,11 +139,6 @@ features = ["test"] [[tool.hatch.envs.test.matrix]] python = ["3.8", "3.9", "3.10", "3.11", "3.12"] -[tool.hatch.envs.test.scripts] -test = "pytest {args:tests/}" -example = "pytest examples/ {args}" -all = ["test", "example"] - ################################################################################ ## PyTest Configuration ################################################################################ @@ -186,17 +180,6 @@ exclude_lines = [ [tool.ruff] target-version = "py38" -select = ["ALL"] - -ignore = [ - "D200", # Require single line docstrings to be on one line. - "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 - "FIX002", # Forbid TODO in comments - "TD002", # Assign someone to 'TODO' comments -] # TODO: Remove the explicity extend-exclude once astral-sh/ruff#6262 is fixed. # https://github.com/pact-foundation/pact-python/issues/458 @@ -236,19 +219,38 @@ extend-exclude = [ "tests/test_verify_wrapper.py", ] -[tool.ruff.pyupgrade] +[tool.ruff.lint] +select = ["ALL"] + +ignore = [ + "D200", # Require single line docstrings to be on one line. + "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 + "FIX002", # Forbid TODO in comments + "TD002", # Assign someone to 'TODO' comments + + # The following are disabled for compatibility with the formatter + "COM812", # enforce trailing commas + "ISC001", # require imports to be sorted +] + +[tool.ruff.lint.pyupgrade] keep-runtime-typing = true -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "google" -################################################################################ -## Black Configuration -################################################################################ +[tool.ruff.isort] +known-first-party = ["pact"] + +[tool.ruff.flake8-tidy-imports] +ban-relative-imports = "all" -[tool.black] -target-version = ["py38"] -extend-exclude = '^/(pact|tests)/(?!v3).+\.py$' +[tool.ruff.format] +preview = true +docstring-code-format = true ################################################################################ ## Mypy Configuration diff --git a/tests/v3/compatiblity_suite/test_v1_consumer.py b/tests/v3/compatiblity_suite/test_v1_consumer.py index e402bb682..8427e07fd 100644 --- a/tests/v3/compatiblity_suite/test_v1_consumer.py +++ b/tests/v3/compatiblity_suite/test_v1_consumer.py @@ -9,11 +9,11 @@ import pytest import requests -from pact.v3 import Pact from pytest_bdd import given, parsers, scenario, then, when from yarl import URL -from .util import ( # type: ignore[import-untyped] +from pact.v3 import Pact +from tests.v3.compatiblity_suite.util import ( FIXTURES_ROOT, InteractionDefinition, string_to_int, diff --git a/tests/v3/test_async_interaction.py b/tests/v3/test_async_interaction.py index 64e215481..3dc9a0b32 100644 --- a/tests/v3/test_async_interaction.py +++ b/tests/v3/test_async_interaction.py @@ -7,6 +7,7 @@ import re import pytest + from pact.v3 import Pact diff --git a/tests/v3/test_ffi.py b/tests/v3/test_ffi.py index 496240e1c..4886d0ccd 100644 --- a/tests/v3/test_ffi.py +++ b/tests/v3/test_ffi.py @@ -9,6 +9,7 @@ import re import pytest + from pact.v3 import ffi diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py index 51a6134d0..c328b18e3 100644 --- a/tests/v3/test_http_interaction.py +++ b/tests/v3/test_http_interaction.py @@ -10,6 +10,7 @@ import aiohttp import pytest + from pact.v3 import Pact if TYPE_CHECKING: diff --git a/tests/v3/test_pact.py b/tests/v3/test_pact.py index e919e760a..56a6d1f58 100644 --- a/tests/v3/test_pact.py +++ b/tests/v3/test_pact.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Literal import pytest + from pact.v3 import Pact if TYPE_CHECKING: diff --git a/tests/v3/test_sync_interaction.py b/tests/v3/test_sync_interaction.py index 64e215481..3dc9a0b32 100644 --- a/tests/v3/test_sync_interaction.py +++ b/tests/v3/test_sync_interaction.py @@ -7,6 +7,7 @@ import re import pytest + from pact.v3 import Pact From 46802e9157caef1bb17f8469865fef6e46a9eb30 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 30 Jan 2024 10:43:36 +1100 Subject: [PATCH 0170/1376] chore(dev): add markdownlint pre-commit Markdownlint checks Markdown files for simple issues and formatting. The markdownlint configuration ensures consistency with the existing editorconfig. Signed-off-by: JP-Ellis --- .markdownlint.yml | 7 ++ .pre-commit-config.yaml | 10 +++ CONTRIBUTING.md | 6 +- README.md | 148 +++++++++++++++++++--------------------- RELEASING.md | 14 ++-- docker/README.md | 2 +- examples/README.md | 4 ++ 7 files changed, 104 insertions(+), 87 deletions(-) create mode 100644 .markdownlint.yml diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 000000000..738e6fc13 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,7 @@ +default: true +list-marker-space: + ul_single: 3 + ul_multi: 3 + ol_single: 2 + ol_multi: 2 +line-length: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 63df0509a..91abffcd9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,3 +65,13 @@ repos: types: [python] exclude: ^(pact|tests)/(?!v3/).*\.py$ stages: [pre-push] + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.38.0 + hooks: + - id: markdownlint + exclude: | + (?x)^( + .github/PULL_REQUEST_TEMPLATE\.md + | CHANGELOG.md + ) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47a5a07bb..b49f5a069 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -131,9 +131,9 @@ Working on your first Pull Request? You can learn how from this free video serie Please make sure the following is done when submitting a pull request: -1. **Keep your PR small.** Small pull requests (~300 lines of diff) are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. -2. **Use descriptive titles.** It is recommended to follow this [commit message style](#semantic-commit-messages). -3. **Test your changes.** Describe your [**test plan**](#test-plan) in your pull request description. +1. **Keep your PR small.** Small pull requests (~300 lines of diff) are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. +2. **Use descriptive titles.** It is recommended to follow this [commit message style](#conventional-commit-messages). +3. **Test your changes.** Describe your [**test plan**](#test-plan) in your pull request description. All pull requests should be opened against the `master` branch. diff --git a/README.md b/README.md index a4640c5e5..47d0a1a71 100644 --- a/README.md +++ b/README.md @@ -10,28 +10,28 @@ For more information about what Pact is, and how it can help you test your code Note: As of Version 1.0 deprecates support for python 2.7 to allow us to incorporate python 3.x features more readily. If you want to still use Python 2.7 use the 0.x.y versions. Only bug fixes will now be added to that release. -# How to use pact-python +## How to use pact-python -## Installation +### Installation -``` +```console pip install pact-python ``` -## Getting started +### Getting started A guide follows but if you go to the [examples](https://github.com/pact-foundation/pact-python/tree/master/examples). This has a consumer, provider and pact-broker set of tests for both FastAPI and Flask. -## Writing a Pact +### Writing a Pact Creating a complete contract is a two step process: -1. Create a test on the consumer side that declares the expectations it has of the provider -2. Create a provider state that allows the contract to pass when replayed against the provider +1. Create a test on the consumer side that declares the expectations it has of the provider +2. Create a provider state that allows the contract to pass when replayed against the provider -## Writing the Consumer Test +### Writing the Consumer Test If we have a method that communicates with one of our external services, which we'll call `Provider`, and our product, `Consumer` is hitting an endpoint on `Provider` at `/users/` to get information about a particular user. @@ -104,7 +104,7 @@ Using the Pact object as a [context manager], we call our method under test whic pact.verify() ``` -### Requests +#### Requests When defining the expected HTTP request that your code is expected to make you can specify the method, path, body, headers, and query: @@ -146,11 +146,11 @@ The mock service offers you several important features when building your contra - If a request is made that does not match one you defined or if a request from your code is missing it will return an error with details. - Finally, it will record your contracts as a JSON file that you can store in your repository or publish to a Pact broker. -## Expecting Variable Content +### Expecting Variable Content The above test works great if that user information is always static, but what happens if the user has a last updated field that is set to the current time every time the object is modified? To handle variable data and make your tests more robust, there are 3 helpful matchers: -### Term(matcher, generate) +#### Term(matcher, generate) Asserts the value should match the given regular expression. You could use this to expect a timestamp with a particular format in the request or response where you know you need a particular format, but are unconcerned about the exact date: @@ -171,7 +171,7 @@ body = { When you run the tests for the consumer, the mock service will return the value you provided as `generate`, in this case `2016-12-15T20:16:01`. When the contract is verified on the provider, the regex will be used to search the response from the real provider service and the test will be considered successful if the regex finds a match in the response. -### Like(matcher) +#### Like(matcher) Asserts the element's type matches the matcher. For example: @@ -199,7 +199,7 @@ Like({ ``` -### EachLike(matcher, minimum=1) +#### EachLike(matcher, minimum=1) Asserts the value is an array type that consists of elements like the one passed in. It can be used to assert simple arrays: @@ -222,6 +222,8 @@ EachLike({ > Note, you do not need to specify everything that will be returned from the Provider in a JSON response, any extra data that is received will be ignored and the tests will still pass. + + > Note, to get the generated values from an object that can contain matchers like Term, Like, EachLike, etc. for assertion in self.assertEqual(result, expected) you may need to use get_generated_values() helper function: ```python @@ -229,7 +231,7 @@ from pact.matchers import get_generated_values self.assertEqual(result, get_generated_values(expected)) ``` -### Match common formats +#### Match common formats Often times, you find yourself having to re-write regular expressions for common formats. @@ -271,30 +273,30 @@ Like({ For more information see [Matching](https://docs.pact.io/getting_started/matching) -## Uploading pact files to a Pact Broker +### Uploading pact files to a Pact Broker There are two ways to publish your pact files, to a Pact Broker. -1. [Pact CLI tools](https://docs.pact.io/pact_broker/client_cli) **recommended** -2. Pact Python API +1. [Pact CLI tools](https://docs.pact.io/pact_broker/client_cli) **recommended** +2. Pact Python API -### CLI +#### Broker CLI See [Publishing and retrieving pacts](https://docs.pact.io/pact_broker/publishing_and_retrieving_pacts) Example uploading to a Pact Broker -``` +```console pact-broker publish /path/to/pacts/consumer-provider.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-username someUsername --broker-password somePassword ``` Example uploading to a PactFlow Broker -``` +```console pact-broker publish /path/to/pacts/consumer-provider.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-token SomeToken ``` -### Python API +#### Broker Python API ```python broker = Broker(broker_base_url="http://localhost") @@ -319,11 +321,11 @@ The parameters for this differ slightly in naming from their CLI equivalents: | `--consumer-app-version` | `version` | | `n/a` | `consumer_name` | -## Verifying Pacts Against a Service +### Verifying Pacts Against a Service In addition to writing Pacts for Python consumers, you can also verify those Pacts against a provider of any language. There are two ways to do this. -### CLI +#### Verifier CLI After installing pact-python a `pact-verifier` application should be available. To get details about its use you can call it with the help argument: @@ -341,80 +343,80 @@ Which will immediately invoke the Pact verifier, making HTTP requests to the ser There are several options for configuring how the Pacts are verified: -###### --provider-base-url +- **`--provider-base-url`** -Required. Defines the URL of the server to make requests to when verifying the Pacts. + Required. Defines the URL of the server to make requests to when verifying the Pacts. -###### --pact-url +- **`--pact-url`** -Required if --pact-urls not specified. The location of a Pact file you want to verify. This can be a URL to a [Pact Broker] or a local path, to provide multiple files, specify multiple arguments. + Required if --pact-urls not specified. The location of a Pact file you want to verify. This can be a URL to a [Pact Broker] or a local path, to provide multiple files, specify multiple arguments. -``` -pact-verifier --provider-base-url=http://localhost:8080 --pact-url=./pacts/one.json --pact-url=./pacts/two.json -``` + ```console + pact-verifier --provider-base-url=http://localhost:8080 --pact-url=./pacts/one.json --pact-url=./pacts/two.json + ``` -###### --pact-urls +- **`--pact-urls`** -Required if --pact-url not specified. The location of the Pact files you want to verify. This can be a URL to a [Pact Broker] or one or more local paths, separated by a comma. + Required if --pact-url not specified. The location of the Pact files you want to verify. This can be a URL to a [Pact Broker] or one or more local paths, separated by a comma. -###### --provider-states-url +- **`--provider-states-url`** -_DEPRECATED AFTER v 0.6.0._ The URL where your provider application will produce the list of available provider states. The verifier calls this URL to ensure the Pacts specify valid states before making the HTTP requests. + _DEPRECATED AFTER v 0.6.0._ The URL where your provider application will produce the list of available provider states. The verifier calls this URL to ensure the Pacts specify valid states before making the HTTP requests. -###### --provider-states-setup-url +- **`--provider-states-setup-url`** -The URL which should be called to setup a specific provider state before a Pact is verified. This URL will be called with a POST request, and the JSON body `{consumer: 'Consumer name', state: 'a thing exists'}`. + The URL which should be called to setup a specific provider state before a Pact is verified. This URL will be called with a POST request, and the JSON body `{consumer: 'Consumer name', state: 'a thing exists'}`. -###### --pact-broker-url +- **`--pact-broker-url`** -Base URl for the Pact Broker instance to publish pacts to. Can also be specified via the environment variable `PACT_BROKER_BASE_URL`. + Base URl for the Pact Broker instance to publish pacts to. Can also be specified via the environment variable `PACT_BROKER_BASE_URL`. -###### --pact-broker-username +- **`--pact-broker-username`** -The username to use when contacting the Pact Broker. Can also be specified via the environment variable `PACT_BROKER_USERNAME`. + The username to use when contacting the Pact Broker. Can also be specified via the environment variable `PACT_BROKER_USERNAME`. -###### --pact-broker-password +- **`--pact-broker-password`** -The password to use when contacting the Pact Broker. You can also specify this value as the environment variable `PACT_BROKER_PASSWORD`. + The password to use when contacting the Pact Broker. You can also specify this value as the environment variable `PACT_BROKER_PASSWORD`. -###### --pact-broker-token +- **`--pact-broker-token`** -The bearer token to use when contacting the Pact Broker. You can also specify this value as the environment variable `PACT_BROKER_TOKEN`. + The bearer token to use when contacting the Pact Broker. You can also specify this value as the environment variable `PACT_BROKER_TOKEN`. -###### --consumer-version-tag +- **`--consumer-version-tag`** -Retrieve the latest pacts with this consumer version tag. Used in conjunction with `--provider`. May be specified multiple times. + Retrieve the latest pacts with this consumer version tag. Used in conjunction with `--provider`. May be specified multiple times. -###### --consumer-version-selector +- **`--consumer-version-selector`** -You can also retrieve pacts with consumer version selector, a more flexible approach in specifying which pacts you need. May be specified multiple times. Read more about selectors [here](https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/). + You can also retrieve pacts with consumer version selector, a more flexible approach in specifying which pacts you need. May be specified multiple times. Read more about selectors [here](https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/). -###### --provider-version-tag +- **`--provider-version-tag`** -Tag to apply to the provider application version. May be specified multiple times. + Tag to apply to the provider application version. May be specified multiple times. -###### --provider-version-branch +- **`--provider-version-branch`** -Branch to apply to the provider application version. + Branch to apply to the provider application version. -###### --custom-provider-header +- **`--custom-provider-header`** -Header to add to provider state set up and pact verification requests e.g.`Authorization: Basic cGFjdDpwYWN0` -May be specified multiple times. + Header to add to provider state set up and pact verification requests e.g.`Authorization: Basic cGFjdDpwYWN0` + May be specified multiple times. -###### -t, --timeout +- **`-t, --timeout`** -The duration in seconds we should wait to confirm that the verification process was successful. Defaults to 30. + The duration in seconds we should wait to confirm that the verification process was successful. Defaults to 30. -###### -a, --provider-app-version +- **`-a, --provider-app-version`** -The provider application version. Required for publishing verification results. + The provider application version. Required for publishing verification results. -###### -r, --publish-verification-results +- **`-r, --publish-verification-results`** -Publish verification results to the broker. + Publish verification results to the broker. -### Python API +#### Verifier Python API You can use the Verifier class. This allows you to write native python code and the test framework of your choice. @@ -468,7 +470,7 @@ You can see more details in the examples - [Flask Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_01_provider_flask.py) - [FastAPI Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_01_provider_fastapi.py) -### Provider States +#### Provider States In many cases, your contracts will need very specific data to exist on the provider to pass successfully. If you are fetching a user profile, that user needs to exist, if querying a list of records, one or more records needs to exist. To support decoupling the testing of the consumer and provider, Pact offers the idea of provider states to communicate from the consumer what data should exist on the provider. @@ -480,7 +482,7 @@ When setting up the testing of a provider you will also need to setup the manage For more information about provider states, refer to the [Pact documentation] on [Provider States]. -# Development +## Development @@ -488,8 +490,8 @@ Please read [CONTRIBUTING.md](https://github.com/pact-foundation/pact-python/blo To setup a development environment: -1. If you want to run tests for all Python versions, install 2.7, 3.3, 3.4, 3.5, and 3.6 from source or using a tool like [pyenv] -2. Its recommended to create a Python [virtualenv] for the project +1. If you want to run tests for all Python versions, install 2.7, 3.3, 3.4, 3.5, and 3.6 from source or using a tool like [pyenv] +2. Its recommended to create a Python [virtualenv] for the project To setup the environment, run tests, and package the application, run: `make release` @@ -499,17 +501,17 @@ This creates a `dist/pact-python-N.N.N.tar.gz` file, where the Ns are the curren `pip install ./dist/pact-python-N.N.N.tar.gz` -## Offline Installation of Standalone Packages +### Offline Installation of Standalone Packages Although all Ruby standalone applications are predownloaded into the wheel artifact, it may be useful, for development, purposes to install custom Ruby binaries. In which case, use the `bin-path` flag. -``` +```console pip install pact-python --bin-path=/absolute/path/to/folder/containing/pact/binaries/for/your/os ``` Pact binaries can be found at [Pact Ruby Releases](https://github.com/pact-foundation/pact-ruby-standalone/releases). -## Testing +### Testing This project has unit and end to end tests, which can both be run from make: @@ -517,7 +519,7 @@ Unit: `make test` End to end: `make e2e` -## Contact +### Contact Join us in slack: [![slack](https://slack.pact.io/badge.svg)](https://slack.pact.io) @@ -526,16 +528,10 @@ or - Twitter: [@pact_up](https://twitter.com/pact_up) - Stack Overflow: [stackoverflow.com/questions/tagged/pact](https://stackoverflow.com/questions/tagged/pact) -[bundler]: http://bundler.io/ [context manager]: https://en.wikibooks.org/wiki/Python_Programming/Context_Managers -[Pact]: https://docs.pact.io [Pact Broker]: https://docs.pact.io/pact_broker [Pact documentation]: https://docs.pact.io/ -[Pact Mock Service]: https://github.com/pact-foundation/pact-mock_service [Pact specification]: https://github.com/pact-foundation/pact-specification [Provider States]: https://docs.pact.io/getting_started/provider_states -[pact-provider-verifier]: https://github.com/pact-foundation/pact-provider-verifier [pyenv]: https://github.com/pyenv/pyenv -[rvm]: https://rvm.io/ -[rbenv]: https://github.com/rbenv/rbenv [virtualenv]: http://python-guide-pt-br.readthedocs.io/en/latest/dev/virtualenvs/ diff --git a/RELEASING.md b/RELEASING.md index 43b72f594..5d04b574c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -5,7 +5,7 @@ The easiest way is to just run the following command from the root folder with the HEAD commit on trunk and the appropriate version. We follow `..` versioning. ```shell -$ script/release_prep.sh X.Y.Z +script/release_prep.sh X.Y.Z ``` This script effectively runs the following: @@ -15,26 +15,26 @@ This script effectively runs the following: 2. Update the `CHANGELOG.md` using: ```shell - $ git log --pretty=format:' * %h - %s (%an, %ad)' vX.Y.Z..HEAD + git log --pretty=format:' * %h - %s (%an, %ad)' vX.Y.Z..HEAD ``` 3. Add files to git ```shell - $ git add CHANGELOG.md pact/__version__.py + git add CHANGELOG.md pact/__version__.py ``` 4. Commit ```shell - $ git commit -m "Releasing version X.Y.Z" + git commit -m "Releasing version X.Y.Z" ``` 5. Tag ```shell - $ git tag -a vX.Y.Z -m "Releasing version X.Y.Z" - $ git push origin master --tags + git tag -a vX.Y.Z -m "Releasing version X.Y.Z" + git push origin master --tags ``` ## Updating Pact Ruby @@ -43,7 +43,7 @@ To upgrade the versions of `pact-mock_service` and `pact-provider-verifier`, cha ## Publishing to pypi -1. Wait until GitHub Actions have run and the new tag is available at https://github.com/pact-foundation/pact-python/releases/tag/vX.Y.Z +1. Wait until GitHub Actions have run and the new tag is available at `https://github.com/pact-foundation/pact-python/releases/tag/vX.Y.Z` 2. Set the title to `pact-python-X.Y.Z` diff --git a/docker/README.md b/docker/README.md index 50ddfe74c..dd220ac52 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,7 +2,7 @@ This is for contributors who want to make changes and test for all different versions of python currently supported. If you don't want to set up and install all the different python versions locally (and there are some difficulties with that) you can just run them in docker using containers. -# Setup +## Setup To build a container say for Python 3.11, change to the root directory of the project and run: diff --git a/examples/README.md b/examples/README.md index 4a91ca5f5..ff4cfecdd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,6 +16,7 @@ Pact is a contract testing tool. Contract testing is a way to ensure that servic An interaction between a HTTP client (the _consumer_) and a server (the _provider_) would typically look like this: +
```mermaid @@ -29,11 +30,13 @@ sequenceDiagram ```
+ To test this interaction naively would require both the consumer and provider to be running at the same time. While this is straightforward in the above example, this quickly becomes impractical as the number of interactions grows between many microservices. Pact solves this by allowing the consumer and provider to be tested independently. Pact achieves this be mocking the other side of the interaction: +
```mermaid @@ -60,6 +63,7 @@ sequenceDiagram ```
+ In the first stage, the consumer defines a number of interactions in the form below. Pact sets up a mock server that will respond to the requests as defined by the consumer. All these interactions, containing both the request and expected response, are all sent to the Pact Broker. From 96795e0c7e034eab7ad885f0f7d35ee969b81f6b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 00:55:42 +0000 Subject: [PATCH 0171/1376] chore(deps): update dependency dev/ruff to v0.1.15 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d1fe60f1e..d667abe32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ test = [ "pytest-cov ~= 4.0", "testcontainers ~= 3.0", ] -dev = ["pact-python[types]", "pact-python[test]", "ruff==0.1.14"] +dev = ["pact-python[types]", "pact-python[test]", "ruff==0.1.15"] ################################################################################ ## Hatch Build Configuration From 0004d88466afaf7431701fc5ce2942ada9f9234c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 00:56:17 +0000 Subject: [PATCH 0172/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.1.15 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91abffcd9..d2896a84f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.14 + rev: v0.1.15 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 279db5a2a8543244a71b8bc97b8138dcc46870d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 00:55:46 +0000 Subject: [PATCH 0173/1376] chore(deps): update pre-commit hook igorshubovych/markdownlint-cli to v0.39.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d2896a84f..1ce73bad2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -67,7 +67,7 @@ repos: stages: [pre-push] - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.38.0 + rev: v0.39.0 hooks: - id: markdownlint exclude: | From 389d755b8e0198bfbb9283e1eacdba62a2fbe153 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 30 Jan 2024 13:25:19 +1100 Subject: [PATCH 0174/1376] chore(ci): fix pypy linux builds The test segment of the CIBuildWheel process was failing due to Rust not being available for the installation/building of `pydantic_core`. This should resolve the issue by installing Rust in the Docker image just before the test step. This is only done on pypy Linux builds, as the other interpreters and platforms seem to be working fine. Signed-off-by: JP-Ellis --- pyproject.toml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d667abe32..111683ddb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -276,3 +276,14 @@ assert isinstance(pact.v3.ffi.version(), str);\"""" # The repair tool unfortunately did not like the bundled Ruby distributable. # TODO: Check whether delocate-wheel can be configured. repair-wheel-command = "" + +[[tool.cibuildwheel.overrides]] +# Pydantic for pypy needs to be built from source, which requires Rust. +select = "pp*-*linux*" +before-test = """ +curl -sSf https://sh.rustup.rs -sSf | sh -s -- --profile=minimal -y +for bin in $(ls "$HOME/.cargo/bin"); do + ln -v "$HOME/.cargo/bin/$bin" "/usr/bin/$bin" +done +rustup show +""" From a492c3a907911537c3098081c8d2236a266aff7c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 19:56:57 +0000 Subject: [PATCH 0175/1376] chore(deps): update codecov/codecov-action digest to ab904c4 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0861ce59e..ef4b7cb25 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,7 +56,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@4fe8c5f003fae66aa5ebb77cfd3e7bfbbda0b6b0 # v3 + uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457 # v3 with: token: ${{ secrets.CODECOV_TOKEN }} From 43c5c8279b66d61ad9c0fc8357f327f576ec81c5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 31 Jan 2024 10:48:06 +1100 Subject: [PATCH 0176/1376] docs: fix typos Thanks to @hwong557 for reporting these typos. Resolves: #511 Signed-off-by: JP-Ellis --- pact/consumer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pact/consumer.py b/pact/consumer.py index 57281234d..6e0f6b5af 100644 --- a/pact/consumer.py +++ b/pact/consumer.py @@ -13,7 +13,7 @@ class Consumer(object): >>> from pact import Consumer, Provider >>> consumer = Consumer('my-web-front-end') - >>> consumer.has_pact_with(Provider('my-backend-serivce')) + >>> consumer.has_pact_with(Provider('my-backend-service')) """ def __init__(self, name, service_cls=Pact, tags=None, @@ -79,7 +79,7 @@ def has_pact_with(self, provider, host_name='localhost', port=1234, >>> from pact import Consumer, Provider >>> consumer = Consumer('my-web-front-end') >>> consumer.has_pact_with( - ... Provider('my-backend-serivce'), + ... Provider('my-backend-service'), ... host_name='192.168.1.1', ... port=8000) @@ -91,7 +91,7 @@ def has_pact_with(self, provider, host_name='localhost', port=1234, `localhost`. :type host_name: str :param port: The TCP port to use when contacting the Pact mock service. - This will need to tbe the same port used by your code under test + This will need to be the same port used by your code under test to contact the mock service. It defaults to: 1234 :type port: int :param log_dir: The directory where logs should be written. Defaults to From e50c59a9b212539a6624c7644d524b4cada0258f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 18:16:57 +0000 Subject: [PATCH 0177/1376] chore(deps): update codecov/codecov-action action to v4 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef4b7cb25..52866385a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,7 +56,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457 # v3 + uses: codecov/codecov-action@f30e4959ba63075080d4f7f90cacc18d9f3fafd7 # v4 with: token: ${{ secrets.CODECOV_TOKEN }} From 7d112f3ed34d780e495a5c02f7eb74010c3e474e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 00:15:56 +0000 Subject: [PATCH 0178/1376] chore(deps): update pypa/cibuildwheel action to v2.16.5 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b9cde84f9..1eb9ad581 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,7 +75,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@0b04ab1040366101259658b355777e4ff2d16f83 # v2.16.4 + uses: pypa/cibuildwheel@ce3fb7832089eb3e723a0a99cab7f3eaccf074fd # v2.16.5 env: CIBW_ARCHS: ${{ matrix.archs }} @@ -117,7 +117,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@0b04ab1040366101259658b355777e4ff2d16f83 # v2.16.4 + uses: pypa/cibuildwheel@ce3fb7832089eb3e723a0a99cab7f3eaccf074fd # v2.16.5 env: CIBW_ARCHS: ${{ matrix.archs }} From 3c4ad6e510f7a84a67da42caf9ee6cf3a956bb7d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 21:00:36 +0000 Subject: [PATCH 0179/1376] chore(deps): update codecov/codecov-action digest to e0b68c6 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52866385a..796600cb1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,7 +56,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@f30e4959ba63075080d4f7f90cacc18d9f3fafd7 # v4 + uses: codecov/codecov-action@e0b68c6749509c5f83f984dd99a76a1c1a231044 # v4 with: token: ${{ secrets.CODECOV_TOKEN }} From 3288fb62ccb27347851d64549085c97f670da425 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 21:00:40 +0000 Subject: [PATCH 0180/1376] chore(deps): update ubuntu:22.04 docker digest to e9569c2 --- Dockerfile.ubuntu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 7c31ccdec..5d7722897 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ubuntu:22.04@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 +FROM ubuntu:22.04@sha256:e9569c25505f33ff72e88b2990887c9dcf230f23259da296eb814fc2b41af999 ENV DEBIAN_FRONTEND=noninteractive ARG PYTHON_VERSION 3.9 From 641868e4722ed5ba2a6f57ee595e060933a12f1e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 4 Feb 2024 11:16:31 +0000 Subject: [PATCH 0181/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.14.1 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1ce73bad2..42f703f6a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.13.0 + rev: v3.14.1 hooks: - id: commitizen stages: [commit-msg] From 639055b59398ffa8ceba247c0335cec5a279dd15 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 21:57:53 +0000 Subject: [PATCH 0182/1376] chore(deps): update pactfoundation/pact-broker:latest docker digest to a341c6a --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 796600cb1..15d2bcc6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,7 +78,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:9cdd475459ee608b8ab4c32ad72e7aa9236bc5072c123fe689bdc3f44e1ea8dc + image: pactfoundation/pact-broker:latest@sha256:a341c6af670704b3d0a20a15fcefba62850d915eb9c9dde5e3334879a4541a54 ports: - "9292:9292" env: From 39c59c8b2287d945471bdac16676633d5ed55b1e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:52:59 +0000 Subject: [PATCH 0183/1376] chore(deps): update actions/upload-artifact digest to 5d5d22a --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1eb9ad581..c6dfb1938 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4 with: name: wheels-sdist path: ./dist/*.tar.* @@ -80,7 +80,7 @@ jobs: CIBW_ARCHS: ${{ matrix.archs }} - name: Upload wheels - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl @@ -122,7 +122,7 @@ jobs: CIBW_ARCHS: ${{ matrix.archs }} - name: Upload wheels - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl From afd702436cfefff93dd01af8e18bacb31dbb9f72 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 00:37:39 +0000 Subject: [PATCH 0184/1376] chore(deps): update actions/download-artifact digest to eaceaf8 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c6dfb1938..8ec20af63 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,7 +145,7 @@ jobs: with: python-version: ${{ env.STABLE_PYTHON_VERSION }} - - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4 + - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4 with: path: wheelhouse @@ -166,7 +166,7 @@ jobs: id-token: write steps: - - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4 + - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4 with: path: wheels From 357a4140f4cdf866cc80d3bdc83acdd7a48354c6 Mon Sep 17 00:00:00 2001 From: Jakub STOLARSKI Date: Wed, 7 Feb 2024 02:11:58 +0100 Subject: [PATCH 0185/1376] fix: clean pact interactions on exception If an exception is raised within a Pact context, the Pact is left in an unclean state resulting in subsequent Pacts failing. This is an issue if the exception is expected and handled separately. Ref: #533 --- pact/pact.py | 1 + tests/test_pact.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/pact/pact.py b/pact/pact.py index c03e02ae2..5906a830a 100644 --- a/pact/pact.py +++ b/pact/pact.py @@ -383,6 +383,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): Calls the mock service to verify that all interactions occurred as expected, and has it write out the contracts to disk. """ + self._interactions = [] if (exc_type, exc_val, exc_tb) != (None, None, None): return diff --git a/tests/test_pact.py b/tests/test_pact.py index 76e1e7059..7316f1cdb 100644 --- a/tests/test_pact.py +++ b/tests/test_pact.py @@ -562,6 +562,22 @@ def test_context_raises_error(self): self.mock_setup.assert_called_once_with(pact) self.assertFalse(self.mock_verify.called) + def test_does_not_leave_interactions_after_exception(self): + pact = Pact(self.consumer, self.provider) + (pact + .given('I am creating a new pact using the Pact class') + .upon_receiving('a specific request to the server') + .with_request('GET', '/path') + .will_respond_with(200, body='success')) + with self.assertRaises(RuntimeError): + with pact: + raise RuntimeError + + assert pact._interactions == [] + + + + class PactContextManagerSetupTestCase(PactTestCase): def test_definition_without_description(self): From 09bc6c8675eb6e90db806ac3a803648dc8f406eb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 00:37:49 +0000 Subject: [PATCH 0186/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.2.1 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 42f703f6a..dd5638888 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.15 + rev: v0.2.1 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From afb8c0eebe753d840bf7e1e1304d5a6812465dcd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 00:37:44 +0000 Subject: [PATCH 0187/1376] chore(deps): update dependency dev/ruff to v0.2.1 Signed-off-by: JP-Ellis --- examples/.ruff.toml | 3 ++- hatch_build.py | 4 ++-- pyproject.toml | 10 +++++----- tests/ruff.toml | 2 ++ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/examples/.ruff.toml b/examples/.ruff.toml index 94028649c..72322dfb1 100644 --- a/examples/.ruff.toml +++ b/examples/.ruff.toml @@ -1,12 +1,13 @@ extend = "../pyproject.toml" +[lint] ignore = [ "S101", # Forbid assert statements "D103", # Require docstring in public function "D104", # Require docstring in public package ] -[per-file-ignores] +[lint.per-file-ignores] "tests/**.py" = [ "INP001", # Forbid implicit namespaces ] diff --git a/hatch_build.py b/hatch_build.py index c289baffd..0e053298e 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -193,11 +193,11 @@ def _pact_bin_extract(self, artifact: Path) -> None: """ if str(artifact).endswith(".zip"): with zipfile.ZipFile(artifact) as f: - f.extractall(ROOT_DIR) + f.extractall(ROOT_DIR) # noqa: S202 if str(artifact).endswith(".tar.gz"): with tarfile.open(artifact) as f: - f.extractall(ROOT_DIR) + f.extractall(ROOT_DIR) # noqa: S202 # Cleanup the extract `README.md` (ROOT_DIR / "pact" / "README.md").unlink() diff --git a/pyproject.toml b/pyproject.toml index 111683ddb..1ae52b295 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ test = [ "pytest-cov ~= 4.0", "testcontainers ~= 3.0", ] -dev = ["pact-python[types]", "pact-python[test]", "ruff==0.1.15"] +dev = ["pact-python[types]", "pact-python[test]", "ruff==0.2.1"] ################################################################################ ## Hatch Build Configuration @@ -122,9 +122,9 @@ extra-dependencies = [ ] [tool.hatch.envs.default.scripts] -lint = "ruff check --show-source --show-fixes {args}" +lint = "ruff check --output-format=full --show-fixes {args}" typecheck = "mypy {args:.}" -format = "ruff format --diff {args}" +format = "ruff format {args}" test = "pytest tests/ {args}" example = "pytest examples/ {args}" all = ["format", "lint", "typecheck", "test", "example"] @@ -242,10 +242,10 @@ keep-runtime-typing = true [tool.ruff.lint.pydocstyle] convention = "google" -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["pact"] -[tool.ruff.flake8-tidy-imports] +[tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" [tool.ruff.format] diff --git a/tests/ruff.toml b/tests/ruff.toml index 164732ac2..70fc74680 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -1,4 +1,6 @@ extend = "../pyproject.toml" + +[lint] ignore = [ "D103", # Require docstrings on public functions "INP001", # Forbid implicit namespaces From b3ab0a395aa085d1cc523679cd689e9f17aa332c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 7 Feb 2024 13:54:32 +1100 Subject: [PATCH 0188/1376] chore(deps): upgrade pact ruby standalone to 2.1.0 Signed-off-by: JP-Ellis --- hatch_build.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hatch_build.py b/hatch_build.py index 0e053298e..31c3b3716 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -29,7 +29,9 @@ ROOT_DIR = Path(__file__).parent.resolve() -PACT_BIN_VERSION = os.getenv("PACT_BIN_VERSION", "2.0.7") +# Latest version available at: +# https://github.com/pact-foundation/pact-ruby-standalone/releases +PACT_BIN_VERSION = os.getenv("PACT_BIN_VERSION", "2.1.0") PACT_BIN_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.9") From 7f838b19a932350f18dd2ef263d0edcdb1e6c9de Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 7 Feb 2024 15:47:27 +1100 Subject: [PATCH 0189/1376] chore(deps): upgrade pact ffi to 0.4.15 Signed-off-by: JP-Ellis --- hatch_build.py | 4 +- pact/v3/ffi.py | 772 +++++++++++++++++++++++++++++++----------------- pact/v3/pact.py | 4 +- 3 files changed, 502 insertions(+), 278 deletions(-) diff --git a/hatch_build.py b/hatch_build.py index 31c3b3716..c6e52b91d 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -34,7 +34,9 @@ PACT_BIN_VERSION = os.getenv("PACT_BIN_VERSION", "2.1.0") PACT_BIN_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" -PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.9") +# Latest version available at: +# https://github.com/pact-foundation/pact-reference/releases +PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.15") PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{machine}.{ext}" diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index c0ee4b55c..6ea24cf97 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -127,7 +127,7 @@ class InteractionHandle: Handle to a HTTP Interaction. [Rust - `InteractionHandle`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/mock_server/handles/struct.InteractionHandle.html) + `InteractionHandle`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/mock_server/handles/struct.InteractionHandle.html) """ def __init__(self, ref: int) -> None: @@ -218,7 +218,7 @@ class PactHandle: Handle to a Pact. [Rust - `PactHandle`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/mock_server/handles/struct.PactHandle.html) + `PactHandle`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/mock_server/handles/struct.PactHandle.html) """ def __init__(self, ref: int) -> None: @@ -535,7 +535,7 @@ class ExpressionValueType(Enum): """ Expression Value Type. - [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/models/expressions/enum.ExpressionValueType.html) + [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/models/expressions/enum.ExpressionValueType.html) """ UNKNOWN = lib.ExpressionValueType_Unknown @@ -562,7 +562,7 @@ class GeneratorCategory(Enum): """ Generator Category. - [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/models/generators/enum.GeneratorCategory.html) + [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/models/generators/enum.GeneratorCategory.html) """ METHOD = lib.GeneratorCategory_METHOD @@ -590,7 +590,7 @@ class InteractionPart(Enum): """ Interaction Part. - [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/mock_server/handles/enum.InteractionPart.html) + [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/mock_server/handles/enum.InteractionPart.html) """ REQUEST = lib.InteractionPart_Request @@ -636,7 +636,7 @@ class MatchingRuleCategory(Enum): """ Matching Rule Category. - [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) + [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) """ METHOD = lib.MatchingRuleCategory_METHOD @@ -665,7 +665,7 @@ class PactSpecification(Enum): """ Pact Specification. - [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/models/pact_specification/enum.PactSpecification.html) + [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/models/pact_specification/enum.PactSpecification.html) """ UNKNOWN = lib.PactSpecification_Unknown @@ -697,7 +697,7 @@ class _StringResult(Enum): """ Internal enum from Pact FFI. - [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/mock_server/enum.StringResult.html) + [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/mock_server/enum.StringResult.html) """ FAILED = lib.StringResult_Failed @@ -844,7 +844,7 @@ def version() -> str: """ Return the version of the pact_ffi library. - [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_version) + [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_version) Returns: The version of the pact_ffi library as a string, in the form of `x.y.z`. @@ -864,7 +864,7 @@ def init(log_env_var: str) -> None: tracing subscriber. [Rust - `pactffi_init`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_init) + `pactffi_init`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_init) # Safety @@ -873,7 +873,7 @@ def init(log_env_var: str) -> None: raise NotImplementedError -def init_with_log_level(level: str) -> None: +def init_with_log_level(level: str = "INFO") -> None: """ Initialises logging, and sets the log level explicitly. @@ -881,7 +881,11 @@ def init_with_log_level(level: str) -> None: tracing subscriber. [Rust - `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_init_with_log_level) + `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_init_with_log_level) + + Args: + level: + One of TRACE, DEBUG, INFO, WARN, ERROR, NONE/OFF. Case-insensitive. # Safety @@ -897,7 +901,7 @@ def enable_ansi_support() -> None: On non-Windows platforms, this function is a no-op. [Rust - `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_enable_ansi_support) + `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_enable_ansi_support) # Safety @@ -915,7 +919,7 @@ def log_message( Log using the shared core logging facility. [Rust - `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_message) + `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_log_message) This is useful for callers to have a single set of logs. @@ -949,7 +953,7 @@ def match_message(msg_1: Message, msg_2: Message) -> Mismatches: If the messages match, the returned collection will be empty. [Rust - `pactffi_match_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_match_message) + `pactffi_match_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_match_message) """ raise NotImplementedError @@ -959,7 +963,7 @@ def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: Get an iterator over mismatches. [Rust - `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatches_get_iter) + `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatches_get_iter) """ raise NotImplementedError @@ -968,7 +972,7 @@ def mismatches_delete(mismatches: Mismatches) -> None: """ Delete mismatches. - [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatches_delete) + [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatches_delete) """ raise NotImplementedError @@ -977,7 +981,7 @@ def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: """ Get the next mismatch from a mismatches iterator. - [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatches_iter_next) + [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatches_iter_next) Returns a null pointer if no mismatches remain. """ @@ -988,7 +992,7 @@ def mismatches_iter_delete(iter: MismatchesIterator) -> None: """ Delete a mismatches iterator when you're done with it. - [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatches_iter_delete) + [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatches_iter_delete) """ raise NotImplementedError @@ -997,7 +1001,7 @@ def mismatch_to_json(mismatch: Mismatch) -> str: """ Get a JSON representation of the mismatch. - [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatch_to_json) + [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatch_to_json) """ raise NotImplementedError @@ -1006,7 +1010,7 @@ def mismatch_type(mismatch: Mismatch) -> str: """ Get the type of a mismatch. - [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatch_type) + [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatch_type) """ raise NotImplementedError @@ -1015,7 +1019,7 @@ def mismatch_summary(mismatch: Mismatch) -> str: """ Get a summary of a mismatch. - [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatch_summary) + [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatch_summary) """ raise NotImplementedError @@ -1024,7 +1028,7 @@ def mismatch_description(mismatch: Mismatch) -> str: """ Get a description of a mismatch. - [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatch_description) + [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatch_description) """ raise NotImplementedError @@ -1033,7 +1037,7 @@ def mismatch_ansi_description(mismatch: Mismatch) -> str: """ Get an ANSI-compatible description of a mismatch. - [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mismatch_ansi_description) + [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatch_ansi_description) """ raise NotImplementedError @@ -1043,7 +1047,7 @@ def get_error_message(length: int = 1024) -> str | None: Provide the error message from `LAST_ERROR` to the calling C code. [Rust - `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_get_error_message) + `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_get_error_message) This function should be called after any other function in the pact_matching FFI indicates a failure with its own error message, if the caller wants to @@ -1096,7 +1100,7 @@ def log_to_stdout(level_filter: LevelFilter) -> int: """ Convenience function to direct all logging to stdout. - [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_to_stdout) + [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_log_to_stdout) """ raise NotImplementedError @@ -1106,7 +1110,7 @@ def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: Convenience function to direct all logging to stderr. [Rust - `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_to_stderr) + `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_log_to_stderr) Args: level_filter: @@ -1130,7 +1134,7 @@ def log_to_file(file_name: str, level_filter: LevelFilter) -> int: Convenience function to direct all logging to a file. [Rust - `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_to_file) + `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_log_to_file) # Safety @@ -1144,7 +1148,7 @@ def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: """ Convenience function to direct all logging to a task local memory buffer. - [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_log_to_buffer) + [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_log_to_buffer) """ if isinstance(level_filter, str): level_filter = LevelFilter[level_filter.upper()] @@ -1158,7 +1162,7 @@ def logger_init() -> None: """ Initialize the FFI logger with no sinks. - [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_logger_init) + [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_logger_init) This initialized logger does nothing until `pactffi_logger_apply` has been called. @@ -1180,7 +1184,7 @@ def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: Attach an additional sink to the thread-local logger. [Rust - `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_logger_attach_sink) + `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_logger_attach_sink) This logger does nothing until `pactffi_logger_apply` has been called. @@ -1227,7 +1231,7 @@ def logger_apply() -> int: Apply the previously configured sinks and levels to the program. [Rust - `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_logger_apply) + `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_logger_apply) If no sinks have been setup, will set the log level to info and the target to standard out. @@ -1243,7 +1247,7 @@ def fetch_log_buffer(log_id: str) -> str: Fetch the in-memory logger buffer contents. [Rust - `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_fetch_log_buffer) + `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_fetch_log_buffer) This will only have any contents if the `buffer` sink has been configured to log to. The contents will be allocated on the heap and will need to be freed @@ -1269,7 +1273,7 @@ def parse_pact_json(json: str) -> Pact: Parses the provided JSON into a Pact model. [Rust - `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_parse_pact_json) + `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_parse_pact_json) The returned Pact model must be freed with the `pactffi_pact_model_delete` function when no longer needed. @@ -1286,7 +1290,7 @@ def pact_model_delete(pact: Pact) -> None: """ Frees the memory used by the Pact model. - [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_model_delete) + [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_model_delete) """ raise NotImplementedError @@ -1296,7 +1300,7 @@ def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: Returns an iterator over all the interactions in the Pact. [Rust - `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_model_interaction_iterator) + `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_model_interaction_iterator) The iterator will have to be deleted using the `pactffi_pact_interaction_iter_delete` function. The iterator will contain a @@ -1315,7 +1319,7 @@ def pact_spec_version(pact: Pact) -> PactSpecification: """ Returns the Pact specification enum that the Pact is for. - [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_spec_version) + [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_spec_version) """ raise NotImplementedError @@ -1324,7 +1328,7 @@ def pact_interaction_delete(interaction: PactInteraction) -> None: """ Frees the memory used by the Pact interaction model. - [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_delete) + [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_delete) """ raise NotImplementedError @@ -1333,7 +1337,7 @@ def async_message_new() -> AsynchronousMessage: """ Get a mutable pointer to a newly-created default message on the heap. - [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_new) + [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_new) # Safety @@ -1350,7 +1354,7 @@ def async_message_delete(message: AsynchronousMessage) -> None: """ Destroy the `AsynchronousMessage` being pointed to. - [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_delete) + [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_delete) """ raise NotImplementedError @@ -1360,7 +1364,7 @@ def async_message_get_contents(message: AsynchronousMessage) -> MessageContents: Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. [Rust - `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_contents) + `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_contents) # Safety @@ -1379,7 +1383,7 @@ def async_message_get_contents_str(message: AsynchronousMessage) -> str: """ Get the message contents of an `AsynchronousMessage` in string form. - [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_contents_str) + [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_contents_str) # Safety @@ -1406,7 +1410,7 @@ def async_message_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_set_contents_str) + `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_set_contents_str) - `message` - the message to set the contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -1434,7 +1438,7 @@ def async_message_get_contents_length(message: AsynchronousMessage) -> int: Get the length of the contents of a `AsynchronousMessage`. [Rust - `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_contents_length) + `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_contents_length) # Safety @@ -1453,7 +1457,7 @@ def async_message_get_contents_bin(message: AsynchronousMessage) -> str: Get the contents of an `AsynchronousMessage` as bytes. [Rust - `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_contents_bin) + `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_contents_bin) # Safety @@ -1480,7 +1484,7 @@ def async_message_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_set_contents_bin) + `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_set_contents_bin) * `message` - the message to set the contents for * `contents` - pointer to contents to copy from @@ -1507,7 +1511,7 @@ def async_message_get_description(message: AsynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_description) + `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_description) # Safety @@ -1533,7 +1537,7 @@ def async_message_set_description( """ Write the `description` field on the `AsynchronousMessage`. - [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_set_description) + [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_set_description) # Safety @@ -1558,7 +1562,7 @@ def async_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_provider_state) + `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_provider_state) # Safety @@ -1583,7 +1587,7 @@ def async_message_get_provider_state_iter( """ Get an iterator over provider states. - [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) + [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) # Safety @@ -1600,7 +1604,7 @@ def consumer_get_name(consumer: Consumer) -> str: r""" Get a copy of this consumer's name. - [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_consumer_get_name) + [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_consumer_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -1646,7 +1650,7 @@ def pact_get_consumer(pact: Pact) -> Consumer: `pactffi_pact_consumer_delete` when no longer required. [Rust - `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_get_consumer) + `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_get_consumer) # Errors @@ -1660,7 +1664,7 @@ def pact_consumer_delete(consumer: Consumer) -> None: """ Frees the memory used by the Pact consumer. - [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_consumer_delete) + [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_consumer_delete) """ raise NotImplementedError @@ -1669,7 +1673,7 @@ def message_contents_get_contents_str(contents: MessageContents) -> str: """ Get the message contents in string form. - [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_get_contents_str) + [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_get_contents_str) # Safety @@ -1696,7 +1700,7 @@ def message_contents_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_set_contents_str) + `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_set_contents_str) * `contents` - the message contents to set the contents for * `contents_str` - pointer to contents to copy from. Must be a valid @@ -1723,7 +1727,7 @@ def message_contents_get_contents_length(contents: MessageContents) -> int: """ Get the length of the message contents. - [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_get_contents_length) + [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_get_contents_length) # Safety @@ -1742,7 +1746,7 @@ def message_contents_get_contents_bin(contents: MessageContents) -> str: Get the contents of a message as a pointer to an array of bytes. [Rust - `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_get_contents_bin) + `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_get_contents_bin) # Safety @@ -1769,7 +1773,7 @@ def message_contents_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_set_contents_bin) + `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_set_contents_bin) * `message` - the message contents to set the contents for * `contents_bin` - pointer to contents to copy from @@ -1798,7 +1802,7 @@ def message_contents_get_metadata_iter( Get an iterator over the metadata of a message. [Rust - `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) + `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) The returned pointer must be deleted with `pactffi_message_metadata_iter_delete` when done with it. @@ -1829,7 +1833,7 @@ def message_contents_get_matching_rule_iter( Get an iterator over the matching rules for a category of a message. [Rust - `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) + `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -1869,7 +1873,7 @@ def request_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP request. - [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) + [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -1906,7 +1910,7 @@ def response_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP response. - [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) + [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -1944,7 +1948,7 @@ def message_contents_get_generators_iter( Get an iterator over the generators for a category of a message. [Rust - `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_contents_get_generators_iter) + `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -1969,7 +1973,7 @@ def request_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP request. [Rust - `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_request_contents_get_generators_iter) + `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_request_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -1994,7 +1998,7 @@ def response_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP response. [Rust - `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_response_contents_get_generators_iter) + `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_response_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -2019,7 +2023,7 @@ def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: any generator. [Rust - `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_parse_matcher_definition) + `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_parse_matcher_definition) The following are examples of matching rule definitions: @@ -2055,7 +2059,7 @@ def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: using the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matcher_definition_error) + `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matcher_definition_error) """ raise NotImplementedError @@ -2069,7 +2073,7 @@ def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matcher_definition_value) + `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matcher_definition_value) Note that different expressions values can have types other than a string. Use `pactffi_matcher_definition_value_type` to get the actual type of the @@ -2083,7 +2087,7 @@ def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: """ Frees the memory used by the result of parsing the matching definition expression. - [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matcher_definition_delete) + [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matcher_definition_delete) """ raise NotImplementedError @@ -2096,7 +2100,7 @@ def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Ge NULL pointer, otherwise returns the generator as a pointer. [Rust - `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matcher_definition_generator) + `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matcher_definition_generator) The generator pointer will be a valid pointer as long as `pactffi_matcher_definition_delete` has not been called on the definition. @@ -2115,7 +2119,7 @@ def matcher_definition_value_type( If there was an error parsing the expression, it will return Unknown. [Rust - `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matcher_definition_value_type) + `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matcher_definition_value_type) """ raise NotImplementedError @@ -2124,7 +2128,7 @@ def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: """ Free the iterator when you're done using it. - [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_iter_delete) + [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_iter_delete) """ raise NotImplementedError @@ -2139,7 +2143,7 @@ def matcher_definition_iter( `pactffi_matching_rule_iter_delete` function once done with it. [Rust - `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matcher_definition_iter) + `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matcher_definition_iter) If there was an error parsing the expression, this function will return a NULL pointer. @@ -2155,7 +2159,7 @@ def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: deleted but will be cleaned up when the iterator is deleted. [Rust - `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_iter_next) + `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_iter_next) Will return a NULL pointer when the iterator has advanced past the end of the list. @@ -2177,7 +2181,7 @@ def matching_rule_id(rule_result: MatchingRuleResult) -> int: Return the ID of the matching rule. [Rust - `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_id) + `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_id) The ID corresponds to the following rules: @@ -2223,7 +2227,7 @@ def matching_rule_value(rule_result: MatchingRuleResult) -> str: pointer. [Rust - `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_value) + `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_value) The associated values for the rules are: @@ -2271,7 +2275,7 @@ def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: Will return a NULL pointer if the matching rule result was a reference. [Rust - `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_pointer) + `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_pointer) # Safety @@ -2289,7 +2293,7 @@ def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: structure. I.e., [Rust - `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_reference_name) + `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_reference_name) ```json { @@ -2323,7 +2327,7 @@ def validate_datetime(value: str, format: str) -> None: `pactffi_get_error_message`. [Rust - `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_validate_datetime) + `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_validate_datetime) # Errors If the function receives a panic, it will return 2 and the message associated with the panic can be retrieved with `pactffi_get_error_message`. @@ -2351,7 +2355,7 @@ def generator_to_json(generator: Generator) -> str: Get the JSON form of the generator. [Rust - `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generator_to_json) + `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generator_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -2374,7 +2378,7 @@ def generator_generate_string(generator: Generator, context_json: str) -> str: function). [Rust - `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generator_generate_string) + `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generator_generate_string) If anything goes wrong, it will return a NULL pointer. """ @@ -2390,7 +2394,7 @@ def generator_generate_integer(generator: Generator, context_json: str) -> int: should be the values returned from the Provider State callback function). [Rust - `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generator_generate_integer) + `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generator_generate_integer) If anything goes wrong or the generator is not a type that can generate an integer value, it will return a zero value. @@ -2403,7 +2407,7 @@ def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generators_iter_delete) + `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generators_iter_delete) """ raise NotImplementedError @@ -2413,7 +2417,7 @@ def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePa Get the next path and generator out of the iterator, if possible. [Rust - `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generators_iter_next) + `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generators_iter_next) The returned pointer must be deleted with `pactffi_generator_iter_pair_delete`. @@ -2435,7 +2439,7 @@ def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: Free a pair of key and value returned from `pactffi_generators_iter_next`. [Rust - `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generators_iter_pair_delete) + `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generators_iter_pair_delete) """ raise NotImplementedError @@ -2444,7 +2448,7 @@ def sync_http_new() -> SynchronousHttp: """ Get a mutable pointer to a newly-created default interaction on the heap. - [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_new) + [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_new) # Safety @@ -2462,7 +2466,7 @@ def sync_http_delete(interaction: SynchronousHttp) -> None: Destroy the `SynchronousHttp` interaction being pointed to. [Rust - `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_delete) + `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_delete) """ raise NotImplementedError @@ -2472,7 +2476,7 @@ def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: Get the request of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_request) + `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_request) # Safety @@ -2492,7 +2496,7 @@ def sync_http_get_request_contents(interaction: SynchronousHttp) -> str: Get the request contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_request_contents) + `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_request_contents) # Safety @@ -2519,7 +2523,7 @@ def sync_http_set_request_contents( Sets the request contents of the interaction. [Rust - `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_set_request_contents) + `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_set_request_contents) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -2547,7 +2551,7 @@ def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: Get the length of the request contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) + `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) # Safety @@ -2566,7 +2570,7 @@ def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes: Get the request contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) + `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) # Safety @@ -2593,7 +2597,7 @@ def sync_http_set_request_contents_bin( Sets the request contents of the interaction as an array of bytes. [Rust - `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) + `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from @@ -2620,7 +2624,7 @@ def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: Get the response of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_response) + `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_response) # Safety @@ -2640,7 +2644,7 @@ def sync_http_get_response_contents(interaction: SynchronousHttp) -> str: Get the response contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_response_contents) + `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_response_contents) # Safety @@ -2668,7 +2672,7 @@ def sync_http_set_response_contents( Sets the response contents of the interaction. [Rust - `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_set_response_contents) + `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_set_response_contents) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -2696,7 +2700,7 @@ def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: Get the length of the response contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) + `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) # Safety @@ -2715,7 +2719,7 @@ def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes: Get the response contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) + `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) # Safety @@ -2742,7 +2746,7 @@ def sync_http_set_response_contents_bin( Sets the response contents of the `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) + `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from @@ -2769,7 +2773,7 @@ def sync_http_get_description(interaction: SynchronousHttp) -> str: Get a copy of the description. [Rust - `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_description) + `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_description) # Safety @@ -2793,7 +2797,7 @@ def sync_http_set_description(interaction: SynchronousHttp, description: str) -> Write the `description` field on the `SynchronousHttp`. [Rust - `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_set_description) + `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_set_description) # Safety @@ -2818,7 +2822,7 @@ def sync_http_get_provider_state( Get a copy of the provider state at the given index from this interaction. [Rust - `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_provider_state) + `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_provider_state) # Safety @@ -2844,7 +2848,7 @@ def sync_http_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) + `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) # Safety @@ -2869,7 +2873,7 @@ def pact_interaction_as_synchronous_http( longer required. [Rust - `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) + `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) # Safety This function is safe as long as the interaction pointer is a valid pointer. @@ -2888,7 +2892,7 @@ def pact_interaction_as_message(interaction: PactInteraction) -> Message: must be freed with `pactffi_message_delete` when no longer required. [Rust - `pactffi_pact_interaction_as_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_as_message) + `pactffi_pact_interaction_as_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_as_message) Note that if the interaction is a V4 `AsynchronousMessage`, it will be converted to a V3 `Message` before being returned. @@ -2913,7 +2917,7 @@ def pact_interaction_as_asynchronous_message( no longer required. [Rust - `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) + `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) Note that if the interaction is a V3 `Message`, it will be converted to a V4 `AsynchronousMessage` before being returned. @@ -2938,7 +2942,7 @@ def pact_interaction_as_synchronous_message( no longer required. [Rust - `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) + `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) # Safety This function is safe as long as the interaction pointer is a valid pointer. @@ -2953,7 +2957,7 @@ def pact_message_iter_delete(iter: PactMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_message_iter_delete) + `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_message_iter_delete) """ lib.pactffi_pact_message_iter_delete(iter._ptr) @@ -2963,7 +2967,7 @@ def pact_message_iter_next(iter: PactMessageIterator) -> Message: Get the next message from the message pact. [Rust - `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_message_iter_next) + `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_message_iter_next) """ ptr = lib.pactffi_pact_message_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -2977,7 +2981,7 @@ def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMes Get the next synchronous request/response message from the V4 pact. [Rust - `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_sync_message_iter_next) + `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_sync_message_iter_next) """ ptr = lib.pactffi_pact_sync_message_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -2991,7 +2995,7 @@ def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) + `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) """ lib.pactffi_pact_sync_message_iter_delete(iter._ptr) @@ -3001,7 +3005,7 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: Get the next synchronous HTTP request/response interaction from the V4 pact. [Rust - `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_sync_http_iter_next) + `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_sync_http_iter_next) """ ptr = lib.pactffi_pact_sync_http_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -3015,7 +3019,7 @@ def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) + `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) """ lib.pactffi_pact_sync_http_iter_delete(iter._ptr) @@ -3025,7 +3029,7 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction Get the next interaction from the pact. [Rust - `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_iter_next) + `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_iter_next) """ ptr = lib.pactffi_pact_interaction_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -3039,7 +3043,7 @@ def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_interaction_iter_delete) + `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_iter_delete) """ lib.pactffi_pact_interaction_iter_delete(iter._ptr) @@ -3049,7 +3053,7 @@ def matching_rule_to_json(rule: MatchingRule) -> str: Get the JSON form of the matching rule. [Rust - `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rule_to_json) + `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -3066,7 +3070,7 @@ def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rules_iter_delete) + `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rules_iter_delete) """ raise NotImplementedError @@ -3078,7 +3082,7 @@ def matching_rules_iter_next( Get the next path and matching rule out of the iterator, if possible. [Rust - `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rules_iter_next) + `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rules_iter_next) The returned pointer must be deleted with `pactffi_matching_rules_iter_pair_delete`. @@ -3100,7 +3104,7 @@ def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) + `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) """ raise NotImplementedError @@ -3110,7 +3114,7 @@ def message_new() -> Message: Get a mutable pointer to a newly-created default message on the heap. [Rust - `pactffi_message_new`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_new) + `pactffi_message_new`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_new) # Safety @@ -3132,7 +3136,7 @@ def message_new_from_json( Constructs a `Message` from the JSON string. [Rust - `pactffi_message_new_from_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_new_from_json) + `pactffi_message_new_from_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_new_from_json) # Safety @@ -3150,7 +3154,7 @@ def message_new_from_body(body: str, content_type: str) -> Message: Constructs a `Message` from a body with a given content-type. [Rust - `pactffi_message_new_from_body`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_new_from_body) + `pactffi_message_new_from_body`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_new_from_body) # Safety @@ -3168,7 +3172,7 @@ def message_delete(message: Message) -> None: Destroy the `Message` being pointed to. [Rust - `pactffi_message_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_delete) + `pactffi_message_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_delete) """ raise NotImplementedError @@ -3178,11 +3182,14 @@ def message_get_contents(message: Message) -> OwnedString | None: Get the contents of a `Message` in string form. [Rust - `pactffi_message_get_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_contents) + `pactffi_message_get_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_contents) # Safety - The returned string must be deleted with `pactffi_string_delete`. + The returned string must be deleted with `pactffi_string_delete` and can + outlive the message. This function must only ever be called from a foreign + language. Calling it from a Rust function that has a Tokio runtime in its + call stack can result in a deadlock. The returned string can outlive the message. @@ -3201,7 +3208,7 @@ def message_set_contents(message: Message, contents: str, content_type: str) -> Sets the contents of the message. [Rust - `pactffi_message_set_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_set_contents) + `pactffi_message_set_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_set_contents) # Safety @@ -3223,7 +3230,7 @@ def message_get_contents_length(message: Message) -> int: Get the length of the contents of a `Message`. [Rust - `pactffi_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_contents_length) + `pactffi_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_contents_length) # Safety @@ -3242,7 +3249,7 @@ def message_get_contents_bin(message: Message) -> str: Get the contents of a `Message` as a pointer to an array of bytes. [Rust - `pactffi_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_contents_bin) + `pactffi_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_contents_bin) # Safety @@ -3269,7 +3276,7 @@ def message_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_set_contents_bin) + `pactffi_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_set_contents_bin) # Safety @@ -3290,7 +3297,7 @@ def message_get_description(message: Message) -> OwnedString: Get a copy of the description. [Rust - `pactffi_message_get_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_description) + `pactffi_message_get_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_description) # Safety @@ -3313,7 +3320,7 @@ def message_set_description(message: Message, description: str) -> int: Write the `description` field on the `Message`. [Rust - `pactffi_message_set_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_set_description) + `pactffi_message_set_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_set_description) # Safety @@ -3335,7 +3342,7 @@ def message_get_provider_state(message: Message, index: int) -> ProviderState: Get a copy of the provider state at the given index from this message. [Rust - `pactffi_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_provider_state) + `pactffi_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_provider_state) # Safety @@ -3358,7 +3365,7 @@ def message_get_provider_state_iter(message: Message) -> ProviderStateIterator: Get an iterator over provider states. [Rust - `pactffi_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_provider_state_iter) + `pactffi_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_provider_state_iter) # Safety @@ -3376,7 +3383,7 @@ def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: Get the next value from the iterator. [Rust - `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_iter_next) + `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_iter_next) # Safety @@ -3397,7 +3404,7 @@ def provider_state_iter_delete(iter: ProviderStateIterator) -> None: Delete the iterator. [Rust - `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_iter_delete) + `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_iter_delete) """ raise NotImplementedError @@ -3407,7 +3414,7 @@ def message_find_metadata(message: Message, key: str) -> str: Get a copy of the metadata value indexed by `key`. [Rust - `pactffi_message_find_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_find_metadata) + `pactffi_message_find_metadata`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_find_metadata) # Safety @@ -3433,7 +3440,7 @@ def message_insert_metadata(message: Message, key: str, value: str) -> int: Insert the (`key`, `value`) pair into this Message's `metadata` HashMap. [Rust - `pactffi_message_insert_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_insert_metadata) + `pactffi_message_insert_metadata`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_insert_metadata) # Safety @@ -3453,14 +3460,16 @@ def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadata Get the next key and value out of the iterator, if possible. [Rust - `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_metadata_iter_next) + `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_metadata_iter_next) The returned pointer must be deleted with `pactffi_message_metadata_pair_delete`. # Safety - The underlying data must not change during iteration. + The underlying data must not change during iteration. This function must + only ever be called from a foreign language. Calling it from a Rust function + that has a Tokio runtime in its call stack can result in a deadlock. # Error Handling @@ -3474,7 +3483,7 @@ def message_get_metadata_iter(message: Message) -> MessageMetadataIterator: Get an iterator over the metadata of a message. [Rust - `pactffi_message_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_get_metadata_iter) + `pactffi_message_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_metadata_iter) # Safety @@ -3499,7 +3508,7 @@ def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: Free the metadata iterator when you're done using it. [Rust - `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_metadata_iter_delete) + `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_metadata_iter_delete) """ raise NotImplementedError @@ -3509,7 +3518,7 @@ def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_metadata_pair_delete) + `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_metadata_pair_delete) """ raise NotImplementedError @@ -3521,7 +3530,7 @@ def message_pact_new_from_json(file_name: str, json_str: str) -> MessagePact: The provided file name is used when generating error messages. [Rust - `pactffi_message_pact_new_from_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_new_from_json) + `pactffi_message_pact_new_from_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_new_from_json) # Safety @@ -3540,7 +3549,7 @@ def message_pact_delete(message_pact: MessagePact) -> None: Delete the `MessagePact` being pointed to. [Rust - `pactffi_message_pact_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_delete) + `pactffi_message_pact_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_delete) """ raise NotImplementedError @@ -3553,7 +3562,7 @@ def message_pact_get_consumer(message_pact: MessagePact) -> Consumer: pointer. [Rust - `pactffi_message_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_get_consumer) + `pactffi_message_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_get_consumer) # Safety @@ -3575,7 +3584,7 @@ def message_pact_get_provider(message_pact: MessagePact) -> Provider: pointer. [Rust - `pactffi_message_pact_get_provider`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_get_provider) + `pactffi_message_pact_get_provider`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_get_provider) # Safety @@ -3596,7 +3605,7 @@ def message_pact_get_message_iter( Get an iterator over the messages of a message pact. [Rust - `pactffi_message_pact_get_message_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_get_message_iter) + `pactffi_message_pact_get_message_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_get_message_iter) # Safety @@ -3621,7 +3630,7 @@ def message_pact_message_iter_next(iter: MessagePactMessageIterator) -> Message: Get the next message from the message pact. [Rust - `pactffi_message_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_message_iter_next) + `pactffi_message_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_message_iter_next) # Safety @@ -3640,7 +3649,7 @@ def message_pact_message_iter_delete(iter: MessagePactMessageIterator) -> None: Delete the iterator. [Rust - `pactffi_message_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_message_iter_delete) + `pactffi_message_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_message_iter_delete) """ raise NotImplementedError @@ -3650,7 +3659,7 @@ def message_pact_find_metadata(message_pact: MessagePact, key1: str, key2: str) Get a copy of the metadata value indexed by `key1` and `key2`. [Rust - `pactffi_message_pact_find_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_find_metadata) + `pactffi_message_pact_find_metadata`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_find_metadata) # Safety @@ -3678,7 +3687,7 @@ def message_pact_get_metadata_iter( Get an iterator over the metadata of a message pact. [Rust - `pactffi_message_pact_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_get_metadata_iter) + `pactffi_message_pact_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_get_metadata_iter) # Safety @@ -3705,7 +3714,7 @@ def message_pact_metadata_iter_next( Get the next triple out of the iterator, if possible. [Rust - `pactffi_message_pact_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_metadata_iter_next) + `pactffi_message_pact_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_metadata_iter_next) # Safety @@ -3723,7 +3732,7 @@ def message_pact_metadata_iter_delete(iter: MessagePactMetadataIterator) -> None """ Free the metadata iterator when you're done using it. - [Rust `pactffi_message_pact_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_metadata_iter_delete) + [Rust `pactffi_message_pact_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_metadata_iter_delete) """ raise NotImplementedError @@ -3732,7 +3741,7 @@ def message_pact_metadata_triple_delete(triple: MessagePactMetadataTriple) -> No """ Free a triple returned from `pactffi_message_pact_metadata_iter_next`. - [Rust `pactffi_message_pact_metadata_triple_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_pact_metadata_triple_delete) + [Rust `pactffi_message_pact_metadata_triple_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_metadata_triple_delete) """ raise NotImplementedError @@ -3742,7 +3751,7 @@ def provider_get_name(provider: Provider) -> str: Get a copy of this provider's name. [Rust - `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_get_name) + `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -3788,7 +3797,7 @@ def pact_get_provider(pact: Pact) -> Provider: `pactffi_pact_provider_delete` when no longer required. [Rust - `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_get_provider) + `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_get_provider) # Errors @@ -3803,7 +3812,7 @@ def pact_provider_delete(provider: Provider) -> None: Frees the memory used by the Pact provider. [Rust - `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_provider_delete) + `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_provider_delete) """ raise NotImplementedError @@ -3815,7 +3824,7 @@ def provider_state_get_name(provider_state: ProviderState) -> str: This needs to be deleted with `pactffi_string_delete`. [Rust - `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_get_name) + `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_get_name) # Safety @@ -3835,7 +3844,7 @@ def provider_state_get_param_iter( Get an iterator over the params of a provider state. [Rust - `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_get_param_iter) + `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_get_param_iter) # Safety @@ -3862,7 +3871,7 @@ def provider_state_param_iter_next( Get the next key and value out of the iterator, if possible. [Rust - `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_param_iter_next) + `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_param_iter_next) Returns a pointer to a heap allocated array of 2 elements, the pointer to the key string on the heap, and the pointer to the value string on the heap. @@ -3885,7 +3894,7 @@ def provider_state_delete(provider_state: ProviderState) -> None: Free the provider state when you're done using it. [Rust - `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_delete) + `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_delete) """ raise NotImplementedError @@ -3895,7 +3904,7 @@ def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: Free the provider state param iterator when you're done using it. [Rust - `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_param_iter_delete) + `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_param_iter_delete) """ raise NotImplementedError @@ -3905,7 +3914,7 @@ def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: Free a pair of key and value. [Rust - `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_provider_state_param_pair_delete) + `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_param_pair_delete) """ raise NotImplementedError @@ -3915,7 +3924,7 @@ def sync_message_new() -> SynchronousMessage: Get a mutable pointer to a newly-created default message on the heap. [Rust - `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_new) + `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_new) # Safety @@ -3933,7 +3942,7 @@ def sync_message_delete(message: SynchronousMessage) -> None: Destroy the `Message` being pointed to. [Rust - `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_delete) + `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_delete) """ raise NotImplementedError @@ -3943,7 +3952,7 @@ def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: Get the request contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) + `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) # Safety @@ -3970,7 +3979,7 @@ def sync_message_set_request_contents_str( Sets the request contents of the message. [Rust - `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) + `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) - `message` - the message to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -3998,7 +4007,7 @@ def sync_message_get_request_contents_length(message: SynchronousMessage) -> int Get the length of the request contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) + `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) # Safety @@ -4017,7 +4026,7 @@ def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes: Get the request contents of a `SynchronousMessage` as a bytes. [Rust - `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) + `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) # Safety @@ -4044,7 +4053,7 @@ def sync_message_set_request_contents_bin( Sets the request contents of the message as an array of bytes. [Rust - `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) + `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) * `message` - the message to set the request contents for * `contents` - pointer to contents to copy from @@ -4071,7 +4080,7 @@ def sync_message_get_request_contents(message: SynchronousMessage) -> MessageCon Get the request contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_request_contents) + `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_request_contents) # Safety @@ -4091,7 +4100,7 @@ def sync_message_get_number_responses(message: SynchronousMessage) -> int: Get the number of response messages in the `SynchronousMessage`. [Rust - `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_number_responses) + `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_number_responses) # Safety @@ -4112,7 +4121,7 @@ def sync_message_get_response_contents_str( Get the response contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) + `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) # Safety @@ -4145,7 +4154,7 @@ def sync_message_set_response_contents_str( with default values. [Rust - `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) + `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response. @@ -4177,7 +4186,7 @@ def sync_message_get_response_contents_length( Get the length of the response contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) + `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) # Safety @@ -4199,7 +4208,7 @@ def sync_message_get_response_contents_bin( Get the response contents of a `SynchronousMessage` as bytes. [Rust - `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) + `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) # Safety @@ -4230,7 +4239,7 @@ def sync_message_set_response_contents_bin( responses will be padded with default values. [Rust - `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) + `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response @@ -4261,7 +4270,7 @@ def sync_message_get_response_contents( Get the response contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_response_contents) + `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_response_contents) # Safety @@ -4281,7 +4290,7 @@ def sync_message_get_description(message: SynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_description) + `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_description) # Safety @@ -4305,7 +4314,7 @@ def sync_message_set_description(message: SynchronousMessage, description: str) Write the `description` field on the `SynchronousMessage`. [Rust - `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_set_description) + `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_set_description) # Safety @@ -4330,7 +4339,7 @@ def sync_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_provider_state) + `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_provider_state) # Safety @@ -4356,7 +4365,7 @@ def sync_message_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) + `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) # Safety @@ -4374,7 +4383,7 @@ def string_delete(string: OwnedString) -> None: Delete a string previously returned by this FFI. [Rust - `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_string_delete) + `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_string_delete) """ lib.pactffi_string_delete(string._ptr) @@ -4389,7 +4398,7 @@ def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: the mock server is returned. [Rust - `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_create_mock_server) + `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_create_mock_server) * `pact_str` - Pact JSON * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:80) @@ -4425,7 +4434,7 @@ def get_tls_ca_certificate() -> OwnedString: Fetch the CA Certificate used to generate the self-signed certificate. [Rust - `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_get_tls_ca_certificate) + `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_get_tls_ca_certificate) **NOTE:** The string for the result is allocated on the heap, and will have to be freed by the caller using pactffi_string_delete. @@ -4446,7 +4455,7 @@ def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) - operating system. The port of the mock server is returned. [Rust - `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_create_mock_server_for_pact) + `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_create_mock_server_for_pact) * `pact` - Handle to a Pact model created with created with `pactffi_new_pact`. @@ -4489,7 +4498,7 @@ def create_mock_server_for_transport( Create a mock server for the provided Pact handle and transport. [Rust - `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_create_mock_server_for_transport) + `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_create_mock_server_for_transport) Args: pact: @@ -4550,7 +4559,7 @@ def mock_server_matched(mock_server_handle: PactServerHandle) -> bool: if any request has not been successfully matched, or the method panics. [Rust - `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mock_server_matched) + `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mock_server_matched) """ return lib.pactffi_mock_server_matched(mock_server_handle._ref) @@ -4562,7 +4571,7 @@ def mock_server_mismatches( External interface to get all the mismatches from a mock server. [Rust - `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mock_server_mismatches) + `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mock_server_mismatches) # Errors @@ -4588,7 +4597,7 @@ def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: and cleanup any memory allocated for it. [Rust - `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_cleanup_mock_server) + `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_cleanup_mock_server) Args: mock_server_handle: @@ -4616,7 +4625,7 @@ def write_pact_file( directory to write the file to is passed as the second parameter. [Rust - `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_write_pact_file) + `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_write_pact_file) Args: mock_server_handle: @@ -4667,7 +4676,7 @@ def mock_server_logs(mock_server_handle: PactServerHandle) -> str: started. [Rust - `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_mock_server_logs) + `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mock_server_logs) Raises: RuntimeError: If the logs for the mock server can not be retrieved. @@ -4690,7 +4699,7 @@ def generate_datetime_string(format: str) -> StringResult: string needs to be freed with the `pactffi_string_delete` function [Rust - `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generate_datetime_string) + `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generate_datetime_string) # Safety @@ -4707,7 +4716,7 @@ def check_regex(regex: str, example: str) -> bool: Checks that the example string matches the given regex. [Rust - `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_check_regex) + `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_check_regex) # Safety @@ -4726,7 +4735,7 @@ def generate_regex_value(regex: str) -> StringResult: `pactffi_string_delete` function. [Rust - `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_generate_regex_value) + `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generate_regex_value) # Safety @@ -4741,7 +4750,7 @@ def free_string(s: str) -> None: [DEPRECATED] Frees the memory allocated to a string by another function. [Rust - `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_free_string) + `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_free_string) This function is deprecated. Use `pactffi_string_delete` instead. @@ -4763,7 +4772,7 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle: Creates a new Pact model and returns a handle to it. [Rust - `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_pact) + `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_new_pact) Args: consumer_name: @@ -4783,12 +4792,25 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle: ) +def pact_handle_to_pointer(pact: PactHandle) -> Pact: + """ + Unwraps a Pact handle to the underlying Pact. + + The Pact model which has been cloned from the Pact handle's inner Pact + model. + + The returned Pact model must be freed with the `pactffi_pact_model_delete` + function when no longer needed. + """ + raise NotImplementedError + + def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: """ Creates a new HTTP Interaction and returns a handle to it. [Rust - `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_interaction) + `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_new_interaction) Args: pact: @@ -4813,7 +4835,7 @@ def new_message_interaction(pact: PactHandle, description: str) -> InteractionHa Creates a new message interaction and return a handle to it. [Rust - `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_message_interaction) + `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_new_message_interaction) Args: pact: @@ -4842,7 +4864,7 @@ def new_sync_message_interaction( Creates a new synchronous message interaction and return a handle to it. [Rust - `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_sync_message_interaction) + `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_new_sync_message_interaction) Args: pact: @@ -4867,7 +4889,7 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> None: Sets the description for the Interaction. [Rust - `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_upon_receiving) + `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_upon_receiving) This function @@ -4904,7 +4926,7 @@ def given(interaction: InteractionHandle, description: str) -> None: Adds a provider state to the Interaction. [Rust - `pactffi_given`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_given) + `pactffi_given`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_given) Args: interaction: @@ -4930,7 +4952,7 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> Non used with V4 interactions. [Rust - `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_interaction_test_name) + `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_interaction_test_name) Args: interaction: @@ -4990,7 +5012,7 @@ def given_with_param( be parsed as JSON. [Rust - `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_given_with_param) + `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_given_with_param) Args: interaction: @@ -5031,7 +5053,7 @@ def given_with_params( with a `value` key. [Rust - `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_given_with_params) + `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_given_with_params) Args: interaction: @@ -5081,7 +5103,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None Configures the request for the Interaction. [Rust - `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_request) + `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_request) Args: interaction: @@ -5095,7 +5117,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md) + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md) which allows regex patterns. For examples: ```json @@ -5129,7 +5151,7 @@ def with_query_parameter_v2( Configures a query parameter for the Interaction. [Rust - `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_query_parameter_v2) + `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_query_parameter_v2) To setup a query parameter with multiple values, you can either call this function multiple times with a different index value: @@ -5166,7 +5188,23 @@ def with_query_parameter_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md) + + If you want the matching rules to apply to all values (and not just the one + with the given index), make sure to set the value to be an array. + + ```python + with_query_parameter_v2( + handle, + "id", + 0, + json.dumps({ + "value": ["2"], + "pact:matcher:type": "regex", + "regex": r"\d+", + }), + ) + ``` Args: interaction: @@ -5184,7 +5222,7 @@ def with_query_parameter_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: If there was an error setting the query parameter. @@ -5205,7 +5243,7 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None: Sets the specification version for a given Pact model. [Rust - `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_specification) + `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_specification) Args: pact: @@ -5220,6 +5258,23 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None: raise RuntimeError(msg) +def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: + """ + Fetches the Pact specification version for the given Pact model. + + [Rust + `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_handle_get_pact_spec_version) + + Args: + handle: + Handle to a Pact model. + + Returns: + The spec version for the Pact model. + """ + raise NotImplementedError + + def with_pact_metadata( pact: PactHandle, namespace: str, @@ -5234,7 +5289,7 @@ def with_pact_metadata( mock server for it has already started) [Rust - `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_pact_metadata) + `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_pact_metadata) Args: pact: @@ -5270,7 +5325,7 @@ def with_header_v2( r""" Configures a header for the Interaction. - [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_header_v2) + [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_header_v2) To setup a header with multiple values, you can either call this function multiple times with a different index value: @@ -5308,7 +5363,7 @@ def with_header_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -5330,7 +5385,7 @@ def with_header_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: If there was an error setting the header. @@ -5361,7 +5416,7 @@ def set_header( and generators can not be configured with it. [Rust - `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_set_header) + `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_set_header) If matching rules are required to be set, use `pactffi_with_header_v2`. @@ -5398,7 +5453,7 @@ def response_status(interaction: InteractionHandle, status: int) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_response_status) + `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_response_status) Args: interaction: @@ -5416,6 +5471,54 @@ def response_status(interaction: InteractionHandle, status: int) -> None: raise RuntimeError(msg) +def response_status_v2(interaction: InteractionHandle, status: str) -> None: + """ + Configures the response for the Interaction. + + [Rust + `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_response_status_v2) + + To include matching rules for the status (only statusCode or integer really + makes sense to use), include the matching rule JSON format with the value as + a single JSON document. I.e. + + ```python + response_status_v2( + handle, + json.dumps({ + "pact:generator:type": "RandomInt", + "min": 100, + "max": 399, + "pact:matcher:type": "statusCode", + "status": "nonError", + }), + ) + ``` + + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md) + + Args: + interaction: + Handle to the Interaction. + + status: + The response status. Defaults to 200. + + This may be a simple string in which case it will be used as-is, or + it may be a [JSON matching + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). + + Raises: + RuntimeError: If the response status could not be set. + """ + success: bool = lib.pactffi_response_status_v2( + interaction._ref, status.encode("utf-8") + ) + if not success: + msg = f"The response status {status} could not be set for {interaction}." + raise RuntimeError(msg) + + def with_body( interaction: InteractionHandle, part: InteractionPart, @@ -5426,7 +5529,7 @@ def with_body( Adds the body for the interaction. [Rust - `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_body) + `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -5447,7 +5550,7 @@ def with_body( body: The body contents. For JSON payloads, matching rules can be embedded - in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md). + in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: If the body could not be specified. @@ -5463,6 +5566,45 @@ def with_body( raise RuntimeError(msg) +def with_binary_body( + interaction: InteractionHandle, + part: InteractionPart, + content_type: str | None, + body: bytes | None, +) -> None: + """ + Adds the body for the interaction. + + [Rust + `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_binary_body) + + For HTTP and async message interactions, this will overwrite the body. With + asynchronous messages, the part parameter will be ignored. With synchronous + messages, the request contents will be overwritten, while a new response + will be appended to the message. + + Args: + interaction: + Handle to the Interaction. + + part: + The part of the interaction to add the body to (Request or + Response). + + content_type: + The content type of the body. Will be ignored if a content type + header is already set. If `None`, the content type will be set to + `application/octet-stream`. + + body: + The body contents. If `None`, the body will be set to null. + + Raises: + RuntimeError: If the body could not be modified. + """ + raise NotImplementedError + + def with_binary_file( interaction: InteractionHandle, part: InteractionPart, @@ -5477,7 +5619,7 @@ def with_binary_file( already started) [Rust - `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_binary_file) + `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_binary_file) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -5518,6 +5660,36 @@ def with_binary_file( raise RuntimeError(msg) +def with_matching_rules( + interaction: InteractionHandle, + part: InteractionPart, + rules: str, +) -> None: + """ + Add matching rules to the interaction. + + [Rust + `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_matching_rules) + + This function can be called multiple times, in which case the matching + rules will be merged. + + Args: + interaction: + Handle to the Interaction. + + part: + Request or response part (if applicable). + + rules: + JSON string of the matching rules to add to the interaction. + + Raises: + RuntimeError: If the rules could not be added. + """ + raise NotImplementedError + + def with_multipart_file_v2( # noqa: PLR0913 interaction: InteractionHandle, part: InteractionPart, @@ -5534,7 +5706,7 @@ def with_multipart_file_v2( # noqa: PLR0913 already started) or an error occurs. [Rust - `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_multipart_file_v2) + `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_multipart_file_v2) This function can be called multiple times. In that case, each subsequent call will be appended to the existing multipart body as a new part. @@ -5588,7 +5760,7 @@ def with_multipart_file( already started) or an error occurs. [Rust - `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_multipart_file) + `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_multipart_file) * `interaction` - Interaction handle to set the body for. * `part` - Request or response part. @@ -5625,7 +5797,7 @@ def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: Get an iterator over all the messages of the Pact. [Rust - `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_handle_get_message_iter) + `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_handle_get_message_iter) # Safety @@ -5649,7 +5821,7 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -5675,7 +5847,7 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: `pactffi_pact_sync_http_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) + `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) # Safety @@ -5696,7 +5868,7 @@ def new_message_pact(consumer_name: str, provider_name: str) -> MessagePactHandl Creates a new Pact Message model and returns a handle to it. [Rust - `pactffi_new_message_pact`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_message_pact) + `pactffi_new_message_pact`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_new_message_pact) * `consumer_name` - The name of the consumer for the pact. * `provider_name` - The name of the provider for the pact. @@ -5712,7 +5884,7 @@ def new_message(pact: MessagePactHandle, description: str) -> MessageHandle: Creates a new Message and returns a handle to it. [Rust - `pactffi_new_message`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_new_message) + `pactffi_new_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_new_message) * `description` - The message description. It needs to be unique for each Message. @@ -5727,7 +5899,7 @@ def message_expects_to_receive(message: MessageHandle, description: str) -> None Sets the description for the Message. [Rust - `pactffi_message_expects_to_receive`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_expects_to_receive) + `pactffi_message_expects_to_receive`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_expects_to_receive) * `description` - The message description. It needs to be unique for each message. @@ -5740,7 +5912,7 @@ def message_given(message: MessageHandle, description: str) -> None: Adds a provider state to the Interaction. [Rust - `pactffi_message_given`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_given) + `pactffi_message_given`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_given) * `description` - The provider state description. It needs to be unique for each message @@ -5758,7 +5930,7 @@ def message_given_with_param( Adds a provider state to the Message with a parameter key and value. [Rust - `pactffi_message_given_with_param`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_given_with_param) + `pactffi_message_given_with_param`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_given_with_param) * `description` - The provider state description. It needs to be unique. * `name` - Parameter name. @@ -5777,7 +5949,7 @@ def message_with_contents( Adds the contents of the Message. [Rust - `pactffi_message_with_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_with_contents) + `pactffi_message_with_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_with_contents) Accepts JSON, binary and other payload types. Binary data will be base64 encoded when serialised. @@ -5804,7 +5976,7 @@ def message_with_metadata(message_handle: MessageHandle, key: str, value: str) - Adds expected metadata to the Message. [Rust - `pactffi_message_with_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_with_metadata) + `pactffi_message_with_metadata`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_with_metadata) * `key` - metadata key * `value` - metadata value. @@ -5812,16 +5984,66 @@ def message_with_metadata(message_handle: MessageHandle, key: str, value: str) - raise NotImplementedError +def message_with_metadata_v2( + message_handle: MessageHandle, + key: str, + value: str, +) -> None: + """ + Adds expected metadata to the Message. + + [Rust + `pactffi_message_with_metadata_v2`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_with_metadata_v2) + + Args: + message_handle: + Handle to the Message. + + key: + Metadata key. + + value: + Metadata value. + + This may be a simple string in which case it will be used as-is, or + it may be a [JSON matching + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). + + To include matching rules for the metadata, include the matching rule JSON + format with the value as a single JSON document. I.e. + + ```python + message_with_metadata_v2( + handle, + "contentType", + json.dumps({ + "pact:matcher:type": "regex", + "regex": "text/.*", + }), + ) + ``` + + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). + """ + raise NotImplementedError + + def message_reify(message_handle: MessageHandle) -> OwnedString: """ Reifies the given message. [Rust - `pactffi_message_reify`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_message_reify) + `pactffi_message_reify`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_reify) Reification is the process of stripping away any matchers, and returning the - original contents. NOTE: the returned string needs to be deallocated with - the `free_string` function + original contents. + + # Safety + + The returned string needs to be deallocated with the `free_string` function. + This function must only ever be called from a foreign language. Calling it + from a Rust function that has a Tokio runtime in its call stack can result + in a deadlock. """ raise NotImplementedError @@ -5840,7 +6062,7 @@ def write_message_pact_file( pointer is passed, the current working directory is used. [Rust - `pactffi_write_message_pact_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_write_message_pact_file) + `pactffi_write_message_pact_file`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_write_message_pact_file) If overwrite is true, the file will be overwritten with the contents of the current pact. Otherwise, it will be merged with any existing pact file. @@ -5874,7 +6096,7 @@ def with_message_pact_metadata( version [Rust - `pactffi_with_message_pact_metadata`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_with_message_pact_metadata) + `pactffi_with_message_pact_metadata`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_message_pact_metadata) * `pact` - Handle to a Pact model * `namespace` - the top level metadat key to set any key values on @@ -5894,7 +6116,7 @@ def pact_handle_write_file( External interface to write out the pact file. [Rust - `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_pact_handle_write_file) + `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_handle_write_file) This function should be called if all the consumer tests have passed. @@ -5934,7 +6156,7 @@ def free_pact_handle(pact: PactHandle) -> None: Delete a Pact handle and free the resources used by it. [Rust - `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_free_pact_handle) + `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_free_pact_handle) Raises: RuntimeError: If the handle could not be freed. @@ -5954,7 +6176,7 @@ def free_message_pact_handle(pact: MessagePactHandle) -> int: Delete a Pact handle and free the resources used by it. [Rust - `pactffi_free_message_pact_handle`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_free_message_pact_handle) + `pactffi_free_message_pact_handle`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_free_message_pact_handle) # Error Handling @@ -5971,7 +6193,7 @@ def verify(args: str) -> int: """ External interface to verifier a provider. - [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verify) + [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verify) * `args` - the same as the CLI interface, except newline delimited @@ -6001,7 +6223,7 @@ def verifier_new() -> VerifierHandle: free all allocated resources. [Rust - `pactffi_verifier_new`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_new) + `pactffi_verifier_new`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_new) Deprecated: This function is deprecated. Use `pactffi_verifier_new_for_application` which allows the calling @@ -6031,7 +6253,7 @@ def verifier_new_for_application(name: str, version: str) -> VerifierHandle: free all allocated resources [Rust - `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_new_for_application) + `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_new_for_application) # Safety @@ -6048,7 +6270,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None: """ Shutdown the verifier and release all resources. - [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_shutdown) + [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_shutdown) """ raise NotImplementedError @@ -6067,7 +6289,7 @@ def verifier_set_provider_info( # noqa: PLR0913 Passing a NULL for any field will use the default value for that field. [Rust - `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_provider_info) + `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_provider_info) # Safety @@ -6090,7 +6312,7 @@ def verifier_add_provider_transport( Passing a NULL for any field will use the default value for that field. [Rust - `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_add_provider_transport) + `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_add_provider_transport) For non-plugin based message interactions, set protocol to "message" and set scheme to an empty string or "https" if secure HTTP is required. @@ -6115,7 +6337,7 @@ def verifier_set_filter_info( Set the filters for the Pact verifier. [Rust - `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_filter_info) + `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_filter_info) If `filter_description` is not empty, it needs to be as a regular expression. @@ -6142,7 +6364,7 @@ def verifier_set_provider_state( Set the provider state URL for the Pact verifier. [Rust - `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_provider_state) + `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_provider_state) `teardown` is a boolean value. If teardown state change requests should be made after an interaction is validated (default is false). Set it to greater @@ -6168,7 +6390,7 @@ def verifier_set_verification_options( Set the options used by the verifier when calling the provider. [Rust - `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_verification_options) + `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_verification_options) `disable_ssl_verification` is a boolean value. Set it to greater than zero to turn the option on. @@ -6189,7 +6411,7 @@ def verifier_set_coloured_output(handle: VerifierHandle, coloured_output: int) - By default, coloured output is enabled. [Rust - `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_coloured_output) + `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_coloured_output) `coloured_output` is a boolean value. Set it to greater than zero to turn the option on. @@ -6208,7 +6430,7 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, is_error: int) -> int Enables or disables if no pacts are found to verify results in an error. [Rust - `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) + `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) `is_error` is a boolean value. Set it to greater than zero to enable an error when no pacts are found to verify, and set it to zero to disable this. @@ -6234,7 +6456,7 @@ def verifier_set_publish_options( # noqa: PLR0913 Set the options used when publishing verification results to the Broker. [Rust - `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_publish_options) + `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_publish_options) # Args @@ -6263,7 +6485,7 @@ def verifier_set_consumer_filters( Set the consumer filters for the Pact verifier. [Rust - `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_set_consumer_filters) + `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_consumer_filters) # Safety @@ -6283,7 +6505,7 @@ def verifier_add_custom_header( Adds a custom header to be added to the requests made to the provider. [Rust - `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_add_custom_header) + `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_add_custom_header) # Safety @@ -6298,7 +6520,7 @@ def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: Adds a Pact file as a source to verify. [Rust - `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_add_file_source) + `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_add_file_source) # Safety @@ -6316,7 +6538,7 @@ def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> Non All pacts from the directory that match the provider name will be verified. [Rust - `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_add_directory_source) + `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_add_directory_source) # Safety @@ -6340,7 +6562,7 @@ def verifier_url_source( The Pact file will be fetched from the URL. [Rust - `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_url_source) + `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_url_source) If a username and password is given, then basic authentication will be used when fetching the pact file. If a token is provided, then bearer token @@ -6369,7 +6591,7 @@ def verifier_broker_source( name. [Rust - `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_broker_source) + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_broker_source) If a username and password is given, then basic authentication will be used when fetching the pact file. If a token is provided, then bearer token @@ -6408,7 +6630,7 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 `https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/`). [Rust - `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) + `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) The consumer version selectors must be passed in in JSON format. @@ -6435,7 +6657,7 @@ def verifier_execute(handle: VerifierHandle) -> int: """ Runs the verification. - [Rust `pactffi_verifier_execute`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_execute) + [Rust `pactffi_verifier_execute`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_execute) # Error Handling @@ -6452,7 +6674,7 @@ def verifier_cli_args() -> str: string. [Rust - `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_cli_args) + `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_cli_args) The purpose is to then be able to use in other languages which wrap the FFI library, to implement the same CLI functionality automatically without @@ -6512,7 +6734,7 @@ def verifier_logs(handle: VerifierHandle) -> OwnedString: function call to avoid leaking memory. [Rust - `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_logs) + `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_logs) Will return a NULL pointer if the logs for the verification can not be retrieved. @@ -6529,7 +6751,7 @@ def verifier_logs_for_provider(provider_name: str) -> OwnedString: function call to avoid leaking memory. [Rust - `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_logs_for_provider) + `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_logs_for_provider) Will return a NULL pointer if the logs for the verification can not be retrieved. @@ -6545,7 +6767,7 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: call to avoid leaking memory. [Rust - `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_output) + `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_output) * `strip_ansi` - This parameter controls ANSI escape codes. Setting it to a non-zero value @@ -6564,7 +6786,7 @@ def verifier_json(handle: VerifierHandle) -> OwnedString: call to avoid leaking memory. [Rust - `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_verifier_json) + `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_json) Will return a NULL pointer if the handle is invalid. """ @@ -6586,7 +6808,7 @@ def using_plugin( otherwise you will have plugin processes left running. [Rust - `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_using_plugin) + `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_using_plugin) Args: pact: @@ -6625,7 +6847,7 @@ def cleanup_plugins(pact: PactHandle) -> None: zero). [Rust - `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_cleanup_plugins) + `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_cleanup_plugins) """ lib.pactffi_cleanup_plugins(pact._ref) @@ -6644,7 +6866,7 @@ def interaction_contents( format of the JSON contents. [Rust - `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_interaction_contents) + `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_interaction_contents) Args: interaction: @@ -6700,7 +6922,7 @@ def matches_string_value( function once it is no longer required. [Rust - `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_string_value) + `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_string_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string @@ -6731,7 +6953,7 @@ def matches_u64_value( function once it is no longer required. [Rust - `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_u64_value) + `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_u64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -6761,7 +6983,7 @@ def matches_i64_value( function once it is no longer required. [Rust - `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_i64_value) + `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_i64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -6791,7 +7013,7 @@ def matches_f64_value( function once it is no longer required. [Rust - `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_f64_value) + `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_f64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -6821,7 +7043,7 @@ def matches_bool_value( function once it is no longer required. [Rust - `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_bool_value) + `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_bool_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get, 0 == false and 1 == true @@ -6853,7 +7075,7 @@ def matches_binary_value( # noqa: PLR0913 function once it is no longer required. [Rust - `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_binary_value) + `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_binary_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -6888,7 +7110,7 @@ def matches_json_value( function once it is no longer required. [Rust - `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/?search=pactffi_matches_json_value) + `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_json_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string diff --git a/pact/v3/pact.py b/pact/v3/pact.py index 495868441..0a2880500 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -483,7 +483,7 @@ def with_header( # JSON Matching Pact's matching rules are defined in the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md) + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md) and support a wide range of matching rules. These can be specified using a JSON object as a strong using `json.dumps(...)`. For example, the above rule whereby the `X-Foo` header has multiple values can be @@ -723,7 +723,7 @@ def with_query_parameter(self, name: str, value: str) -> Self: ``` For more information on the format of the JSON object, see the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.9/rust/pact_ffi/IntegrationJson.md). + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). Args: name: From 177ef1fa86b50a0f893d7c4f7c62b31b9abd8bdd Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 7 Feb 2024 17:38:15 +1100 Subject: [PATCH 0190/1376] feat(v3): add with_matching_rules With the upgrade to the Pact FFI library, it is now possible to set matching rules directly during the Pact definition. These will be leveraged during the `v2` compatibility suite tests. Signed-off-by: JP-Ellis --- pact/v3/ffi.py | 9 ++++++++- pact/v3/pact.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 6ea24cf97..7a47b9d89 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -5687,7 +5687,14 @@ def with_matching_rules( Raises: RuntimeError: If the rules could not be added. """ - raise NotImplementedError + success: bool = lib.pactffi_with_matching_rules( + interaction._ref, + part.value, + rules.encode("utf-8"), + ) + if not success: + msg = f"Unable to set matching rules for {interaction}." + raise RuntimeError(msg) def with_multipart_file_v2( # noqa: PLR0913 diff --git a/pact/v3/pact.py b/pact/v3/pact.py index 0a2880500..f8e573a0b 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -380,6 +380,41 @@ def with_plugin_contents( ) return self + def with_matching_rules( + self, + rules: dict[str, Any] | str, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Add matching rules to the interaction. + + Matching rules are used to specify how the request or response should be + matched. This is useful for specifying that certain parts of the request + or response are flexible, such as the date or time. + + Args: + rules: + Matching rules to add to the interaction. This must be + encodable using [`json.dumps(...)`][json.dumps], or a string. + + part: + Whether the matching rules should be added to the request or the + response. If `None`, then the function intelligently determines + whether the matching rules should be added to the request or the + response, based on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + if isinstance(rules, dict): + rules = json.dumps(rules) + + pact.v3.ffi.with_matching_rules( + self._handle, + self._parse_interaction_part(part), + rules, + ) + return self + class HttpInteraction(Interaction): """ From f4f3fc5148d7ff1925505681b2251e99251065ee Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 8 Feb 2024 12:34:54 +1100 Subject: [PATCH 0191/1376] chore(test/v3): move bdd steps into shared module As most of the BDD steps are shared across the V1 and V2 suites, all of the steps have been migrated into a shared module. Due to the way PyTest-BDD inserts and resolves these steps, the general pattern of: ```python def outer(stacklevel: int=1): @when/given/then(..., stacklevel=stacklevel + 1) def _(): ... ``` is required; otherwise, the steps are not available. This commit also includes the addition of a Markdown table parser, and a few minor changes in preparation to the V2 suite (as this commit was created following the implementation of the full V2 consumer suite). Signed-off-by: JP-Ellis --- .../v3/compatiblity_suite/test_v1_consumer.py | 672 ++---------------- .../{util.py => util/__init__.py} | 191 ++++- tests/v3/compatiblity_suite/util/consumer.py | 654 +++++++++++++++++ 3 files changed, 892 insertions(+), 625 deletions(-) rename tests/v3/compatiblity_suite/{util.py => util/__init__.py} (59%) create mode 100644 tests/v3/compatiblity_suite/util/consumer.py diff --git a/tests/v3/compatiblity_suite/test_v1_consumer.py b/tests/v3/compatiblity_suite/test_v1_consumer.py index 8427e07fd..3710db730 100644 --- a/tests/v3/compatiblity_suite/test_v1_consumer.py +++ b/tests/v3/compatiblity_suite/test_v1_consumer.py @@ -2,29 +2,37 @@ from __future__ import annotations -import json import logging -import re -from typing import TYPE_CHECKING, Any, Generator import pytest -import requests -from pytest_bdd import given, parsers, scenario, then, when -from yarl import URL - -from pact.v3 import Pact -from tests.v3.compatiblity_suite.util import ( - FIXTURES_ROOT, - InteractionDefinition, - string_to_int, - truncate, +from pytest_bdd import given, parsers, scenario + +from tests.v3.compatiblity_suite.util import InteractionDefinition, parse_markdown_table +from tests.v3.compatiblity_suite.util.consumer import ( + a_response_is_returned, + request_n_is_made_to_the_mock_server, + request_n_is_made_to_the_mock_server_with_the_following_changes, + the_content_type_will_be_set_as, + the_mismatches_will_contain_a_mismatch_with_path_with_the_error, + the_mismatches_will_contain_a_mismatch_with_the_error, + the_mock_server_is_started_with_interactions, + the_mock_server_status_will_be, + the_mock_server_status_will_be_an_expected_but_not_received_error_for_interaction_n, + the_mock_server_status_will_be_an_unexpected_request_received_for_interaction_n, + the_mock_server_status_will_be_an_unexpected_request_received_for_path, + the_mock_server_status_will_be_mismatches, + the_mock_server_will_write_out_a_pact_file_for_the_interaction_when_done, + the_nth_interaction_request_content_type_will_be, + the_nth_interaction_request_query_parameters_will_be, + the_nth_interaction_request_will_be_for_method, + the_nth_interaction_request_will_contain_the_document, + the_nth_interaction_request_will_contain_the_header, + the_nth_interaction_will_contain_the_document, + the_pact_file_will_contain_n_interactions, + the_pact_test_is_done, + the_payload_will_contain_the_json_document, ) -if TYPE_CHECKING: - from pathlib import Path - - from pact.v3.pact import PactServer - logger = logging.getLogger(__name__) ################################################################################ @@ -289,9 +297,10 @@ def test_request_with_a_multipart_body_negative_case() -> None: @given( parsers.parse("the following HTTP interactions have been defined:\n{content}"), target_fixture="interaction_definitions", + converters={"content": parse_markdown_table}, ) def the_following_http_interactions_have_been_defined( - content: str, + content: list[dict[str, str]], ) -> dict[int, InteractionDefinition]: """ Parse the HTTP interactions table into a dictionary. @@ -311,20 +320,14 @@ def the_following_http_interactions_have_been_defined( The first row is ignored, as it is assumed to be the column headers. The order of the columns is similarly ignored. """ - rows = [ - list(map(str.strip, row.split("|")))[1:-1] - for row in content.split("\n") - if row.strip() - ] - # Check that the table is well-formed - assert len(rows[0]) == 9 - assert rows[0][0] == "No" + assert len(content[0]) == 9, f"Expected 9 columns, got {len(content[0])}" + assert "No" in content[0], "'No' column not found" # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} - for row in rows[1:]: - interactions[int(row[0])] = InteractionDefinition(**dict(zip(rows[0], row))) + for row in content: + interactions[int(row["No"])] = InteractionDefinition(**row) return interactions @@ -332,597 +335,30 @@ def the_following_http_interactions_have_been_defined( ## When ################################################################################ - -@when( - parsers.re( - r"the mock server is started" - r" with interactions?" - r' "?(?P((\d+)(,\s)?)+)"?', - ), - converters={"ids": lambda s: list(map(int, s.split(",")))}, - target_fixture="srv", -) -def the_mock_server_is_started_with_interactions( # noqa: C901 - ids: list[int], - interaction_definitions: dict[int, InteractionDefinition], -) -> Generator[PactServer, Any, None]: - """The mock server is started with interactions.""" - pact = Pact("consumer", "provider") - pact.with_specification("V1") - for iid in ids: - definition = interaction_definitions[iid] - logging.info("Adding interaction %s", iid) - - interaction = pact.upon_receiving(f"interactions {iid}") - logging.info("-> with_request(%s, %s)", definition.method, definition.path) - interaction.with_request(definition.method, definition.path) - - if definition.query: - query = URL.build(query_string=definition.query).query - logging.info("-> with_query_parameters(%s)", query.items()) - interaction.with_query_parameters(query.items()) - - if definition.headers: - logging.info("-> with_headers(%s)", definition.headers.items()) - interaction.with_headers(definition.headers.items()) - - if definition.body: - if definition.body.string: - logging.info( - "-> with_body(%s, %s)", - truncate(definition.body.string), - definition.body.mime_type, - ) - interaction.with_body( - definition.body.string, - definition.body.mime_type, - ) - elif definition.body.bytes: - logging.info( - "-> with_binary_file(%s, %s)", - truncate(definition.body.bytes), - definition.body.mime_type, - ) - interaction.with_binary_body( - definition.body.bytes, - definition.body.mime_type, - ) - else: - msg = "Unexpected body definition" - raise RuntimeError(msg) - - logging.info("-> will_respond_with(%s)", definition.response) - interaction.will_respond_with(definition.response) - - if definition.response_content: - if definition.response_body is None: - msg = "Expected response body along with response content type" - raise ValueError(msg) - - if definition.response_body.string: - logging.info( - "-> with_body(%s, %s)", - truncate(definition.response_body.string), - definition.response_content, - ) - interaction.with_body( - definition.response_body.string, - definition.response_content, - ) - elif definition.response_body.bytes: - logging.info( - "-> with_binary_file(%s, %s)", - truncate(definition.response_body.bytes), - definition.response_content, - ) - interaction.with_binary_body( - definition.response_body.bytes, - definition.response_content, - ) - else: - msg = "Unexpected body definition" - raise RuntimeError(msg) - - with pact.serve(raises=False) as srv: - yield srv - - -@when( - parsers.re( - r"request (?P\d+) is made to the mock server", - ), - converters={"request_id": int}, - target_fixture="response", -) -def request_n_is_made_to_the_mock_server( - interaction_definitions: dict[int, InteractionDefinition], - request_id: int, - srv: PactServer, -) -> requests.Response: - """ - Request n is made to the mock server. - """ - definition = interaction_definitions[request_id] - if ( - definition.body - and definition.body.mime_type - and "Content-Type" not in definition.headers - ): - definition.headers.add("Content-Type", definition.body.mime_type) - - return requests.request( - definition.method, - str(srv.url.with_path(definition.path)), - params=( - URL.build(query_string=definition.query).query if definition.query else None - ), - headers=definition.headers if definition.headers else None, # type: ignore[arg-type] - data=definition.body.bytes if definition.body else None, - ) - - -@when( - parsers.re( - r"request (?P\d+) is made to the mock server" - r" with the following changes?:\n(?P.*)", - re.DOTALL, - ), - converters={"request_id": int}, - target_fixture="response", -) -def request_n_is_made_to_the_mock_server_with_the_following_changes( - interaction_definitions: dict[int, InteractionDefinition], - request_id: int, - content: str, - srv: PactServer, -) -> requests.Response: - """ - Request n is made to the mock server with changes. - - The content is a markdown table with a subset of the columns defining the - definition (as in the given step). - """ - definition = interaction_definitions[request_id] - rows = [ - list(map(str.strip, row.split("|")))[1:-1] - for row in content.split("\n") - if row.strip() - ] - assert len(rows) == 2, "Expected two rows in the table" - updates = dict(zip(rows[0], rows[1])) - definition.update(**updates) - - if ( - definition.body - and definition.body.mime_type - and "Content-Type" not in definition.headers - ): - definition.headers.add("Content-Type", definition.body.mime_type) - - return requests.request( - definition.method, - str(srv.url.with_path(definition.path)), - params=( - URL.build(query_string=definition.query).query if definition.query else None - ), - headers=definition.headers if definition.headers else None, # type: ignore[arg-type] - data=definition.body.bytes if definition.body else None, - ) - +request_n_is_made_to_the_mock_server() +request_n_is_made_to_the_mock_server_with_the_following_changes() +the_mock_server_is_started_with_interactions("V1") +the_pact_test_is_done() ################################################################################ ## Then ################################################################################ - -@then( - parsers.re( - r"a (?P\d+) (success|error) response is returned", - ), - converters={"code": int}, -) -def a_response_is_returned( - response: requests.Response, - code: int, - srv: PactServer, -) -> None: - """ - A response is returned. - """ - logging.info("Request Information:") - logging.info("-> Method: %s", response.request.method) - logging.info("-> URL: %s", response.request.url) - logging.info( - "-> Headers: %s", - json.dumps( - dict(**response.request.headers), - indent=2, - ), - ) - logging.info( - "-> Body: %s", - truncate(response.request.body) if response.request.body else None, - ) - logging.info("Mismatches:\n%s", json.dumps(srv.mismatches, indent=2)) - assert response.status_code == code - - -@then( - parsers.re( - r'the payload will contain the "(?P[^"]+)" JSON document', - ), -) -def the_payload_will_contain_the_json_document( - response: requests.Response, - file: str, -) -> None: - """ - The payload will contain the JSON document. - """ - path = FIXTURES_ROOT / f"{file}.json" - assert response.json() == json.loads(path.read_text()) - - -@then( - parsers.re( - r'the content type will be set as "(?P[^"]+)"', - ), -) -def the_content_type_will_be_set_as( - response: requests.Response, - content_type: str, -) -> None: - assert response.headers["Content-Type"] == content_type - - -@when("the pact test is done") -def the_pact_test_is_done() -> None: - """ - The pact test is done. - """ - - -@then( - parsers.re(r"the mock server status will (?P(NOT )?)be OK"), - converters={"negated": lambda s: s == "NOT "}, -) -def the_mock_server_status_will_be( - srv: PactServer, - negated: bool, # noqa: FBT001 -) -> None: - """ - The mock server status will be. - """ - assert srv.matched is not negated - - -@then( - parsers.re( - r"the mock server status will be" - r" an expected but not received error" - r" for interaction \{(?P\d+)\}", - ), - converters={"n": int}, -) -def the_mock_server_status_will_be_an_expected_but_not_received_error_for_interaction_n( - srv: PactServer, - n: int, - interaction_definitions: dict[int, InteractionDefinition], -) -> None: - """ - The mock server status will be an expected but not received error for interaction n. - """ - assert srv.matched is False - assert len(srv.mismatches) > 0 - - for mismatch in srv.mismatches: - if ( - mismatch["method"] == interaction_definitions[n].method - and mismatch["path"] == interaction_definitions[n].path - and mismatch["type"] == "missing-request" - ): - return - pytest.fail("Expected mismatch not found") - - -@then( - parsers.re( - r"the mock server status will be" - r' an unexpected "(?P[^"]+)" request received error' - r" for interaction \{(?P\d+)\}", - ), - converters={"n": int}, -) -def the_mock_server_status_will_be_an_unexpected_request_received_for_interaction_n( - srv: PactServer, - method: str, - n: int, - interaction_definitions: dict[int, InteractionDefinition], -) -> None: - """ - The mock server status will be an expected but not received error for interaction n. - """ - assert srv.matched is False - assert len(srv.mismatches) > 0 - - for mismatch in srv.mismatches: - if ( - mismatch["method"] == interaction_definitions[n].method - and mismatch["request"]["method"] == method - and mismatch["path"] == interaction_definitions[n].path - and mismatch["type"] == "request-not-found" - ): - return - pytest.fail("Expected mismatch not found") - - -@then( - parsers.re( - r"the mock server status will be" - r' an unexpected "(?P[^"]+)" request received error' - r' for path "(?P[^"]+)"', - ), - converters={"n": int}, -) -def the_mock_server_status_will_be_an_unexpected_request_received_for_path( - srv: PactServer, - method: str, - path: str, -) -> None: - """ - The mock server status will be an expected but not received error for interaction n. - """ - assert srv.matched is False - assert len(srv.mismatches) > 0 - - for mismatch in srv.mismatches: - if ( - mismatch["request"]["method"] == method - and mismatch["path"] == path - and mismatch["type"] == "request-not-found" - ): - return - pytest.fail("Expected mismatch not found") - - -@then("the mock server status will be mismatches") -def the_mock_server_status_will_be_mismatches( - srv: PactServer, -) -> None: - """ - The mock server status will be mismatches. - """ - assert srv.matched is False - assert len(srv.mismatches) > 0 - - -@then( - parsers.re( - r'the mismatches will contain a "(?P[^"]+)" mismatch' - r' with error "(?P[^"]+)"', - ), -) -def the_mismatches_will_contain_a_mismatch_with_the_error( - srv: PactServer, - mismatch_type: str, - error: str, -) -> None: - """ - The mismatches will contain a mismatch with the error. - """ - if mismatch_type == "query": - mismatch_type = "QueryMismatch" - elif mismatch_type == "header": - mismatch_type = "HeaderMismatch" - elif mismatch_type == "body": - mismatch_type = "BodyMismatch" - elif mismatch_type == "body-content-type": - mismatch_type = "BodyTypeMismatch" - else: - msg = f"Unexpected mismatch type: {mismatch_type}" - raise ValueError(msg) - - logger.info("Expecting mismatch: %s", mismatch_type) - logger.info("With error: %s", error) - for mismatch in srv.mismatches: - for sub_mismatch in mismatch["mismatches"]: - if ( - error in sub_mismatch["mismatch"] - and sub_mismatch["type"] == mismatch_type - ): - return - pytest.fail("Expected mismatch not found") - - -@then( - parsers.re( - r'the mismatches will contain a "(?P[^"]+)" mismatch' - r' with path "(?P[^"]+)"' - r' with error "(?P[^"]+)"', - ), -) -def the_mismatches_will_contain_a_mismatch_with_path_with_the_error( - srv: PactServer, - mismatch_type: str, - path: str, - error: str, -) -> None: - """ - The mismatches will contain a mismatch with the error. - """ - mismatch_type = "BodyMismatch" if mismatch_type == "body" else mismatch_type - for mismatch in srv.mismatches: - for sub_mismatch in mismatch["mismatches"]: - if ( - sub_mismatch["mismatch"] == error - and sub_mismatch["type"] == mismatch_type - and sub_mismatch["path"] == path - ): - return - pytest.fail("Expected mismatch not found") - - -@then( - parsers.re( - r"the mock server will (?P(NOT )?)write out" - r" a Pact file for the interactions? when done", - ), - converters={"negated": lambda s: s == "NOT "}, - target_fixture="pact_file", -) -def the_mock_server_will_write_out_a_pact_file_for_the_interaction_when_done( - srv: PactServer, - temp_dir: Path, - negated: bool, # noqa: FBT001 -) -> dict[str, Any] | None: - """ - The mock server will write out a Pact file for the interaction when done. - """ - if not negated: - srv.write_file(temp_dir) - output = temp_dir / "consumer-provider.json" - assert output.is_file() - return json.load(output.open()) - return None - - -@then( - parsers.re(r"the pact file will contain \{(?P\d+)\} interactions?"), - converters={"n": int}, -) -def the_pact_file_will_contain_n_interactions( - pact_file: dict[str, Any], - n: int, -) -> None: - """ - The pact file will contain n interactions. - """ - assert len(pact_file["interactions"]) == n - - -@then( - parsers.re( - r"the \{(?P\w+)\} interaction response" - r' will contain the "(?P[^"]+)" document', - ), - converters={"n": string_to_int}, -) -def the_nth_interaction_will_contain_the_document( - pact_file: dict[str, Any], - n: int, - file: str, -) -> None: - """ - The nth interaction response will contain the document. - """ - file_path = FIXTURES_ROOT / file - if file.endswith(".json"): - assert pact_file["interactions"][n - 1]["response"]["body"] == json.load( - file_path.open(), - ) - - -@then( - parsers.re( - r'the \{(?P\w+)\} interaction request will be for a "(?P[A-Z]+)"', - ), - converters={"n": string_to_int}, -) -def the_nth_interaction_request_will_be_for_method( - pact_file: dict[str, Any], - n: int, - method: str, -) -> None: - """ - The nth interaction request will be for a method. - """ - assert pact_file["interactions"][n - 1]["request"]["method"] == method - - -@then( - parsers.re( - r"the \{(?P\w+)\} interaction request" - r' query parameters will be "(?P[^"]+)"', - ), - converters={"n": string_to_int}, -) -def the_nth_interaction_request_query_parameters_will_be( - pact_file: dict[str, Any], - n: int, - query: str, -) -> None: - """ - The nth interaction request query parameters will be. - """ - assert query == pact_file["interactions"][n - 1]["request"]["query"] - - -@then( - parsers.re( - r"the \{(?P\w+)\} interaction request" - r' will contain the header "(?P[^"]+)"' - r' with value "(?P[^"]+)"', - ), - converters={"n": string_to_int}, -) -def the_nth_interaction_request_will_contain_the_header( - pact_file: dict[str, Any], - n: int, - key: str, - value: str, -) -> None: - """ - The nth interaction request will contain the header. - """ - expected = {key: value} - actual = pact_file["interactions"][n - 1]["request"]["headers"] - assert expected.keys() == actual.keys() - for key in expected: - assert expected[key] == actual[key] or [expected[key]] == actual[key] - - -@then( - parsers.re( - r"the \{(?P\w+)\} interaction request" - r' content type will be "(?P[^"]+)"', - ), - converters={"n": string_to_int}, -) -def the_nth_interaction_request_content_type_will_be( - pact_file: dict[str, Any], - n: int, - content_type: str, -) -> None: - """ - The nth interaction request will contain the header. - """ - assert ( - pact_file["interactions"][n - 1]["request"]["headers"]["Content-Type"] - == content_type - ) - - -@then( - parsers.re( - r"the \{(?P\w+)\} interaction request" - r' will contain the "(?P[^"]+)" document', - ), - converters={"n": string_to_int}, -) -def the_nth_interaction_request_will_contain_the_document( - pact_file: dict[str, Any], - n: int, - file: str, -) -> None: - """ - The nth interaction request will contain the document. - """ - file_path = FIXTURES_ROOT / file - if file.endswith(".json"): - assert pact_file["interactions"][n - 1]["request"]["body"] == json.load( - file_path.open(), - ) - else: - assert ( - pact_file["interactions"][n - 1]["request"]["body"] == file_path.read_text() - ) +a_response_is_returned() +the_content_type_will_be_set_as() +the_mismatches_will_contain_a_mismatch_with_path_with_the_error() +the_mismatches_will_contain_a_mismatch_with_the_error() +the_mock_server_status_will_be() +the_mock_server_status_will_be_an_expected_but_not_received_error_for_interaction_n() +the_mock_server_status_will_be_an_unexpected_request_received_for_interaction_n() +the_mock_server_status_will_be_an_unexpected_request_received_for_path() +the_mock_server_status_will_be_mismatches() +the_mock_server_will_write_out_a_pact_file_for_the_interaction_when_done() +the_nth_interaction_request_content_type_will_be() +the_nth_interaction_request_query_parameters_will_be() +the_nth_interaction_request_will_be_for_method() +the_nth_interaction_request_will_contain_the_document() +the_nth_interaction_request_will_contain_the_header() +the_nth_interaction_will_contain_the_document() +the_pact_file_will_contain_n_interactions() +the_payload_will_contain_the_json_document() diff --git a/tests/v3/compatiblity_suite/util.py b/tests/v3/compatiblity_suite/util/__init__.py similarity index 59% rename from tests/v3/compatiblity_suite/util.py rename to tests/v3/compatiblity_suite/util/__init__.py index 53be2902c..947dd7cae 100644 --- a/tests/v3/compatiblity_suite/util.py +++ b/tests/v3/compatiblity_suite/util/__init__.py @@ -1,5 +1,22 @@ """ Utility functions to help with testing. + +## Sharing PyTest BDD Steps + +The PyTest BDD library does some 'magic' to make the given/when/then steps +available in the test context. This is done by inspecting the stack frame of the +calling function and injecting the step definition function as a fixture. + +This is a problem when sharing steps between different test suites, as the stack +frame is different. Fortunately, PyTest BDD allows us to specify the stack level +to inspect, so we can use that to our advantage with the following pattern: + +```python +def some_step(stacklevel: int = 1) -> None: + @when(..., stacklevel=stacklevel + 1) + def _(): + # Step definition goes here +``` """ from __future__ import annotations @@ -12,9 +29,14 @@ from xml.etree import ElementTree from multidict import MultiDict +from yarl import URL + +if typing.TYPE_CHECKING: + import pact.v3 + import pact.v3.pact logger = logging.getLogger(__name__) -SUITE_ROOT = Path(__file__).parent / "definition" +SUITE_ROOT = Path(__file__).parent.parent / "definition" FIXTURES_ROOT = SUITE_ROOT / "fixtures" @@ -100,6 +122,33 @@ def truncate(data: str | bytes) -> str: ) +def parse_markdown_table(content: str) -> list[dict[str, str]]: + """ + Parse a Markdown table into a list of dictionaries. + + The table is expected to be in the following format: + + ```markdown + | key1 | key2 | key3 | + | val1 | val2 | val3 | + ``` + + Note that the first row is expected to be the column headers, and the + remaining rows are the values. There is no header/body separation. + """ + rows = [ + list(map(str.strip, row.split("|")))[1:-1] + for row in content.split("\n") + if row.strip() + ] + + if len(rows) < 2: + msg = f"Expected at least two rows in the table, got {len(rows)}" + raise ValueError(msg) + + return [dict(zip(rows[0], row)) for row in rows[1:]] + + class InteractionDefinition: """ Interaction definition. @@ -221,15 +270,16 @@ def __init__(self, **kwargs: str) -> None: self.id: int | None = None self.method: str = kwargs.pop("method") self.path: str = kwargs.pop("path") - self.response: int = int(kwargs.pop("response")) + self.response: int = int(kwargs.pop("response", 200)) self.query: str | None = None self.headers: MultiDict[str] = MultiDict() self.body: InteractionDefinition.Body | None = None self.response_content: str | None = None self.response_body: InteractionDefinition.Body | None = None + self.matching_rules: str | None = None self.update(**kwargs) - def update(self, **kwargs: str) -> None: # noqa: C901 + def update(self, **kwargs: str) -> None: # noqa: C901, PLR0912 """ Update the interaction definition. @@ -238,37 +288,58 @@ def update(self, **kwargs: str) -> None: # noqa: C901 """ if interaction_id := kwargs.pop("No", None): self.id = int(interaction_id) + if method := kwargs.pop("method", None): self.method = method + if path := kwargs.pop("path", None): self.path = path + if query := kwargs.pop("query", None): self.query = query + if headers := kwargs.pop("headers", None): - self.headers = InteractionDefinition.parse_headers(headers) + self.headers = self.parse_headers(headers) + + if headers := ( + kwargs.pop("raw headers", None) or kwargs.pop("raw_headers", None) + ): + self.headers = self.parse_headers(headers) + if body := kwargs.pop("body", None): # When updating the body, we _only_ update the body content, not # the content type. orig_content_type = self.body.mime_type if self.body else None self.body = InteractionDefinition.Body(body) self.body.mime_type = orig_content_type or self.body.mime_type + if content_type := ( kwargs.pop("content_type", None) or kwargs.pop("content type", None) ): if self.body is None: self.body = InteractionDefinition.Body("") self.body.mime_type = content_type + if response := kwargs.pop("response", None): self.response = int(response) + if response_content := ( kwargs.pop("response_content", None) or kwargs.pop("response content", None) ): self.response_content = response_content + if response_body := ( kwargs.pop("response_body", None) or kwargs.pop("response body", None) ): self.response_body = InteractionDefinition.Body(response_body) + if matching_rules := ( + kwargs.pop("matching_rules", None) or kwargs.pop("matching rules", None) + ): + self.matching_rules = InteractionDefinition.parse_matching_rules( + matching_rules + ) + if len(kwargs) > 0: msg = f"Unexpected arguments: {kwargs.keys()}" raise TypeError(msg) @@ -281,8 +352,8 @@ def __repr__(self) -> str: ", ".join(f"{k}={v!r}" for k, v in vars(self).items()), ) - @staticmethod - def parse_headers(headers: str) -> MultiDict[str]: + @classmethod + def parse_headers(cls, headers: str) -> MultiDict[str]: """ Parse the headers. @@ -296,6 +367,112 @@ def parse_headers(headers: str) -> MultiDict[str]: """ kvs: list[tuple[str, str]] = [] for header in headers.split(", "): - k, v = header.strip("'").split(": ") + k, _sep, v = header.strip("'").partition(": ") kvs.append((k, v)) return MultiDict(kvs) + + @classmethod + def parse_matching_rules(cls, matching_rules: str) -> str: + """ + Parse the matching rules. + + The matching rules are in one of two formats: + + - An explicit JSON object, prefixed by `JSON: `. + - A fixture file which contains the matching rules. + """ + if matching_rules.startswith("JSON: "): + return matching_rules[6:] + + with (FIXTURES_ROOT / matching_rules).open("r") as file: + return file.read() + + def add_to_pact(self, pact: pact.v3.Pact, name: str) -> None: # noqa: PLR0912, C901 + """ + Add the interaction to the pact. + + This is a convenience method that allows the interaction definition to + be added to the pact, defining the "upon receiving ... with ... will + respond with ...". + + Args: + pact: + The pact being defined. + + name: + Name for this interaction. Must be unique for the pact. + """ + interaction = pact.upon_receiving(name) + logging.info("with_request(%s, %s)", self.method, self.path) + interaction.with_request(self.method, self.path) + + if self.query: + query = URL.build(query_string=self.query).query + logging.info("with_query_parameters(%s)", query.items()) + interaction.with_query_parameters(query.items()) + + if self.headers: + logging.info("with_headers(%s)", self.headers.items()) + interaction.with_headers(self.headers.items()) + + if self.body: + if self.body.string: + logging.info( + "with_body(%s, %s)", + truncate(self.body.string), + self.body.mime_type, + ) + interaction.with_body( + self.body.string, + self.body.mime_type, + ) + elif self.body.bytes: + logging.info( + "with_binary_file(%s, %s)", + truncate(self.body.bytes), + self.body.mime_type, + ) + interaction.with_binary_body( + self.body.bytes, + self.body.mime_type, + ) + else: + msg = "Unexpected body definition" + raise RuntimeError(msg) + + if self.matching_rules: + logging.info("with_matching_rules(%s)", self.matching_rules) + interaction.with_matching_rules(self.matching_rules) + + if self.response: + logging.info("will_respond_with(%s)", self.response) + interaction.will_respond_with(self.response) + + if self.response_content: + if self.response_body is None: + msg = "Expected response body along with response content type" + raise ValueError(msg) + + if self.response_body.string: + logging.info( + "with_body(%s, %s)", + truncate(self.response_body.string), + self.response_content, + ) + interaction.with_body( + self.response_body.string, + self.response_content, + ) + elif self.response_body.bytes: + logging.info( + "with_binary_file(%s, %s)", + truncate(self.response_body.bytes), + self.response_content, + ) + interaction.with_binary_body( + self.response_body.bytes, + self.response_content, + ) + else: + msg = "Unexpected body definition" + raise RuntimeError(msg) diff --git a/tests/v3/compatiblity_suite/util/consumer.py b/tests/v3/compatiblity_suite/util/consumer.py new file mode 100644 index 000000000..41dde7cfc --- /dev/null +++ b/tests/v3/compatiblity_suite/util/consumer.py @@ -0,0 +1,654 @@ +""" +Utility functions for the consumer tests. +""" + +from __future__ import annotations + +import json +import logging +import re +from typing import TYPE_CHECKING, Any + +import pytest +import requests +from pytest_bdd import parsers, then, when +from yarl import URL + +from pact.v3 import Pact +from tests.v3.compatiblity_suite.util import ( + FIXTURES_ROOT, + parse_markdown_table, + string_to_int, + truncate, +) + +if TYPE_CHECKING: + from collections.abc import Generator + from pathlib import Path + + from pact.v3.pact import PactServer + from tests.v3.compatiblity_suite.util import InteractionDefinition + +logger = logging.getLogger(__name__) + +################################################################################ +## When +################################################################################ + + +def the_mock_server_is_started_with_interactions( + version: str, + stacklevel: int = 1, +) -> None: + @when( + parsers.re( + r"the mock server is started" + r" with interactions?" + r' "?(?P((\d+)(,\s)?)+)"?', + ), + converters={"ids": lambda s: list(map(int, s.split(",")))}, + target_fixture="srv", + stacklevel=stacklevel + 1, + ) + def _( + ids: list[int], + interaction_definitions: dict[int, InteractionDefinition], + ) -> Generator[PactServer, Any, None]: + """The mock server is started with interactions.""" + pact = Pact("consumer", "provider") + pact.with_specification(version) + for iid in ids: + definition = interaction_definitions[iid] + logging.info("Adding interaction %s", iid) + definition.add_to_pact(pact, f"interaction {iid}") + + with pact.serve(raises=False) as srv: + yield srv + + +def the_mock_server_is_started_with_interaction_n_but_with_the_following_changes( + version: str, + stacklevel: int = 1, +) -> None: + @when( + parsers.re( + r"the mock server is started" + r" with interaction (?P\d+)" + r" but with the following changes?:\n(?P.*)", + re.DOTALL, + ), + converters={"iid": int, "content": parse_markdown_table}, + target_fixture="srv", + stacklevel=stacklevel + 1, + ) + def _( + iid: int, + interaction_definitions: dict[int, InteractionDefinition], + content: list[dict[str, str]], + ) -> Generator[PactServer, Any, None]: + """The mock server is started with interactions.""" + pact = Pact("consumer", "provider") + pact.with_specification(version) + definition = interaction_definitions[iid] + definition.update(**content[0]) + logging.info("Adding modified interaction %s", iid) + definition.add_to_pact(pact, f"interaction {iid}") + + with pact.serve(raises=False) as srv: + yield srv + + +def request_n_is_made_to_the_mock_server(stacklevel: int = 1) -> None: + @when( + parsers.re( + r"request (?P\d+) is made to the mock server", + ), + converters={"request_id": int}, + target_fixture="response", + stacklevel=stacklevel + 1, + ) + def _( + interaction_definitions: dict[int, InteractionDefinition], + request_id: int, + srv: PactServer, + ) -> requests.Response: + """ + Request n is made to the mock server. + """ + definition = interaction_definitions[request_id] + if ( + definition.body + and definition.body.mime_type + and "Content-Type" not in definition.headers + ): + definition.headers.add("Content-Type", definition.body.mime_type) + + return requests.request( + definition.method, + str(srv.url.with_path(definition.path)), + params=( + URL.build(query_string=definition.query).query + if definition.query + else None + ), + headers=definition.headers if definition.headers else None, # type: ignore[arg-type] + data=definition.body.bytes if definition.body else None, + ) + + +def request_n_is_made_to_the_mock_server_with_the_following_changes( + stacklevel: int = 1, +) -> None: + @when( + parsers.re( + r"request (?P\d+) is made to the mock server" + r" with the following changes?:\n(?P.*)", + re.DOTALL, + ), + converters={"request_id": int, "content": parse_markdown_table}, + target_fixture="response", + stacklevel=stacklevel + 1, + ) + def _( + interaction_definitions: dict[int, InteractionDefinition], + request_id: int, + content: list[dict[str, str]], + srv: PactServer, + ) -> requests.Response: + """ + Request n is made to the mock server with changes. + + The content is a markdown table with a subset of the columns defining the + definition (as in the given step). + """ + definition = interaction_definitions[request_id] + assert len(content) == 1, "Expected exactly one row in the table" + definition.update(**content[0]) + + if ( + definition.body + and definition.body.mime_type + and "Content-Type" not in definition.headers + ): + definition.headers.add("Content-Type", definition.body.mime_type) + + return requests.request( + definition.method, + str(srv.url.with_path(definition.path)), + params=( + URL.build(query_string=definition.query).query + if definition.query + else None + ), + headers=definition.headers if definition.headers else None, # type: ignore[arg-type] + data=definition.body.bytes if definition.body else None, + ) + + +def the_pact_test_is_done(stacklevel: int = 1) -> None: + @when("the pact test is done", stacklevel=stacklevel + 1) + def _() -> None: + """ + The pact test is done. + """ + + +################################################################################ +## Then +################################################################################ + + +def a_response_is_returned(stacklevel: int = 1) -> None: + @then( + parsers.re( + r"a (?P\d+) (success|error) response is returned", + ), + converters={"code": int}, + stacklevel=stacklevel + 1, + ) + def _( + response: requests.Response, + code: int, + srv: PactServer, + ) -> None: + """ + A response is returned. + """ + logging.info( + "Request Information:\n%s", + json.dumps( + { + "method": response.request.method, + "url": response.request.url, + "headers": dict(**response.request.headers), + "body": truncate(response.request.body) + if response.request.body + else None, + }, + indent=2, + ), + ) + logging.info("Mismatches:\n%s", json.dumps(srv.mismatches, indent=2)) + assert response.status_code == code + + +def the_payload_will_contain_the_json_document(stacklevel: int = 1) -> None: + @then( + parsers.re( + r'the payload will contain the "(?P[^"]+)" JSON document', + ), + stacklevel=stacklevel + 1, + ) + def _( + response: requests.Response, + file: str, + ) -> None: + """ + The payload will contain the JSON document. + """ + path = FIXTURES_ROOT / f"{file}.json" + assert response.json() == json.loads(path.read_text()) + + +def the_content_type_will_be_set_as(stacklevel: int = 1) -> None: + @then( + parsers.re( + r'the content type will be set as "(?P[^"]+)"', + ), + stacklevel=stacklevel + 1, + ) + def _( + response: requests.Response, + content_type: str, + ) -> None: + assert response.headers["Content-Type"] == content_type + + +def the_mock_server_status_will_be(stacklevel: int = 1) -> None: + @then( + parsers.re(r"the mock server status will (?P(NOT )?)be OK"), + converters={"negated": lambda s: s == "NOT "}, + stacklevel=stacklevel + 1, + ) + def _( + srv: PactServer, + negated: bool, # noqa: FBT001 + ) -> None: + """ + The mock server status will be. + """ + assert srv.matched is not negated + + +def the_mock_server_status_will_be_an_expected_but_not_received_error_for_interaction_n( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + r"the mock server status will be" + r" an expected but not received error" + r" for interaction \{(?P\d+)\}", + ), + converters={"n": int}, + stacklevel=stacklevel + 1, + ) + def _( + srv: PactServer, + n: int, + interaction_definitions: dict[int, InteractionDefinition], + ) -> None: + """ + The mock server status will be an expected but not received error. + """ + assert srv.matched is False + assert len(srv.mismatches) > 0 + + for mismatch in srv.mismatches: + if ( + mismatch["method"] == interaction_definitions[n].method + and mismatch["path"] == interaction_definitions[n].path + and mismatch["type"] == "missing-request" + ): + return + pytest.fail("Expected mismatch not found") + + +def the_mock_server_status_will_be_an_unexpected_request_received_for_interaction_n( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + r"the mock server status will be" + r' an unexpected "(?P[^"]+)" request received error' + r" for interaction \{(?P\d+)\}", + ), + converters={"n": int}, + stacklevel=stacklevel + 1, + ) + def _( + srv: PactServer, + method: str, + n: int, + interaction_definitions: dict[int, InteractionDefinition], + ) -> None: + """ + The mock server status will be an expected but not received error. + """ + assert srv.matched is False + assert len(srv.mismatches) > 0 + + for mismatch in srv.mismatches: + if ( + mismatch["method"] == interaction_definitions[n].method + and mismatch["request"]["method"] == method + and mismatch["path"] == interaction_definitions[n].path + and mismatch["type"] == "request-not-found" + ): + return + pytest.fail("Expected mismatch not found") + + +def the_mock_server_status_will_be_an_unexpected_request_received_for_path( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + r"the mock server status will be" + r' an unexpected "(?P[^"]+)" request received error' + r' for path "(?P[^"]+)"', + ), + converters={"n": int}, + stacklevel=stacklevel + 1, + ) + def _( + srv: PactServer, + method: str, + path: str, + ) -> None: + """ + The mock server status will be an expected but not received. + """ + assert srv.matched is False + assert len(srv.mismatches) > 0 + + for mismatch in srv.mismatches: + if ( + mismatch["request"]["method"] == method + and mismatch["path"] == path + and mismatch["type"] == "request-not-found" + ): + return + pytest.fail("Expected mismatch not found") + + +def the_mock_server_status_will_be_mismatches(stacklevel: int = 1) -> None: + @then( + "the mock server status will be mismatches", + stacklevel=stacklevel + 1, + ) + def _( + srv: PactServer, + ) -> None: + """ + The mock server status will be mismatches. + """ + assert srv.matched is False + assert len(srv.mismatches) > 0 + + +def the_mismatches_will_contain_a_mismatch_with_the_error(stacklevel: int = 1) -> None: + @then( + parsers.re( + r'the mismatches will contain a "(?P[^"]+)" mismatch' + r' with error "(?P[^"]+)"', + ), + stacklevel=stacklevel + 1, + ) + def _( + srv: PactServer, + mismatch_type: str, + error: str, + ) -> None: + """ + The mismatches will contain a mismatch with the error. + """ + if mismatch_type == "query": + mismatch_type = "QueryMismatch" + elif mismatch_type == "header": + mismatch_type = "HeaderMismatch" + elif mismatch_type == "body": + mismatch_type = "BodyMismatch" + elif mismatch_type == "body-content-type": + mismatch_type = "BodyTypeMismatch" + else: + msg = f"Unexpected mismatch type: {mismatch_type}" + raise ValueError(msg) + + logger.info("Expecting mismatch: %s", mismatch_type) + logger.info("With error: %s", error) + for mismatch in srv.mismatches: + for sub_mismatch in mismatch["mismatches"]: + if ( + error in sub_mismatch["mismatch"] + and sub_mismatch["type"] == mismatch_type + ): + return + pytest.fail("Expected mismatch not found") + + +def the_mismatches_will_contain_a_mismatch_with_path_with_the_error( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + r'the mismatches will contain a "(?P[^"]+)" mismatch' + r' with path "(?P[^"]+)"' + r' with error "(?P[^"]+)"', + ), + stacklevel=stacklevel + 1, + ) + def _( + srv: PactServer, + mismatch_type: str, + path: str, + error: str, + ) -> None: + """ + The mismatches will contain a mismatch with the error. + """ + mismatch_type = "BodyMismatch" if mismatch_type == "body" else mismatch_type + for mismatch in srv.mismatches: + for sub_mismatch in mismatch["mismatches"]: + if ( + sub_mismatch["mismatch"] == error + and sub_mismatch["type"] == mismatch_type + and sub_mismatch["path"] == path + ): + return + pytest.fail("Expected mismatch not found") + + +def the_mock_server_will_write_out_a_pact_file_for_the_interaction_when_done( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + r"the mock server will (?P(NOT )?)write out" + r" a Pact file for the interactions? when done", + ), + converters={"negated": lambda s: s == "NOT "}, + target_fixture="pact_file", + stacklevel=stacklevel + 1, + ) + def _( + srv: PactServer, + temp_dir: Path, + negated: bool, # noqa: FBT001 + ) -> dict[str, Any] | None: + """ + The mock server will write out a Pact file for the interaction when done. + """ + if not negated: + srv.write_file(temp_dir) + output = temp_dir / "consumer-provider.json" + assert output.is_file() + return json.load(output.open()) + return None + + +def the_pact_file_will_contain_n_interactions(stacklevel: int = 1) -> None: + @then( + parsers.re(r"the pact file will contain \{(?P\d+)\} interactions?"), + converters={"n": int}, + stacklevel=stacklevel + 1, + ) + def _( + pact_file: dict[str, Any], + n: int, + ) -> None: + """ + The pact file will contain n interactions. + """ + assert len(pact_file["interactions"]) == n + + +def the_nth_interaction_will_contain_the_document(stacklevel: int = 1) -> None: + @then( + parsers.re( + r"the \{(?P\w+)\} interaction response" + r' will contain the "(?P[^"]+)" document', + ), + converters={"n": string_to_int}, + stacklevel=stacklevel + 1, + ) + def _( + pact_file: dict[str, Any], + n: int, + file: str, + ) -> None: + """ + The nth interaction response will contain the document. + """ + file_path = FIXTURES_ROOT / file + if file.endswith(".json"): + assert pact_file["interactions"][n - 1]["response"]["body"] == json.load( + file_path.open(), + ) + + +def the_nth_interaction_request_will_be_for_method(stacklevel: int = 1) -> None: + @then( + parsers.re( + r"the \{(?P\w+)\} interaction request" + r' will be for a "(?P[A-Z]+)"', + ), + converters={"n": string_to_int}, + stacklevel=stacklevel + 1, + ) + def _( + pact_file: dict[str, Any], + n: int, + method: str, + ) -> None: + """ + The nth interaction request will be for a method. + """ + assert pact_file["interactions"][n - 1]["request"]["method"] == method + + +def the_nth_interaction_request_query_parameters_will_be(stacklevel: int = 1) -> None: + @then( + parsers.re( + r"the \{(?P\w+)\} interaction request" + r' query parameters will be "(?P[^"]+)"', + ), + converters={"n": string_to_int}, + stacklevel=stacklevel + 1, + ) + def _( + pact_file: dict[str, Any], + n: int, + query: str, + ) -> None: + """ + The nth interaction request query parameters will be. + """ + assert query == pact_file["interactions"][n - 1]["request"]["query"] + + +def the_nth_interaction_request_will_contain_the_header(stacklevel: int = 1) -> None: + @then( + parsers.re( + r"the \{(?P\w+)\} interaction request" + r' will contain the header "(?P[^"]+)"' + r' with value "(?P[^"]+)"', + ), + converters={"n": string_to_int}, + stacklevel=stacklevel + 1, + ) + def _( + pact_file: dict[str, Any], + n: int, + key: str, + value: str, + ) -> None: + """ + The nth interaction request will contain the header. + """ + expected = {key: value} + actual = pact_file["interactions"][n - 1]["request"]["headers"] + assert expected.keys() == actual.keys() + for key in expected: + assert expected[key] == actual[key] or [expected[key]] == actual[key] + + +def the_nth_interaction_request_content_type_will_be(stacklevel: int = 1) -> None: + @then( + parsers.re( + r"the \{(?P\w+)\} interaction request" + r' content type will be "(?P[^"]+)"', + ), + converters={"n": string_to_int}, + stacklevel=stacklevel + 1, + ) + def _( + pact_file: dict[str, Any], + n: int, + content_type: str, + ) -> None: + """ + The nth interaction request will contain the header. + """ + assert ( + pact_file["interactions"][n - 1]["request"]["headers"]["Content-Type"] + == content_type + ) + + +def the_nth_interaction_request_will_contain_the_document(stacklevel: int = 1) -> None: + @then( + parsers.re( + r"the \{(?P\w+)\} interaction request" + r' will contain the "(?P[^"]+)" document', + ), + converters={"n": string_to_int}, + stacklevel=stacklevel + 1, + ) + def _( + pact_file: dict[str, Any], + n: int, + file: str, + ) -> None: + """ + The nth interaction request will contain the document. + """ + file_path = FIXTURES_ROOT / file + if file.endswith(".json"): + assert pact_file["interactions"][n - 1]["request"]["body"] == json.load( + file_path.open(), + ) + else: + assert ( + pact_file["interactions"][n - 1]["request"]["body"] + == file_path.read_text() + ) From 928f478bb807dbd3e3ad86739e316be7423aebb5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 8 Feb 2024 12:44:19 +1100 Subject: [PATCH 0192/1376] chore(test/v3): add v2 consumer compatibility suite Signed-off-by: JP-Ellis --- .../v3/compatiblity_suite/test_v2_consumer.py | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 tests/v3/compatiblity_suite/test_v2_consumer.py diff --git a/tests/v3/compatiblity_suite/test_v2_consumer.py b/tests/v3/compatiblity_suite/test_v2_consumer.py new file mode 100644 index 000000000..61f0d02c9 --- /dev/null +++ b/tests/v3/compatiblity_suite/test_v2_consumer.py @@ -0,0 +1,232 @@ +"""Basic HTTP consumer feature tests.""" + +from __future__ import annotations + +import logging + +from pytest_bdd import given, parsers, scenario + +from tests.v3.compatiblity_suite.util import InteractionDefinition, parse_markdown_table +from tests.v3.compatiblity_suite.util.consumer import ( + a_response_is_returned, + request_n_is_made_to_the_mock_server, + request_n_is_made_to_the_mock_server_with_the_following_changes, + the_content_type_will_be_set_as, + the_mismatches_will_contain_a_mismatch_with_path_with_the_error, + the_mismatches_will_contain_a_mismatch_with_the_error, + the_mock_server_is_started_with_interaction_n_but_with_the_following_changes, + the_mock_server_is_started_with_interactions, + the_mock_server_status_will_be, + the_mock_server_status_will_be_an_expected_but_not_received_error_for_interaction_n, + the_mock_server_status_will_be_an_unexpected_request_received_for_interaction_n, + the_mock_server_status_will_be_an_unexpected_request_received_for_path, + the_mock_server_status_will_be_mismatches, + the_mock_server_will_write_out_a_pact_file_for_the_interaction_when_done, + the_nth_interaction_request_content_type_will_be, + the_nth_interaction_request_query_parameters_will_be, + the_nth_interaction_request_will_be_for_method, + the_nth_interaction_request_will_contain_the_document, + the_nth_interaction_request_will_contain_the_header, + the_nth_interaction_will_contain_the_document, + the_pact_file_will_contain_n_interactions, + the_pact_test_is_done, + the_payload_will_contain_the_json_document, +) + +logger = logging.getLogger(__name__) + +################################################################################ +## Scenario +################################################################################ + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Supports a regex matcher (negative case)", +) +def test_supports_a_regex_matcher_negative_case() -> None: + """Supports a regex matcher (negative case).""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Supports a regex matcher (positive case)", +) +def test_supports_a_regex_matcher_positive_case() -> None: + """Supports a regex matcher (positive case).""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Supports a type matcher (negative case)", +) +def test_supports_a_type_matcher_negative_case() -> None: + """Supports a type matcher (negative case).""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Supports a type matcher (positive case)", +) +def test_supports_a_type_matcher_positive_case() -> None: + """Supports a type matcher (positive case).""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Supports matchers for repeated request headers (negative case)", +) +def test_supports_matchers_for_repeated_request_headers_negative_case() -> None: + """Supports matchers for repeated request headers (negative case).""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Supports matchers for repeated request headers (positive case)", +) +def test_supports_matchers_for_repeated_request_headers_positive_case() -> None: + """Supports matchers for repeated request headers (positive case).""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Supports matchers for repeated request query parameters (negative case)", +) +def test_supports_matchers_for_repeated_request_query_parameters_negative_case() -> ( + None +): + """Supports matchers for repeated request query parameters (negative case).""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Supports matchers for repeated request query parameters (positive case)", +) +def test_supports_matchers_for_repeated_request_query_parameters_positive_case() -> ( + None +): + """Supports matchers for repeated request query parameters (positive case).""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Supports matchers for request bodies", +) +def test_supports_matchers_for_request_bodies() -> None: + """Supports matchers for request bodies.""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Supports matchers for request headers", +) +def test_supports_matchers_for_request_headers() -> None: + """Supports matchers for request headers.""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Supports matchers for request query parameters", +) +def test_supports_matchers_for_request_query_parameters() -> None: + """Supports matchers for request query parameters.""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Type matchers cascade to children (negative case)", +) +def test_type_matchers_cascade_to_children_negative_case() -> None: + """Type matchers cascade to children (negative case).""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Type matchers cascade to children (positive case)", +) +def test_type_matchers_cascade_to_children_positive_case() -> None: + """Type matchers cascade to children (positive case).""" + + +@scenario( + "definition/features/V2/http_consumer.feature", + "Supports a matcher for request paths", +) +def test_supports_a_matcher_for_request_paths() -> None: + """Supports a matcher for request paths.""" + + +################################################################################ +## Given +################################################################################ + + +@given( + parsers.parse("the following HTTP interactions have been defined:\n{content}"), + target_fixture="interaction_definitions", +) +def the_following_http_interactions_have_been_defined( + content: str, +) -> dict[int, InteractionDefinition]: + """ + Parse the HTTP interactions table into a dictionary. + + The table columns are expected to be: + + - No + - method + - path + - query + - headers + - body + - matching rules + + The first row is ignored, as it is assumed to be the column headers. The + order of the columns is similarly ignored. + """ + table = parse_markdown_table(content) + + # Check that the table is well-formed + assert len(table[0]) == 7, f"Expected 7 columns, got {len(table[0])}" + assert "No" in table[0], "'No' column not found" + + # Parse the table into a more useful format + interactions: dict[int, InteractionDefinition] = {} + for row in table: + interactions[int(row["No"])] = InteractionDefinition(**row) + + return interactions + + +################################################################################ +## When +################################################################################ + +request_n_is_made_to_the_mock_server() +request_n_is_made_to_the_mock_server_with_the_following_changes() +the_mock_server_is_started_with_interactions("V2") +the_mock_server_is_started_with_interaction_n_but_with_the_following_changes("V2") +the_pact_test_is_done() + +################################################################################ +## Then +################################################################################ + +a_response_is_returned() +the_content_type_will_be_set_as() +the_mismatches_will_contain_a_mismatch_with_path_with_the_error() +the_mismatches_will_contain_a_mismatch_with_the_error() +the_mock_server_status_will_be() +the_mock_server_status_will_be_an_expected_but_not_received_error_for_interaction_n() +the_mock_server_status_will_be_an_unexpected_request_received_for_interaction_n() +the_mock_server_status_will_be_an_unexpected_request_received_for_path() +the_mock_server_status_will_be_mismatches() +the_mock_server_will_write_out_a_pact_file_for_the_interaction_when_done() +the_nth_interaction_request_content_type_will_be() +the_nth_interaction_request_query_parameters_will_be() +the_nth_interaction_request_will_be_for_method() +the_nth_interaction_request_will_contain_the_document() +the_nth_interaction_request_will_contain_the_header() +the_nth_interaction_will_contain_the_document() +the_pact_file_will_contain_n_interactions() +the_payload_will_contain_the_json_document() From f02ea24321f41ea752512a2aec83350f96ebc7a1 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 9 Feb 2024 13:20:38 +0000 Subject: [PATCH 0193/1376] chore(deps): update to pact-ruby-standalone-v2.4.0 Updates Ruby 3.3.0 (from Ruby 3.2.2) & OpenSSL 3.2.0 (from OpenSSL 1.1.1) Drops requirement for bash See full changelog v2.0.1 -> v2.4.0 https://github.com/pact-foundation/pact-ruby-standalone/blob/master/CHANGELOG.md --- hatch_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hatch_build.py b/hatch_build.py index c6e52b91d..fef17641c 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -31,7 +31,7 @@ # Latest version available at: # https://github.com/pact-foundation/pact-ruby-standalone/releases -PACT_BIN_VERSION = os.getenv("PACT_BIN_VERSION", "2.1.0") +PACT_BIN_VERSION = os.getenv("PACT_BIN_VERSION", "2.4.0") PACT_BIN_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" # Latest version available at: From e06188c24e570fd054e7794fdd9d620eb2ad86c2 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Tue, 13 Feb 2024 11:33:42 +0000 Subject: [PATCH 0194/1376] chore(deps): update to pact-ruby-standalone-v2.4.1 --- hatch_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hatch_build.py b/hatch_build.py index fef17641c..587ac6cda 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -31,7 +31,7 @@ # Latest version available at: # https://github.com/pact-foundation/pact-ruby-standalone/releases -PACT_BIN_VERSION = os.getenv("PACT_BIN_VERSION", "2.4.0") +PACT_BIN_VERSION = os.getenv("PACT_BIN_VERSION", "2.4.1") PACT_BIN_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" # Latest version available at: From 2a61fbb593a07dca051b5ac492478143c1f69bd3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 19:34:34 +0000 Subject: [PATCH 0195/1376] chore(deps): update endbug/label-sync digest to 5207415 --- .github/workflows/labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 48d8876b1..f336eab06 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Synchronize labels - uses: EndBug/label-sync@da00f2c11fdb78e4fae44adac2fdd713778ea3e8 # v2 + uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2 with: config-file: | https://raw.githubusercontent.com/pact-foundation/.github/master/.github/labels.yml From 4ed754b3657962e6936e65154c7a2c3dfe6d280a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Feb 2024 00:39:57 +0000 Subject: [PATCH 0196/1376] chore(deps): update dependency dev/ruff to v0.2.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1ae52b295..0244f2f89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ test = [ "pytest-cov ~= 4.0", "testcontainers ~= 3.0", ] -dev = ["pact-python[types]", "pact-python[test]", "ruff==0.2.1"] +dev = ["pact-python[types]", "pact-python[test]", "ruff==0.2.2"] ################################################################################ ## Hatch Build Configuration From 187f1af3b18566f3a51969bb4cf13d5fb19d349c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Feb 2024 00:40:00 +0000 Subject: [PATCH 0197/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.2.2 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd5638888..3f788767f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.1 + rev: v0.2.2 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 561200881e7e36beb01b635a1c687bfbaa4ab883 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 03:45:02 +0000 Subject: [PATCH 0198/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.15.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f788767f..2e6b7544c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.14.1 + rev: v3.15.0 hooks: - id: commitizen stages: [commit-msg] From db024462392281c9cfe998240fc2afae4375d97f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 09:23:31 +0000 Subject: [PATCH 0199/1376] chore(deps): update ubuntu:22.04 docker digest to f9d633f --- Dockerfile.ubuntu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 5d7722897..e2f9580b7 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ubuntu:22.04@sha256:e9569c25505f33ff72e88b2990887c9dcf230f23259da296eb814fc2b41af999 +FROM ubuntu:22.04@sha256:f9d633ff6640178c2d0525017174a688e2c1aef28f0a0130b26bd5554491f0da ENV DEBIAN_FRONTEND=noninteractive ARG PYTHON_VERSION 3.9 From 54e75ab79d8908dbe17b0d501742051c009a8a15 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 21 Feb 2024 14:34:48 +1100 Subject: [PATCH 0200/1376] chore(tests): add v3 consumer compatibility suite Signed-off-by: JP-Ellis --- .../v3/compatiblity_suite/test_v3_consumer.py | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 tests/v3/compatiblity_suite/test_v3_consumer.py diff --git a/tests/v3/compatiblity_suite/test_v3_consumer.py b/tests/v3/compatiblity_suite/test_v3_consumer.py new file mode 100644 index 000000000..370a6757c --- /dev/null +++ b/tests/v3/compatiblity_suite/test_v3_consumer.py @@ -0,0 +1,186 @@ +"""Basic HTTP consumer feature tests.""" + +from __future__ import annotations + +import json +import logging +import re +from typing import TYPE_CHECKING, Any, Generator + +from pytest_bdd import given, parsers, scenario, then, when + +from pact.v3.pact import HttpInteraction, Pact +from tests.v3.compatiblity_suite.util import parse_markdown_table + +if TYPE_CHECKING: + from pathlib import Path + +logger = logging.getLogger(__name__) + +################################################################################ +## Scenario +################################################################################ + + +@scenario( + "definition/features/V3/http_consumer.feature", + "Supports specifying multiple provider states", +) +def test_supports_specifying_multiple_provider_states() -> None: + """Supports specifying multiple provider states.""" + + +@scenario( + "definition/features/V3/http_consumer.feature", + "Supports data for provider states", +) +def test_supports_data_for_provider_states() -> None: + """Supports data for provider states.""" + + +################################################################################ +## Given +################################################################################ + + +@given( + "an integration is being defined for a consumer test", + target_fixture="pact_interaction", +) +def an_integration_is_being_defined_for_a_consumer_test() -> ( + Generator[tuple[Pact, HttpInteraction], Any, None] +): + """An integration is being defined for a consumer test.""" + pact = Pact("consumer", "provider") + pact.with_specification("V3") + yield (pact, pact.upon_receiving("a request")) + + +@given(parsers.re(r'a provider state "(?P[^"]+)" is specified')) +def a_provider_state_is_specified( + pact_interaction: tuple[Pact, HttpInteraction], + state: str, +) -> None: + """A provider state is specified.""" + pact_interaction[1].given(state) + + +@given( + parsers.re( + r'a provider state "(?P[^"]+)" is specified' + r" with the following data:\n(?P.+)", + re.DOTALL, + ), + converters={"table": parse_markdown_table}, +) +def a_provider_state_is_specified_with_the_following_data( + pact_interaction: tuple[Pact, HttpInteraction], + state: str, + table: list[dict[str, Any]], +) -> None: + """A provider state is specified.""" + for row in table: + for key, value in row.items(): + if value.startswith('"') and value.endswith('"'): + row[key] = value[1:-1] + for key, value in row.items(): + if value == "true": + row[key] = True + elif value == "false": + row[key] = False + elif value.isdigit(): + row[key] = int(value) + elif value.replace(".", "", 1).isdigit(): + row[key] = float(value) + + pact_interaction[1].given(state, parameters=table[0]) + + +################################################################################ +## When +################################################################################ + + +@when( + "the Pact file for the test is generated", + target_fixture="pact_data", +) +def the_pact_file_for_the_test_is_generated( + temp_dir: Path, + pact_interaction: tuple[Pact, HttpInteraction], +) -> dict[str, Any]: + """The Pact file for the test is generated.""" + pact_interaction[0].write_file(temp_dir) + with (temp_dir / "consumer-provider.json").open("r") as file: + return json.load(file) + + +################################################################################ +## Then +################################################################################ + + +@then( + parsers.re( + r"the interaction in the Pact file will contain" + r" (?P\d+) provider states?" + ), + converters={"num": int}, +) +def the_interaction_in_the_pact_file_will_container_provider_states( + num: int, + pact_data: dict[str, Any], +) -> None: + """The interaction in the Pact file will container provider states.""" + assert "interactions" in pact_data + assert len(pact_data["interactions"]) == 1 + assert "providerStates" in pact_data["interactions"][0] + assert len(pact_data["interactions"][0]["providerStates"]) == num + + +@then( + parsers.re( + r"the interaction in the Pact file will contain" + r' provider state "(?P[^"]+)"' + ), +) +def the_interaction_in_the_pact_file_will_container_provider_state( + state: str, + pact_data: dict[str, Any], +) -> None: + """The interaction in the Pact file will container provider state.""" + assert "interactions" in pact_data + assert len(pact_data["interactions"]) == 1 + assert "providerStates" in pact_data["interactions"][0] + + assert any( + status["name"] == state + for status in pact_data["interactions"][0]["providerStates"] + ) + + +@then( + parsers.re( + r'the provider state "(?P[^"]+)" in the Pact file' + r" will contain the following parameters:\n(?P
.+)", + re.DOTALL, + ), + converters={"table": parse_markdown_table}, +) +def the_provider_state_in_the_pact_file_will_contain_the_following_parameters( + state: str, + table: list[dict[str, Any]], + pact_data: dict[str, Any], +) -> None: + """The provider state in the Pact file will contain the following parameters.""" + assert "interactions" in pact_data + assert len(pact_data["interactions"]) == 1 + assert "providerStates" in pact_data["interactions"][0] + parameters: dict[str, Any] = json.loads(table[0]["parameters"]) + + provider_state = next( + status + for status in pact_data["interactions"][0]["providerStates"] + if status["name"] == state + ) + assert provider_state["params"] == parameters From 17660554b720f744d1bf50dc22c646ba7a6378af Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 21 Feb 2024 13:57:15 +1100 Subject: [PATCH 0201/1376] feat: determine version from vcs The `hatch-vcs` is an extension to hatch's build system which automatically determines the current version of the project based on the VCS (i.e., git in this instance). Specifically, it looks for the latest `vX.Y.Z` tag and: - If the current `HEAD` is identical to the tag, sets a nice 'clean' version; - If the current `HEAD` is infront of the tag, uses a dev build versioning of the form `vX.Y.Z.devN` where `N` is the number of commits between `HEAD` and the tag. Signed-off-by: JP-Ellis --- .gitignore | 3 +++ pact/__init__.py | 27 +++++++++++++++++++++++---- pact/__version__.py | 3 --- pyproject.toml | 8 ++++++-- 4 files changed, 32 insertions(+), 9 deletions(-) delete mode 100644 pact/__version__.py diff --git a/.gitignore b/.gitignore index cd43d0c34..f93aa99ac 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ pact/bin pact/data +# Version is determined from the VCS +pact/__version__.py + ################################################################################ ## Standard Templates ################################################################################ diff --git a/pact/__init__.py b/pact/__init__.py index e81016348..df07abf83 100644 --- a/pact/__init__.py +++ b/pact/__init__.py @@ -9,8 +9,27 @@ from .provider import Provider from .verifier import Verifier -from .__version__ import __version__ # noqa: F401 +from .__version__ import __version__, __version_tuple__ -__all__ = ('Broker', 'Consumer', 'EachLike', 'Like', - 'MessageConsumer', 'MessagePact', 'MessageProvider', - 'Pact', 'Provider', 'SomethingLike', 'Term', 'Format', 'Verifier') +__url__ = "https://github.com/pactflow/accord" +__license__ = "MIT" + +__all__ = [ + '__version__', + '__version_tuple__', + '__url__', + '__license__', + 'Broker', + 'Consumer', + 'EachLike', + 'Like', + 'MessageConsumer', + 'MessagePact', + 'MessageProvider', + 'Pact', + 'Provider', + 'SomethingLike', + 'Term', + 'Format', + 'Verifier', +] diff --git a/pact/__version__.py b/pact/__version__.py deleted file mode 100644 index 9872c1a15..000000000 --- a/pact/__version__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Pact version info.""" - -__version__ = "2.1.0" diff --git a/pyproject.toml b/pyproject.toml index 0244f2f89..1b5a8c6ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,16 +84,20 @@ dev = ["pact-python[types]", "pact-python[test]", "ruff==0.2.2"] [build-system] requires = [ + "cffi", + "hatch-vcs", "hatchling", "packaging", "requests", - "cffi", "setuptools ; python_version >= '3.12'", ] build-backend = "hatchling.build" [tool.hatch.version] -path = "pact/__version__.py" +source = "vcs" + +[tool.hatch.build.hooks.vcs] +version-file = "pact/__version__.py" [tool.hatch.build] include = ["**/py.typed", "**/*.md", "LICENSE", "pact/**/*.py", "pact/**/*.pyi"] From b74e5499759c75e527e563ee5927f15a0b4a6211 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 21 Feb 2024 14:04:32 +1100 Subject: [PATCH 0202/1376] chore: update metadata Given I have written the `v3` submodule, I have added myself into the list of authors. Signed-off-by: JP-Ellis --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1b5a8c6ce..b39ccd812 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,10 @@ 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" }] +authors = [ + { name = "Matthew Balvanz", email = "matthew.balvanz@workiva.com" }, + { name = "Joshua Ellis", email = "josh@jpellis.me" }, +] maintainers = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] readme = "README.md" From 6ed627eebfab0bb81f3580fd194db2166bdfb8ca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 06:57:53 +0000 Subject: [PATCH 0203/1376] chore(deps): update pactfoundation/pact-broker:latest docker digest to 2c5277c --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15d2bcc6d..bbbc4ff6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,7 +78,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:a341c6af670704b3d0a20a15fcefba62850d915eb9c9dde5e3334879a4541a54 + image: pactfoundation/pact-broker:latest@sha256:2c5277c6c3b2e0903868546eeff4f95b6b40ccafb61ece8375fd93de0c37c743 ports: - "9292:9292" env: From f8df889096fed5abb5b9f9b92a9abcaa0b41ca9d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 21:49:00 +0000 Subject: [PATCH 0204/1376] chore(deps): update codecov/codecov-action digest to 0cfda1d --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bbbc4ff6d..329952d73 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,7 +56,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@e0b68c6749509c5f83f984dd99a76a1c1a231044 # v4 + uses: codecov/codecov-action@0cfda1dd0a4ad9efc75517f399d859cd1ea4ced1 # v4 with: token: ${{ secrets.CODECOV_TOKEN }} From 792d7f52460d490704656bf7be85bc9a54d601ef Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 26 Feb 2024 15:31:42 +1100 Subject: [PATCH 0205/1376] chore(tests): move the_pact_file_for_the_test_is_generated to share util This functionality is shared between the V3 and V4 HTTP consumer tests. Signed-off-by: JP-Ellis --- .../v3/compatiblity_suite/test_v3_consumer.py | 23 +++++-------------- tests/v3/compatiblity_suite/util/consumer.py | 18 ++++++++++++++- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/tests/v3/compatiblity_suite/test_v3_consumer.py b/tests/v3/compatiblity_suite/test_v3_consumer.py index 370a6757c..bffb78706 100644 --- a/tests/v3/compatiblity_suite/test_v3_consumer.py +++ b/tests/v3/compatiblity_suite/test_v3_consumer.py @@ -5,15 +5,15 @@ import json import logging import re -from typing import TYPE_CHECKING, Any, Generator +from typing import Any, Generator -from pytest_bdd import given, parsers, scenario, then, when +from pytest_bdd import given, parsers, scenario, then from pact.v3.pact import HttpInteraction, Pact from tests.v3.compatiblity_suite.util import parse_markdown_table - -if TYPE_CHECKING: - from pathlib import Path +from tests.v3.compatiblity_suite.util.consumer import ( + the_pact_file_for_the_test_is_generated, +) logger = logging.getLogger(__name__) @@ -101,18 +101,7 @@ def a_provider_state_is_specified_with_the_following_data( ################################################################################ -@when( - "the Pact file for the test is generated", - target_fixture="pact_data", -) -def the_pact_file_for_the_test_is_generated( - temp_dir: Path, - pact_interaction: tuple[Pact, HttpInteraction], -) -> dict[str, Any]: - """The Pact file for the test is generated.""" - pact_interaction[0].write_file(temp_dir) - with (temp_dir / "consumer-provider.json").open("r") as file: - return json.load(file) +the_pact_file_for_the_test_is_generated() ################################################################################ diff --git a/tests/v3/compatiblity_suite/util/consumer.py b/tests/v3/compatiblity_suite/util/consumer.py index 41dde7cfc..2488b0cd9 100644 --- a/tests/v3/compatiblity_suite/util/consumer.py +++ b/tests/v3/compatiblity_suite/util/consumer.py @@ -26,7 +26,7 @@ from collections.abc import Generator from pathlib import Path - from pact.v3.pact import PactServer + from pact.v3.pact import HttpInteraction, PactServer from tests.v3.compatiblity_suite.util import InteractionDefinition logger = logging.getLogger(__name__) @@ -193,6 +193,22 @@ def _() -> None: """ +def the_pact_file_for_the_test_is_generated(stacklevel: int = 1) -> None: + @when( + "the Pact file for the test is generated", + target_fixture="pact_data", + stacklevel=stacklevel + 1, + ) + def _( + temp_dir: Path, + pact_interaction: tuple[Pact, HttpInteraction], + ) -> dict[str, Any]: + """The Pact file for the test is generated.""" + pact_interaction[0].write_file(temp_dir) + with (temp_dir / "consumer-provider.json").open("r") as file: + return json.load(file) + + ################################################################################ ## Then ################################################################################ From 273189c71c67688179d27d578a388ea4c34d665f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 28 Feb 2024 10:02:51 +1100 Subject: [PATCH 0206/1376] feat(v3): upgrade ffi to 0.4.18 This introduces three new methods to the Interaction class: - `set_key` - `set_pending` - `set_comment` These are all used by V4 pacts. Signed-off-by: JP-Ellis --- hatch_build.py | 2 +- pact/v3/ffi.py | 651 +++++++++++++++++++++++++++--------------------- pact/v3/pact.py | 47 +++- 3 files changed, 414 insertions(+), 286 deletions(-) diff --git a/hatch_build.py b/hatch_build.py index 587ac6cda..303d98611 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -36,7 +36,7 @@ # Latest version available at: # https://github.com/pact-foundation/pact-reference/releases -PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.15") +PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.18") PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{machine}.{ext}" diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index 7a47b9d89..f860d68d6 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -127,7 +127,7 @@ class InteractionHandle: Handle to a HTTP Interaction. [Rust - `InteractionHandle`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/mock_server/handles/struct.InteractionHandle.html) + `InteractionHandle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/mock_server/handles/struct.InteractionHandle.html) """ def __init__(self, ref: int) -> None: @@ -218,7 +218,7 @@ class PactHandle: Handle to a Pact. [Rust - `PactHandle`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/mock_server/handles/struct.PactHandle.html) + `PactHandle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/mock_server/handles/struct.PactHandle.html) """ def __init__(self, ref: int) -> None: @@ -535,7 +535,7 @@ class ExpressionValueType(Enum): """ Expression Value Type. - [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/models/expressions/enum.ExpressionValueType.html) + [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/models/expressions/enum.ExpressionValueType.html) """ UNKNOWN = lib.ExpressionValueType_Unknown @@ -562,7 +562,7 @@ class GeneratorCategory(Enum): """ Generator Category. - [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/models/generators/enum.GeneratorCategory.html) + [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/models/generators/enum.GeneratorCategory.html) """ METHOD = lib.GeneratorCategory_METHOD @@ -590,7 +590,7 @@ class InteractionPart(Enum): """ Interaction Part. - [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/mock_server/handles/enum.InteractionPart.html) + [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/mock_server/handles/enum.InteractionPart.html) """ REQUEST = lib.InteractionPart_Request @@ -636,7 +636,7 @@ class MatchingRuleCategory(Enum): """ Matching Rule Category. - [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) + [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) """ METHOD = lib.MatchingRuleCategory_METHOD @@ -665,7 +665,7 @@ class PactSpecification(Enum): """ Pact Specification. - [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/models/pact_specification/enum.PactSpecification.html) + [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/models/pact_specification/enum.PactSpecification.html) """ UNKNOWN = lib.PactSpecification_Unknown @@ -697,7 +697,7 @@ class _StringResult(Enum): """ Internal enum from Pact FFI. - [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/mock_server/enum.StringResult.html) + [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/mock_server/enum.StringResult.html) """ FAILED = lib.StringResult_Failed @@ -844,7 +844,7 @@ def version() -> str: """ Return the version of the pact_ffi library. - [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_version) + [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_version) Returns: The version of the pact_ffi library as a string, in the form of `x.y.z`. @@ -864,7 +864,7 @@ def init(log_env_var: str) -> None: tracing subscriber. [Rust - `pactffi_init`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_init) + `pactffi_init`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_init) # Safety @@ -881,7 +881,7 @@ def init_with_log_level(level: str = "INFO") -> None: tracing subscriber. [Rust - `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_init_with_log_level) + `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_init_with_log_level) Args: level: @@ -901,7 +901,7 @@ def enable_ansi_support() -> None: On non-Windows platforms, this function is a no-op. [Rust - `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_enable_ansi_support) + `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_enable_ansi_support) # Safety @@ -919,7 +919,7 @@ def log_message( Log using the shared core logging facility. [Rust - `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_log_message) + `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_log_message) This is useful for callers to have a single set of logs. @@ -953,7 +953,7 @@ def match_message(msg_1: Message, msg_2: Message) -> Mismatches: If the messages match, the returned collection will be empty. [Rust - `pactffi_match_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_match_message) + `pactffi_match_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_match_message) """ raise NotImplementedError @@ -963,7 +963,7 @@ def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: Get an iterator over mismatches. [Rust - `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatches_get_iter) + `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatches_get_iter) """ raise NotImplementedError @@ -972,7 +972,7 @@ def mismatches_delete(mismatches: Mismatches) -> None: """ Delete mismatches. - [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatches_delete) + [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatches_delete) """ raise NotImplementedError @@ -981,7 +981,7 @@ def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: """ Get the next mismatch from a mismatches iterator. - [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatches_iter_next) + [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatches_iter_next) Returns a null pointer if no mismatches remain. """ @@ -992,7 +992,7 @@ def mismatches_iter_delete(iter: MismatchesIterator) -> None: """ Delete a mismatches iterator when you're done with it. - [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatches_iter_delete) + [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatches_iter_delete) """ raise NotImplementedError @@ -1001,7 +1001,7 @@ def mismatch_to_json(mismatch: Mismatch) -> str: """ Get a JSON representation of the mismatch. - [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatch_to_json) + [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatch_to_json) """ raise NotImplementedError @@ -1010,7 +1010,7 @@ def mismatch_type(mismatch: Mismatch) -> str: """ Get the type of a mismatch. - [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatch_type) + [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatch_type) """ raise NotImplementedError @@ -1019,7 +1019,7 @@ def mismatch_summary(mismatch: Mismatch) -> str: """ Get a summary of a mismatch. - [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatch_summary) + [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatch_summary) """ raise NotImplementedError @@ -1028,7 +1028,7 @@ def mismatch_description(mismatch: Mismatch) -> str: """ Get a description of a mismatch. - [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatch_description) + [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatch_description) """ raise NotImplementedError @@ -1037,7 +1037,7 @@ def mismatch_ansi_description(mismatch: Mismatch) -> str: """ Get an ANSI-compatible description of a mismatch. - [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mismatch_ansi_description) + [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatch_ansi_description) """ raise NotImplementedError @@ -1047,7 +1047,7 @@ def get_error_message(length: int = 1024) -> str | None: Provide the error message from `LAST_ERROR` to the calling C code. [Rust - `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_get_error_message) + `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_get_error_message) This function should be called after any other function in the pact_matching FFI indicates a failure with its own error message, if the caller wants to @@ -1100,7 +1100,7 @@ def log_to_stdout(level_filter: LevelFilter) -> int: """ Convenience function to direct all logging to stdout. - [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_log_to_stdout) + [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_log_to_stdout) """ raise NotImplementedError @@ -1110,7 +1110,7 @@ def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: Convenience function to direct all logging to stderr. [Rust - `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_log_to_stderr) + `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_log_to_stderr) Args: level_filter: @@ -1134,7 +1134,7 @@ def log_to_file(file_name: str, level_filter: LevelFilter) -> int: Convenience function to direct all logging to a file. [Rust - `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_log_to_file) + `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_log_to_file) # Safety @@ -1148,7 +1148,7 @@ def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: """ Convenience function to direct all logging to a task local memory buffer. - [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_log_to_buffer) + [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_log_to_buffer) """ if isinstance(level_filter, str): level_filter = LevelFilter[level_filter.upper()] @@ -1162,7 +1162,7 @@ def logger_init() -> None: """ Initialize the FFI logger with no sinks. - [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_logger_init) + [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_logger_init) This initialized logger does nothing until `pactffi_logger_apply` has been called. @@ -1184,7 +1184,7 @@ def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: Attach an additional sink to the thread-local logger. [Rust - `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_logger_attach_sink) + `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_logger_attach_sink) This logger does nothing until `pactffi_logger_apply` has been called. @@ -1231,7 +1231,7 @@ def logger_apply() -> int: Apply the previously configured sinks and levels to the program. [Rust - `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_logger_apply) + `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_logger_apply) If no sinks have been setup, will set the log level to info and the target to standard out. @@ -1247,7 +1247,7 @@ def fetch_log_buffer(log_id: str) -> str: Fetch the in-memory logger buffer contents. [Rust - `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_fetch_log_buffer) + `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_fetch_log_buffer) This will only have any contents if the `buffer` sink has been configured to log to. The contents will be allocated on the heap and will need to be freed @@ -1273,7 +1273,7 @@ def parse_pact_json(json: str) -> Pact: Parses the provided JSON into a Pact model. [Rust - `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_parse_pact_json) + `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_parse_pact_json) The returned Pact model must be freed with the `pactffi_pact_model_delete` function when no longer needed. @@ -1290,7 +1290,7 @@ def pact_model_delete(pact: Pact) -> None: """ Frees the memory used by the Pact model. - [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_model_delete) + [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_model_delete) """ raise NotImplementedError @@ -1300,7 +1300,7 @@ def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: Returns an iterator over all the interactions in the Pact. [Rust - `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_model_interaction_iterator) + `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_model_interaction_iterator) The iterator will have to be deleted using the `pactffi_pact_interaction_iter_delete` function. The iterator will contain a @@ -1319,7 +1319,7 @@ def pact_spec_version(pact: Pact) -> PactSpecification: """ Returns the Pact specification enum that the Pact is for. - [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_spec_version) + [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_spec_version) """ raise NotImplementedError @@ -1328,7 +1328,7 @@ def pact_interaction_delete(interaction: PactInteraction) -> None: """ Frees the memory used by the Pact interaction model. - [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_delete) + [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_delete) """ raise NotImplementedError @@ -1337,7 +1337,7 @@ def async_message_new() -> AsynchronousMessage: """ Get a mutable pointer to a newly-created default message on the heap. - [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_new) + [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_new) # Safety @@ -1354,7 +1354,7 @@ def async_message_delete(message: AsynchronousMessage) -> None: """ Destroy the `AsynchronousMessage` being pointed to. - [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_delete) + [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_delete) """ raise NotImplementedError @@ -1364,7 +1364,7 @@ def async_message_get_contents(message: AsynchronousMessage) -> MessageContents: Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. [Rust - `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_contents) + `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_contents) # Safety @@ -1383,7 +1383,7 @@ def async_message_get_contents_str(message: AsynchronousMessage) -> str: """ Get the message contents of an `AsynchronousMessage` in string form. - [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_contents_str) + [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_contents_str) # Safety @@ -1410,7 +1410,7 @@ def async_message_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_set_contents_str) + `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_set_contents_str) - `message` - the message to set the contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -1438,7 +1438,7 @@ def async_message_get_contents_length(message: AsynchronousMessage) -> int: Get the length of the contents of a `AsynchronousMessage`. [Rust - `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_contents_length) + `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_contents_length) # Safety @@ -1457,7 +1457,7 @@ def async_message_get_contents_bin(message: AsynchronousMessage) -> str: Get the contents of an `AsynchronousMessage` as bytes. [Rust - `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_contents_bin) + `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_contents_bin) # Safety @@ -1484,7 +1484,7 @@ def async_message_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_set_contents_bin) + `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_set_contents_bin) * `message` - the message to set the contents for * `contents` - pointer to contents to copy from @@ -1511,7 +1511,7 @@ def async_message_get_description(message: AsynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_description) + `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_description) # Safety @@ -1537,7 +1537,7 @@ def async_message_set_description( """ Write the `description` field on the `AsynchronousMessage`. - [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_set_description) + [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_set_description) # Safety @@ -1562,7 +1562,7 @@ def async_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_provider_state) + `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_provider_state) # Safety @@ -1587,7 +1587,7 @@ def async_message_get_provider_state_iter( """ Get an iterator over provider states. - [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) + [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) # Safety @@ -1604,7 +1604,7 @@ def consumer_get_name(consumer: Consumer) -> str: r""" Get a copy of this consumer's name. - [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_consumer_get_name) + [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_consumer_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -1650,7 +1650,7 @@ def pact_get_consumer(pact: Pact) -> Consumer: `pactffi_pact_consumer_delete` when no longer required. [Rust - `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_get_consumer) + `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_get_consumer) # Errors @@ -1664,7 +1664,7 @@ def pact_consumer_delete(consumer: Consumer) -> None: """ Frees the memory used by the Pact consumer. - [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_consumer_delete) + [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_consumer_delete) """ raise NotImplementedError @@ -1673,7 +1673,7 @@ def message_contents_get_contents_str(contents: MessageContents) -> str: """ Get the message contents in string form. - [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_get_contents_str) + [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_get_contents_str) # Safety @@ -1700,7 +1700,7 @@ def message_contents_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_set_contents_str) + `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_set_contents_str) * `contents` - the message contents to set the contents for * `contents_str` - pointer to contents to copy from. Must be a valid @@ -1727,7 +1727,7 @@ def message_contents_get_contents_length(contents: MessageContents) -> int: """ Get the length of the message contents. - [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_get_contents_length) + [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_get_contents_length) # Safety @@ -1746,7 +1746,7 @@ def message_contents_get_contents_bin(contents: MessageContents) -> str: Get the contents of a message as a pointer to an array of bytes. [Rust - `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_get_contents_bin) + `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_get_contents_bin) # Safety @@ -1773,7 +1773,7 @@ def message_contents_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_set_contents_bin) + `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_set_contents_bin) * `message` - the message contents to set the contents for * `contents_bin` - pointer to contents to copy from @@ -1802,7 +1802,7 @@ def message_contents_get_metadata_iter( Get an iterator over the metadata of a message. [Rust - `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) + `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) The returned pointer must be deleted with `pactffi_message_metadata_iter_delete` when done with it. @@ -1833,7 +1833,7 @@ def message_contents_get_matching_rule_iter( Get an iterator over the matching rules for a category of a message. [Rust - `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) + `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -1873,7 +1873,7 @@ def request_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP request. - [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) + [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -1910,7 +1910,7 @@ def response_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP response. - [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) + [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -1948,7 +1948,7 @@ def message_contents_get_generators_iter( Get an iterator over the generators for a category of a message. [Rust - `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_contents_get_generators_iter) + `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -1973,7 +1973,7 @@ def request_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP request. [Rust - `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_request_contents_get_generators_iter) + `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_request_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -1998,7 +1998,7 @@ def response_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP response. [Rust - `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_response_contents_get_generators_iter) + `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_response_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -2023,7 +2023,7 @@ def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: any generator. [Rust - `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_parse_matcher_definition) + `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_parse_matcher_definition) The following are examples of matching rule definitions: @@ -2059,7 +2059,7 @@ def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: using the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matcher_definition_error) + `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matcher_definition_error) """ raise NotImplementedError @@ -2073,7 +2073,7 @@ def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matcher_definition_value) + `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matcher_definition_value) Note that different expressions values can have types other than a string. Use `pactffi_matcher_definition_value_type` to get the actual type of the @@ -2087,7 +2087,7 @@ def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: """ Frees the memory used by the result of parsing the matching definition expression. - [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matcher_definition_delete) + [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matcher_definition_delete) """ raise NotImplementedError @@ -2100,7 +2100,7 @@ def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Ge NULL pointer, otherwise returns the generator as a pointer. [Rust - `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matcher_definition_generator) + `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matcher_definition_generator) The generator pointer will be a valid pointer as long as `pactffi_matcher_definition_delete` has not been called on the definition. @@ -2119,7 +2119,7 @@ def matcher_definition_value_type( If there was an error parsing the expression, it will return Unknown. [Rust - `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matcher_definition_value_type) + `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matcher_definition_value_type) """ raise NotImplementedError @@ -2128,7 +2128,7 @@ def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: """ Free the iterator when you're done using it. - [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_iter_delete) + [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_iter_delete) """ raise NotImplementedError @@ -2143,7 +2143,7 @@ def matcher_definition_iter( `pactffi_matching_rule_iter_delete` function once done with it. [Rust - `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matcher_definition_iter) + `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matcher_definition_iter) If there was an error parsing the expression, this function will return a NULL pointer. @@ -2159,7 +2159,7 @@ def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: deleted but will be cleaned up when the iterator is deleted. [Rust - `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_iter_next) + `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_iter_next) Will return a NULL pointer when the iterator has advanced past the end of the list. @@ -2181,7 +2181,7 @@ def matching_rule_id(rule_result: MatchingRuleResult) -> int: Return the ID of the matching rule. [Rust - `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_id) + `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_id) The ID corresponds to the following rules: @@ -2227,7 +2227,7 @@ def matching_rule_value(rule_result: MatchingRuleResult) -> str: pointer. [Rust - `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_value) + `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_value) The associated values for the rules are: @@ -2275,7 +2275,7 @@ def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: Will return a NULL pointer if the matching rule result was a reference. [Rust - `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_pointer) + `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_pointer) # Safety @@ -2293,7 +2293,7 @@ def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: structure. I.e., [Rust - `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_reference_name) + `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_reference_name) ```json { @@ -2327,7 +2327,7 @@ def validate_datetime(value: str, format: str) -> None: `pactffi_get_error_message`. [Rust - `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_validate_datetime) + `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_validate_datetime) # Errors If the function receives a panic, it will return 2 and the message associated with the panic can be retrieved with `pactffi_get_error_message`. @@ -2355,7 +2355,7 @@ def generator_to_json(generator: Generator) -> str: Get the JSON form of the generator. [Rust - `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generator_to_json) + `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generator_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -2378,7 +2378,7 @@ def generator_generate_string(generator: Generator, context_json: str) -> str: function). [Rust - `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generator_generate_string) + `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generator_generate_string) If anything goes wrong, it will return a NULL pointer. """ @@ -2394,7 +2394,7 @@ def generator_generate_integer(generator: Generator, context_json: str) -> int: should be the values returned from the Provider State callback function). [Rust - `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generator_generate_integer) + `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generator_generate_integer) If anything goes wrong or the generator is not a type that can generate an integer value, it will return a zero value. @@ -2407,7 +2407,7 @@ def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generators_iter_delete) + `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generators_iter_delete) """ raise NotImplementedError @@ -2417,7 +2417,7 @@ def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePa Get the next path and generator out of the iterator, if possible. [Rust - `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generators_iter_next) + `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generators_iter_next) The returned pointer must be deleted with `pactffi_generator_iter_pair_delete`. @@ -2439,7 +2439,7 @@ def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: Free a pair of key and value returned from `pactffi_generators_iter_next`. [Rust - `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generators_iter_pair_delete) + `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generators_iter_pair_delete) """ raise NotImplementedError @@ -2448,7 +2448,7 @@ def sync_http_new() -> SynchronousHttp: """ Get a mutable pointer to a newly-created default interaction on the heap. - [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_new) + [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_new) # Safety @@ -2466,7 +2466,7 @@ def sync_http_delete(interaction: SynchronousHttp) -> None: Destroy the `SynchronousHttp` interaction being pointed to. [Rust - `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_delete) + `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_delete) """ raise NotImplementedError @@ -2476,7 +2476,7 @@ def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: Get the request of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_request) + `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_request) # Safety @@ -2496,7 +2496,7 @@ def sync_http_get_request_contents(interaction: SynchronousHttp) -> str: Get the request contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_request_contents) + `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_request_contents) # Safety @@ -2523,7 +2523,7 @@ def sync_http_set_request_contents( Sets the request contents of the interaction. [Rust - `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_set_request_contents) + `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_set_request_contents) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -2551,7 +2551,7 @@ def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: Get the length of the request contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) + `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) # Safety @@ -2570,7 +2570,7 @@ def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes: Get the request contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) + `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) # Safety @@ -2597,7 +2597,7 @@ def sync_http_set_request_contents_bin( Sets the request contents of the interaction as an array of bytes. [Rust - `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) + `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from @@ -2624,7 +2624,7 @@ def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: Get the response of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_response) + `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_response) # Safety @@ -2644,7 +2644,7 @@ def sync_http_get_response_contents(interaction: SynchronousHttp) -> str: Get the response contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_response_contents) + `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_response_contents) # Safety @@ -2672,7 +2672,7 @@ def sync_http_set_response_contents( Sets the response contents of the interaction. [Rust - `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_set_response_contents) + `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_set_response_contents) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -2700,7 +2700,7 @@ def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: Get the length of the response contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) + `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) # Safety @@ -2719,7 +2719,7 @@ def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes: Get the response contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) + `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) # Safety @@ -2746,7 +2746,7 @@ def sync_http_set_response_contents_bin( Sets the response contents of the `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) + `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from @@ -2773,7 +2773,7 @@ def sync_http_get_description(interaction: SynchronousHttp) -> str: Get a copy of the description. [Rust - `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_description) + `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_description) # Safety @@ -2797,7 +2797,7 @@ def sync_http_set_description(interaction: SynchronousHttp, description: str) -> Write the `description` field on the `SynchronousHttp`. [Rust - `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_set_description) + `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_set_description) # Safety @@ -2822,7 +2822,7 @@ def sync_http_get_provider_state( Get a copy of the provider state at the given index from this interaction. [Rust - `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_provider_state) + `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_provider_state) # Safety @@ -2848,7 +2848,7 @@ def sync_http_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) + `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) # Safety @@ -2873,7 +2873,7 @@ def pact_interaction_as_synchronous_http( longer required. [Rust - `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) + `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) # Safety This function is safe as long as the interaction pointer is a valid pointer. @@ -2892,7 +2892,7 @@ def pact_interaction_as_message(interaction: PactInteraction) -> Message: must be freed with `pactffi_message_delete` when no longer required. [Rust - `pactffi_pact_interaction_as_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_as_message) + `pactffi_pact_interaction_as_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_as_message) Note that if the interaction is a V4 `AsynchronousMessage`, it will be converted to a V3 `Message` before being returned. @@ -2917,7 +2917,7 @@ def pact_interaction_as_asynchronous_message( no longer required. [Rust - `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) + `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) Note that if the interaction is a V3 `Message`, it will be converted to a V4 `AsynchronousMessage` before being returned. @@ -2942,7 +2942,7 @@ def pact_interaction_as_synchronous_message( no longer required. [Rust - `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) + `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) # Safety This function is safe as long as the interaction pointer is a valid pointer. @@ -2957,7 +2957,7 @@ def pact_message_iter_delete(iter: PactMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_message_iter_delete) + `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_message_iter_delete) """ lib.pactffi_pact_message_iter_delete(iter._ptr) @@ -2967,7 +2967,7 @@ def pact_message_iter_next(iter: PactMessageIterator) -> Message: Get the next message from the message pact. [Rust - `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_message_iter_next) + `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_message_iter_next) """ ptr = lib.pactffi_pact_message_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -2981,7 +2981,7 @@ def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMes Get the next synchronous request/response message from the V4 pact. [Rust - `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_sync_message_iter_next) + `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_sync_message_iter_next) """ ptr = lib.pactffi_pact_sync_message_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -2995,7 +2995,7 @@ def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) + `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) """ lib.pactffi_pact_sync_message_iter_delete(iter._ptr) @@ -3005,7 +3005,7 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: Get the next synchronous HTTP request/response interaction from the V4 pact. [Rust - `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_sync_http_iter_next) + `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_sync_http_iter_next) """ ptr = lib.pactffi_pact_sync_http_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -3019,7 +3019,7 @@ def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) + `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) """ lib.pactffi_pact_sync_http_iter_delete(iter._ptr) @@ -3029,7 +3029,7 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction Get the next interaction from the pact. [Rust - `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_iter_next) + `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_iter_next) """ ptr = lib.pactffi_pact_interaction_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -3043,7 +3043,7 @@ def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_interaction_iter_delete) + `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_iter_delete) """ lib.pactffi_pact_interaction_iter_delete(iter._ptr) @@ -3053,7 +3053,7 @@ def matching_rule_to_json(rule: MatchingRule) -> str: Get the JSON form of the matching rule. [Rust - `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rule_to_json) + `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -3070,7 +3070,7 @@ def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rules_iter_delete) + `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rules_iter_delete) """ raise NotImplementedError @@ -3082,7 +3082,7 @@ def matching_rules_iter_next( Get the next path and matching rule out of the iterator, if possible. [Rust - `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rules_iter_next) + `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rules_iter_next) The returned pointer must be deleted with `pactffi_matching_rules_iter_pair_delete`. @@ -3104,7 +3104,7 @@ def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) + `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) """ raise NotImplementedError @@ -3114,7 +3114,7 @@ def message_new() -> Message: Get a mutable pointer to a newly-created default message on the heap. [Rust - `pactffi_message_new`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_new) + `pactffi_message_new`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_new) # Safety @@ -3136,7 +3136,7 @@ def message_new_from_json( Constructs a `Message` from the JSON string. [Rust - `pactffi_message_new_from_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_new_from_json) + `pactffi_message_new_from_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_new_from_json) # Safety @@ -3154,7 +3154,7 @@ def message_new_from_body(body: str, content_type: str) -> Message: Constructs a `Message` from a body with a given content-type. [Rust - `pactffi_message_new_from_body`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_new_from_body) + `pactffi_message_new_from_body`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_new_from_body) # Safety @@ -3172,7 +3172,7 @@ def message_delete(message: Message) -> None: Destroy the `Message` being pointed to. [Rust - `pactffi_message_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_delete) + `pactffi_message_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_delete) """ raise NotImplementedError @@ -3182,7 +3182,7 @@ def message_get_contents(message: Message) -> OwnedString | None: Get the contents of a `Message` in string form. [Rust - `pactffi_message_get_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_contents) + `pactffi_message_get_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_contents) # Safety @@ -3208,7 +3208,7 @@ def message_set_contents(message: Message, contents: str, content_type: str) -> Sets the contents of the message. [Rust - `pactffi_message_set_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_set_contents) + `pactffi_message_set_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_set_contents) # Safety @@ -3230,7 +3230,7 @@ def message_get_contents_length(message: Message) -> int: Get the length of the contents of a `Message`. [Rust - `pactffi_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_contents_length) + `pactffi_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_contents_length) # Safety @@ -3249,7 +3249,7 @@ def message_get_contents_bin(message: Message) -> str: Get the contents of a `Message` as a pointer to an array of bytes. [Rust - `pactffi_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_contents_bin) + `pactffi_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_contents_bin) # Safety @@ -3276,7 +3276,7 @@ def message_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_set_contents_bin) + `pactffi_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_set_contents_bin) # Safety @@ -3297,7 +3297,7 @@ def message_get_description(message: Message) -> OwnedString: Get a copy of the description. [Rust - `pactffi_message_get_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_description) + `pactffi_message_get_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_description) # Safety @@ -3320,7 +3320,7 @@ def message_set_description(message: Message, description: str) -> int: Write the `description` field on the `Message`. [Rust - `pactffi_message_set_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_set_description) + `pactffi_message_set_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_set_description) # Safety @@ -3342,7 +3342,7 @@ def message_get_provider_state(message: Message, index: int) -> ProviderState: Get a copy of the provider state at the given index from this message. [Rust - `pactffi_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_provider_state) + `pactffi_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_provider_state) # Safety @@ -3365,7 +3365,7 @@ def message_get_provider_state_iter(message: Message) -> ProviderStateIterator: Get an iterator over provider states. [Rust - `pactffi_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_provider_state_iter) + `pactffi_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_provider_state_iter) # Safety @@ -3383,7 +3383,7 @@ def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: Get the next value from the iterator. [Rust - `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_iter_next) + `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_iter_next) # Safety @@ -3404,7 +3404,7 @@ def provider_state_iter_delete(iter: ProviderStateIterator) -> None: Delete the iterator. [Rust - `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_iter_delete) + `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_iter_delete) """ raise NotImplementedError @@ -3414,7 +3414,7 @@ def message_find_metadata(message: Message, key: str) -> str: Get a copy of the metadata value indexed by `key`. [Rust - `pactffi_message_find_metadata`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_find_metadata) + `pactffi_message_find_metadata`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_find_metadata) # Safety @@ -3440,7 +3440,7 @@ def message_insert_metadata(message: Message, key: str, value: str) -> int: Insert the (`key`, `value`) pair into this Message's `metadata` HashMap. [Rust - `pactffi_message_insert_metadata`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_insert_metadata) + `pactffi_message_insert_metadata`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_insert_metadata) # Safety @@ -3460,7 +3460,7 @@ def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadata Get the next key and value out of the iterator, if possible. [Rust - `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_metadata_iter_next) + `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_metadata_iter_next) The returned pointer must be deleted with `pactffi_message_metadata_pair_delete`. @@ -3483,7 +3483,7 @@ def message_get_metadata_iter(message: Message) -> MessageMetadataIterator: Get an iterator over the metadata of a message. [Rust - `pactffi_message_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_get_metadata_iter) + `pactffi_message_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_metadata_iter) # Safety @@ -3508,7 +3508,7 @@ def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: Free the metadata iterator when you're done using it. [Rust - `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_metadata_iter_delete) + `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_metadata_iter_delete) """ raise NotImplementedError @@ -3518,7 +3518,7 @@ def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_metadata_pair_delete) + `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_metadata_pair_delete) """ raise NotImplementedError @@ -3530,7 +3530,7 @@ def message_pact_new_from_json(file_name: str, json_str: str) -> MessagePact: The provided file name is used when generating error messages. [Rust - `pactffi_message_pact_new_from_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_new_from_json) + `pactffi_message_pact_new_from_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_new_from_json) # Safety @@ -3549,7 +3549,7 @@ def message_pact_delete(message_pact: MessagePact) -> None: Delete the `MessagePact` being pointed to. [Rust - `pactffi_message_pact_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_delete) + `pactffi_message_pact_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_delete) """ raise NotImplementedError @@ -3562,7 +3562,7 @@ def message_pact_get_consumer(message_pact: MessagePact) -> Consumer: pointer. [Rust - `pactffi_message_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_get_consumer) + `pactffi_message_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_get_consumer) # Safety @@ -3584,7 +3584,7 @@ def message_pact_get_provider(message_pact: MessagePact) -> Provider: pointer. [Rust - `pactffi_message_pact_get_provider`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_get_provider) + `pactffi_message_pact_get_provider`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_get_provider) # Safety @@ -3605,7 +3605,7 @@ def message_pact_get_message_iter( Get an iterator over the messages of a message pact. [Rust - `pactffi_message_pact_get_message_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_get_message_iter) + `pactffi_message_pact_get_message_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_get_message_iter) # Safety @@ -3630,7 +3630,7 @@ def message_pact_message_iter_next(iter: MessagePactMessageIterator) -> Message: Get the next message from the message pact. [Rust - `pactffi_message_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_message_iter_next) + `pactffi_message_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_message_iter_next) # Safety @@ -3649,7 +3649,7 @@ def message_pact_message_iter_delete(iter: MessagePactMessageIterator) -> None: Delete the iterator. [Rust - `pactffi_message_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_message_iter_delete) + `pactffi_message_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_message_iter_delete) """ raise NotImplementedError @@ -3659,7 +3659,7 @@ def message_pact_find_metadata(message_pact: MessagePact, key1: str, key2: str) Get a copy of the metadata value indexed by `key1` and `key2`. [Rust - `pactffi_message_pact_find_metadata`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_find_metadata) + `pactffi_message_pact_find_metadata`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_find_metadata) # Safety @@ -3687,7 +3687,7 @@ def message_pact_get_metadata_iter( Get an iterator over the metadata of a message pact. [Rust - `pactffi_message_pact_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_get_metadata_iter) + `pactffi_message_pact_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_get_metadata_iter) # Safety @@ -3714,7 +3714,7 @@ def message_pact_metadata_iter_next( Get the next triple out of the iterator, if possible. [Rust - `pactffi_message_pact_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_metadata_iter_next) + `pactffi_message_pact_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_metadata_iter_next) # Safety @@ -3732,7 +3732,7 @@ def message_pact_metadata_iter_delete(iter: MessagePactMetadataIterator) -> None """ Free the metadata iterator when you're done using it. - [Rust `pactffi_message_pact_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_metadata_iter_delete) + [Rust `pactffi_message_pact_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_metadata_iter_delete) """ raise NotImplementedError @@ -3741,7 +3741,7 @@ def message_pact_metadata_triple_delete(triple: MessagePactMetadataTriple) -> No """ Free a triple returned from `pactffi_message_pact_metadata_iter_next`. - [Rust `pactffi_message_pact_metadata_triple_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_pact_metadata_triple_delete) + [Rust `pactffi_message_pact_metadata_triple_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_metadata_triple_delete) """ raise NotImplementedError @@ -3751,7 +3751,7 @@ def provider_get_name(provider: Provider) -> str: Get a copy of this provider's name. [Rust - `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_get_name) + `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -3797,7 +3797,7 @@ def pact_get_provider(pact: Pact) -> Provider: `pactffi_pact_provider_delete` when no longer required. [Rust - `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_get_provider) + `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_get_provider) # Errors @@ -3812,7 +3812,7 @@ def pact_provider_delete(provider: Provider) -> None: Frees the memory used by the Pact provider. [Rust - `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_provider_delete) + `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_provider_delete) """ raise NotImplementedError @@ -3824,7 +3824,7 @@ def provider_state_get_name(provider_state: ProviderState) -> str: This needs to be deleted with `pactffi_string_delete`. [Rust - `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_get_name) + `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_get_name) # Safety @@ -3844,7 +3844,7 @@ def provider_state_get_param_iter( Get an iterator over the params of a provider state. [Rust - `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_get_param_iter) + `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_get_param_iter) # Safety @@ -3871,7 +3871,7 @@ def provider_state_param_iter_next( Get the next key and value out of the iterator, if possible. [Rust - `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_param_iter_next) + `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_param_iter_next) Returns a pointer to a heap allocated array of 2 elements, the pointer to the key string on the heap, and the pointer to the value string on the heap. @@ -3894,7 +3894,7 @@ def provider_state_delete(provider_state: ProviderState) -> None: Free the provider state when you're done using it. [Rust - `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_delete) + `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_delete) """ raise NotImplementedError @@ -3904,7 +3904,7 @@ def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: Free the provider state param iterator when you're done using it. [Rust - `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_param_iter_delete) + `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_param_iter_delete) """ raise NotImplementedError @@ -3914,7 +3914,7 @@ def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: Free a pair of key and value. [Rust - `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_provider_state_param_pair_delete) + `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_param_pair_delete) """ raise NotImplementedError @@ -3924,7 +3924,7 @@ def sync_message_new() -> SynchronousMessage: Get a mutable pointer to a newly-created default message on the heap. [Rust - `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_new) + `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_new) # Safety @@ -3942,7 +3942,7 @@ def sync_message_delete(message: SynchronousMessage) -> None: Destroy the `Message` being pointed to. [Rust - `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_delete) + `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_delete) """ raise NotImplementedError @@ -3952,7 +3952,7 @@ def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: Get the request contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) + `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) # Safety @@ -3979,7 +3979,7 @@ def sync_message_set_request_contents_str( Sets the request contents of the message. [Rust - `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) + `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) - `message` - the message to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -4007,7 +4007,7 @@ def sync_message_get_request_contents_length(message: SynchronousMessage) -> int Get the length of the request contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) + `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) # Safety @@ -4026,7 +4026,7 @@ def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes: Get the request contents of a `SynchronousMessage` as a bytes. [Rust - `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) + `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) # Safety @@ -4053,7 +4053,7 @@ def sync_message_set_request_contents_bin( Sets the request contents of the message as an array of bytes. [Rust - `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) + `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) * `message` - the message to set the request contents for * `contents` - pointer to contents to copy from @@ -4080,7 +4080,7 @@ def sync_message_get_request_contents(message: SynchronousMessage) -> MessageCon Get the request contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_request_contents) + `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_request_contents) # Safety @@ -4100,7 +4100,7 @@ def sync_message_get_number_responses(message: SynchronousMessage) -> int: Get the number of response messages in the `SynchronousMessage`. [Rust - `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_number_responses) + `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_number_responses) # Safety @@ -4121,7 +4121,7 @@ def sync_message_get_response_contents_str( Get the response contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) + `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) # Safety @@ -4154,7 +4154,7 @@ def sync_message_set_response_contents_str( with default values. [Rust - `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) + `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response. @@ -4186,7 +4186,7 @@ def sync_message_get_response_contents_length( Get the length of the response contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) + `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) # Safety @@ -4208,7 +4208,7 @@ def sync_message_get_response_contents_bin( Get the response contents of a `SynchronousMessage` as bytes. [Rust - `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) + `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) # Safety @@ -4239,7 +4239,7 @@ def sync_message_set_response_contents_bin( responses will be padded with default values. [Rust - `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) + `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response @@ -4270,7 +4270,7 @@ def sync_message_get_response_contents( Get the response contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_response_contents) + `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_response_contents) # Safety @@ -4290,7 +4290,7 @@ def sync_message_get_description(message: SynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_description) + `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_description) # Safety @@ -4314,7 +4314,7 @@ def sync_message_set_description(message: SynchronousMessage, description: str) Write the `description` field on the `SynchronousMessage`. [Rust - `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_set_description) + `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_set_description) # Safety @@ -4339,7 +4339,7 @@ def sync_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_provider_state) + `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_provider_state) # Safety @@ -4365,7 +4365,7 @@ def sync_message_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) + `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) # Safety @@ -4383,7 +4383,7 @@ def string_delete(string: OwnedString) -> None: Delete a string previously returned by this FFI. [Rust - `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_string_delete) + `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_string_delete) """ lib.pactffi_string_delete(string._ptr) @@ -4398,7 +4398,7 @@ def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: the mock server is returned. [Rust - `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_create_mock_server) + `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_create_mock_server) * `pact_str` - Pact JSON * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:80) @@ -4434,7 +4434,7 @@ def get_tls_ca_certificate() -> OwnedString: Fetch the CA Certificate used to generate the self-signed certificate. [Rust - `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_get_tls_ca_certificate) + `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_get_tls_ca_certificate) **NOTE:** The string for the result is allocated on the heap, and will have to be freed by the caller using pactffi_string_delete. @@ -4455,7 +4455,7 @@ def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) - operating system. The port of the mock server is returned. [Rust - `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_create_mock_server_for_pact) + `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_create_mock_server_for_pact) * `pact` - Handle to a Pact model created with created with `pactffi_new_pact`. @@ -4498,7 +4498,7 @@ def create_mock_server_for_transport( Create a mock server for the provided Pact handle and transport. [Rust - `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_create_mock_server_for_transport) + `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_create_mock_server_for_transport) Args: pact: @@ -4559,7 +4559,7 @@ def mock_server_matched(mock_server_handle: PactServerHandle) -> bool: if any request has not been successfully matched, or the method panics. [Rust - `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mock_server_matched) + `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mock_server_matched) """ return lib.pactffi_mock_server_matched(mock_server_handle._ref) @@ -4571,7 +4571,7 @@ def mock_server_mismatches( External interface to get all the mismatches from a mock server. [Rust - `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mock_server_mismatches) + `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mock_server_mismatches) # Errors @@ -4597,7 +4597,7 @@ def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: and cleanup any memory allocated for it. [Rust - `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_cleanup_mock_server) + `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_cleanup_mock_server) Args: mock_server_handle: @@ -4625,7 +4625,7 @@ def write_pact_file( directory to write the file to is passed as the second parameter. [Rust - `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_write_pact_file) + `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_write_pact_file) Args: mock_server_handle: @@ -4676,7 +4676,7 @@ def mock_server_logs(mock_server_handle: PactServerHandle) -> str: started. [Rust - `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_mock_server_logs) + `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mock_server_logs) Raises: RuntimeError: If the logs for the mock server can not be retrieved. @@ -4699,7 +4699,7 @@ def generate_datetime_string(format: str) -> StringResult: string needs to be freed with the `pactffi_string_delete` function [Rust - `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generate_datetime_string) + `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generate_datetime_string) # Safety @@ -4716,7 +4716,7 @@ def check_regex(regex: str, example: str) -> bool: Checks that the example string matches the given regex. [Rust - `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_check_regex) + `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_check_regex) # Safety @@ -4735,7 +4735,7 @@ def generate_regex_value(regex: str) -> StringResult: `pactffi_string_delete` function. [Rust - `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_generate_regex_value) + `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generate_regex_value) # Safety @@ -4750,7 +4750,7 @@ def free_string(s: str) -> None: [DEPRECATED] Frees the memory allocated to a string by another function. [Rust - `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_free_string) + `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_free_string) This function is deprecated. Use `pactffi_string_delete` instead. @@ -4772,7 +4772,7 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle: Creates a new Pact model and returns a handle to it. [Rust - `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_new_pact) + `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_new_pact) Args: consumer_name: @@ -4809,8 +4809,11 @@ def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: """ Creates a new HTTP Interaction and returns a handle to it. + Calling this function with the same description as an existing interaction + will result in that interaction being replaced with the new one. + [Rust - `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_new_interaction) + `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_new_interaction) Args: pact: @@ -4832,18 +4835,20 @@ def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: def new_message_interaction(pact: PactHandle, description: str) -> InteractionHandle: """ - Creates a new message interaction and return a handle to it. + Creates a new message interaction and returns a handle to it. + + Calling this function with the same description as an existing interaction + will result in that interaction being replaced with the new one. [Rust - `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_new_message_interaction) + `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_new_message_interaction) Args: pact: Handle to the Pact model. description: - The interaction description. It needs to be unique for each - Pact. + The interaction description. It needs to be unique for each Pact. Returns: Handle to the new Interaction @@ -4861,10 +4866,13 @@ def new_sync_message_interaction( description: str, ) -> InteractionHandle: """ - Creates a new synchronous message interaction and return a handle to it. + Creates a new synchronous message interaction and returns a handle to it. + + Calling this function with the same description as an existing interaction + will result in that interaction being replaced with the new one. [Rust - `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_new_sync_message_interaction) + `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_new_sync_message_interaction) Args: pact: @@ -4889,7 +4897,7 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> None: Sets the description for the Interaction. [Rust - `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_upon_receiving) + `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_upon_receiving) This function @@ -4926,7 +4934,7 @@ def given(interaction: InteractionHandle, description: str) -> None: Adds a provider state to the Interaction. [Rust - `pactffi_given`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_given) + `pactffi_given`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_given) Args: interaction: @@ -4952,7 +4960,7 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> Non used with V4 interactions. [Rust - `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_interaction_test_name) + `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_interaction_test_name) Args: interaction: @@ -5012,7 +5020,7 @@ def given_with_param( be parsed as JSON. [Rust - `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_given_with_param) + `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_given_with_param) Args: interaction: @@ -5053,7 +5061,7 @@ def given_with_params( with a `value` key. [Rust - `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_given_with_params) + `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_given_with_params) Args: interaction: @@ -5103,7 +5111,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None Configures the request for the Interaction. [Rust - `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_request) + `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_request) Args: interaction: @@ -5117,7 +5125,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md) + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md) which allows regex patterns. For examples: ```json @@ -5151,7 +5159,7 @@ def with_query_parameter_v2( Configures a query parameter for the Interaction. [Rust - `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_query_parameter_v2) + `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_query_parameter_v2) To setup a query parameter with multiple values, you can either call this function multiple times with a different index value: @@ -5188,7 +5196,7 @@ def with_query_parameter_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md) If you want the matching rules to apply to all values (and not just the one with the given index), make sure to set the value to be an array. @@ -5222,7 +5230,7 @@ def with_query_parameter_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: If there was an error setting the query parameter. @@ -5243,7 +5251,7 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None: Sets the specification version for a given Pact model. [Rust - `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_specification) + `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_specification) Args: pact: @@ -5263,7 +5271,7 @@ def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: Fetches the Pact specification version for the given Pact model. [Rust - `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_handle_get_pact_spec_version) + `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_handle_get_pact_spec_version) Args: handle: @@ -5289,7 +5297,7 @@ def with_pact_metadata( mock server for it has already started) [Rust - `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_pact_metadata) + `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_pact_metadata) Args: pact: @@ -5325,7 +5333,7 @@ def with_header_v2( r""" Configures a header for the Interaction. - [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_header_v2) + [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_header_v2) To setup a header with multiple values, you can either call this function multiple times with a different index value: @@ -5363,7 +5371,7 @@ def with_header_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -5385,7 +5393,7 @@ def with_header_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: If there was an error setting the header. @@ -5416,7 +5424,7 @@ def set_header( and generators can not be configured with it. [Rust - `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_set_header) + `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_set_header) If matching rules are required to be set, use `pactffi_with_header_v2`. @@ -5453,7 +5461,7 @@ def response_status(interaction: InteractionHandle, status: int) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_response_status) + `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_response_status) Args: interaction: @@ -5476,7 +5484,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_response_status_v2) + `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_response_status_v2) To include matching rules for the status (only statusCode or integer really makes sense to use), include the matching rule JSON format with the value as @@ -5495,7 +5503,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -5506,7 +5514,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: If the response status could not be set. @@ -5529,7 +5537,7 @@ def with_body( Adds the body for the interaction. [Rust - `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_body) + `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -5550,7 +5558,7 @@ def with_body( body: The body contents. For JSON payloads, matching rules can be embedded - in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). + in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: If the body could not be specified. @@ -5576,7 +5584,7 @@ def with_binary_body( Adds the body for the interaction. [Rust - `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_binary_body) + `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_binary_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -5619,7 +5627,7 @@ def with_binary_file( already started) [Rust - `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_binary_file) + `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_binary_file) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -5669,7 +5677,7 @@ def with_matching_rules( Add matching rules to the interaction. [Rust - `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_matching_rules) + `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_matching_rules) This function can be called multiple times, in which case the matching rules will be merged. @@ -5713,7 +5721,7 @@ def with_multipart_file_v2( # noqa: PLR0913 already started) or an error occurs. [Rust - `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_multipart_file_v2) + `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_multipart_file_v2) This function can be called multiple times. In that case, each subsequent call will be appended to the existing multipart body as a new part. @@ -5767,7 +5775,7 @@ def with_multipart_file( already started) or an error occurs. [Rust - `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_multipart_file) + `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_multipart_file) * `interaction` - Interaction handle to set the body for. * `part` - Request or response part. @@ -5799,12 +5807,89 @@ def with_multipart_file( raise NotImplementedError +def set_key(interaction: InteractionHandle, key: str | None) -> None: + """ + Sets the key attribute for the interaction. + + [Rust + `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_set_key) + + Args: + interaction: + Interaction handle to modify. + + key: + Key value. This must be a valid UTF-8 null-terminated string, or + `None` to clear the key. + """ + success: bool = lib.pactffi_set_key( + interaction._ref, + key.encode("utf-8") if key else ffi.NULL, + ) + if not success: + msg = f"Failed to set key for {interaction}." + raise RuntimeError(msg) + + +def set_pending(interaction: InteractionHandle, *, pending: bool) -> None: + """ + Mark the interaction as pending. + + [Rust + `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_set_pending) + + Args: + interaction: + Interaction handle to modify. + + pending: + Boolean value to toggle the pending state of the interaction. + """ + success: bool = lib.pactffi_set_pending(interaction._ref, pending) + if not success: + msg = f"Failed to update pending status for {interaction}." + raise RuntimeError(msg) + + +def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> None: + """ + Add a comment to the interaction. + + [Rust + `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_set_comment) + + Args: + interaction: + Interaction handle to set the comments for. + + key: + Key value + + value: + Comment value. This may be any valid JSON value, or a `None` to + clear the comment. Note that a value that deserialize to a JSON null + will result in a comment being added, with the value being the JSON + null. + + Raises: + RuntimeError: If the comments could not be updated. + """ + success: bool = lib.pactffi_set_comment( + interaction._ref, + key.encode("utf-8"), + value.encode("utf-8") if value else ffi.NULL, + ) + if not success: + msg = f"Failed to set comment for {interaction}." + raise RuntimeError(msg) + + def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: r""" Get an iterator over all the messages of the Pact. [Rust - `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_handle_get_message_iter) + `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_handle_get_message_iter) # Safety @@ -5828,7 +5913,7 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -5854,7 +5939,7 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: `pactffi_pact_sync_http_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) + `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) # Safety @@ -5875,7 +5960,7 @@ def new_message_pact(consumer_name: str, provider_name: str) -> MessagePactHandl Creates a new Pact Message model and returns a handle to it. [Rust - `pactffi_new_message_pact`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_new_message_pact) + `pactffi_new_message_pact`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_new_message_pact) * `consumer_name` - The name of the consumer for the pact. * `provider_name` - The name of the provider for the pact. @@ -5891,7 +5976,7 @@ def new_message(pact: MessagePactHandle, description: str) -> MessageHandle: Creates a new Message and returns a handle to it. [Rust - `pactffi_new_message`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_new_message) + `pactffi_new_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_new_message) * `description` - The message description. It needs to be unique for each Message. @@ -5906,7 +5991,7 @@ def message_expects_to_receive(message: MessageHandle, description: str) -> None Sets the description for the Message. [Rust - `pactffi_message_expects_to_receive`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_expects_to_receive) + `pactffi_message_expects_to_receive`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_expects_to_receive) * `description` - The message description. It needs to be unique for each message. @@ -5919,7 +6004,7 @@ def message_given(message: MessageHandle, description: str) -> None: Adds a provider state to the Interaction. [Rust - `pactffi_message_given`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_given) + `pactffi_message_given`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_given) * `description` - The provider state description. It needs to be unique for each message @@ -5937,7 +6022,7 @@ def message_given_with_param( Adds a provider state to the Message with a parameter key and value. [Rust - `pactffi_message_given_with_param`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_given_with_param) + `pactffi_message_given_with_param`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_given_with_param) * `description` - The provider state description. It needs to be unique. * `name` - Parameter name. @@ -5956,7 +6041,7 @@ def message_with_contents( Adds the contents of the Message. [Rust - `pactffi_message_with_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_with_contents) + `pactffi_message_with_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_with_contents) Accepts JSON, binary and other payload types. Binary data will be base64 encoded when serialised. @@ -5983,7 +6068,7 @@ def message_with_metadata(message_handle: MessageHandle, key: str, value: str) - Adds expected metadata to the Message. [Rust - `pactffi_message_with_metadata`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_with_metadata) + `pactffi_message_with_metadata`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_with_metadata) * `key` - metadata key * `value` - metadata value. @@ -6000,7 +6085,7 @@ def message_with_metadata_v2( Adds expected metadata to the Message. [Rust - `pactffi_message_with_metadata_v2`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_with_metadata_v2) + `pactffi_message_with_metadata_v2`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_with_metadata_v2) Args: message_handle: @@ -6014,7 +6099,7 @@ def message_with_metadata_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). To include matching rules for the metadata, include the matching rule JSON format with the value as a single JSON document. I.e. @@ -6030,7 +6115,7 @@ def message_with_metadata_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). """ raise NotImplementedError @@ -6040,7 +6125,7 @@ def message_reify(message_handle: MessageHandle) -> OwnedString: Reifies the given message. [Rust - `pactffi_message_reify`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_message_reify) + `pactffi_message_reify`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_reify) Reification is the process of stripping away any matchers, and returning the original contents. @@ -6069,7 +6154,7 @@ def write_message_pact_file( pointer is passed, the current working directory is used. [Rust - `pactffi_write_message_pact_file`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_write_message_pact_file) + `pactffi_write_message_pact_file`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_write_message_pact_file) If overwrite is true, the file will be overwritten with the contents of the current pact. Otherwise, it will be merged with any existing pact file. @@ -6103,7 +6188,7 @@ def with_message_pact_metadata( version [Rust - `pactffi_with_message_pact_metadata`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_with_message_pact_metadata) + `pactffi_with_message_pact_metadata`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_message_pact_metadata) * `pact` - Handle to a Pact model * `namespace` - the top level metadat key to set any key values on @@ -6123,7 +6208,7 @@ def pact_handle_write_file( External interface to write out the pact file. [Rust - `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_pact_handle_write_file) + `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_handle_write_file) This function should be called if all the consumer tests have passed. @@ -6163,7 +6248,7 @@ def free_pact_handle(pact: PactHandle) -> None: Delete a Pact handle and free the resources used by it. [Rust - `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_free_pact_handle) + `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_free_pact_handle) Raises: RuntimeError: If the handle could not be freed. @@ -6183,7 +6268,7 @@ def free_message_pact_handle(pact: MessagePactHandle) -> int: Delete a Pact handle and free the resources used by it. [Rust - `pactffi_free_message_pact_handle`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_free_message_pact_handle) + `pactffi_free_message_pact_handle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_free_message_pact_handle) # Error Handling @@ -6200,7 +6285,7 @@ def verify(args: str) -> int: """ External interface to verifier a provider. - [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verify) + [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verify) * `args` - the same as the CLI interface, except newline delimited @@ -6230,7 +6315,7 @@ def verifier_new() -> VerifierHandle: free all allocated resources. [Rust - `pactffi_verifier_new`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_new) + `pactffi_verifier_new`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_new) Deprecated: This function is deprecated. Use `pactffi_verifier_new_for_application` which allows the calling @@ -6260,7 +6345,7 @@ def verifier_new_for_application(name: str, version: str) -> VerifierHandle: free all allocated resources [Rust - `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_new_for_application) + `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_new_for_application) # Safety @@ -6277,7 +6362,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None: """ Shutdown the verifier and release all resources. - [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_shutdown) + [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_shutdown) """ raise NotImplementedError @@ -6296,7 +6381,7 @@ def verifier_set_provider_info( # noqa: PLR0913 Passing a NULL for any field will use the default value for that field. [Rust - `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_provider_info) + `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_provider_info) # Safety @@ -6319,7 +6404,7 @@ def verifier_add_provider_transport( Passing a NULL for any field will use the default value for that field. [Rust - `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_add_provider_transport) + `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_provider_transport) For non-plugin based message interactions, set protocol to "message" and set scheme to an empty string or "https" if secure HTTP is required. @@ -6344,7 +6429,7 @@ def verifier_set_filter_info( Set the filters for the Pact verifier. [Rust - `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_filter_info) + `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_filter_info) If `filter_description` is not empty, it needs to be as a regular expression. @@ -6371,7 +6456,7 @@ def verifier_set_provider_state( Set the provider state URL for the Pact verifier. [Rust - `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_provider_state) + `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_provider_state) `teardown` is a boolean value. If teardown state change requests should be made after an interaction is validated (default is false). Set it to greater @@ -6397,7 +6482,7 @@ def verifier_set_verification_options( Set the options used by the verifier when calling the provider. [Rust - `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_verification_options) + `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_verification_options) `disable_ssl_verification` is a boolean value. Set it to greater than zero to turn the option on. @@ -6418,7 +6503,7 @@ def verifier_set_coloured_output(handle: VerifierHandle, coloured_output: int) - By default, coloured output is enabled. [Rust - `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_coloured_output) + `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_coloured_output) `coloured_output` is a boolean value. Set it to greater than zero to turn the option on. @@ -6437,7 +6522,7 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, is_error: int) -> int Enables or disables if no pacts are found to verify results in an error. [Rust - `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) + `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) `is_error` is a boolean value. Set it to greater than zero to enable an error when no pacts are found to verify, and set it to zero to disable this. @@ -6463,7 +6548,7 @@ def verifier_set_publish_options( # noqa: PLR0913 Set the options used when publishing verification results to the Broker. [Rust - `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_publish_options) + `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_publish_options) # Args @@ -6492,7 +6577,7 @@ def verifier_set_consumer_filters( Set the consumer filters for the Pact verifier. [Rust - `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_set_consumer_filters) + `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_consumer_filters) # Safety @@ -6512,7 +6597,7 @@ def verifier_add_custom_header( Adds a custom header to be added to the requests made to the provider. [Rust - `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_add_custom_header) + `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_custom_header) # Safety @@ -6527,7 +6612,7 @@ def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: Adds a Pact file as a source to verify. [Rust - `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_add_file_source) + `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_file_source) # Safety @@ -6545,7 +6630,7 @@ def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> Non All pacts from the directory that match the provider name will be verified. [Rust - `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_add_directory_source) + `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_directory_source) # Safety @@ -6569,7 +6654,7 @@ def verifier_url_source( The Pact file will be fetched from the URL. [Rust - `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_url_source) + `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_url_source) If a username and password is given, then basic authentication will be used when fetching the pact file. If a token is provided, then bearer token @@ -6598,7 +6683,7 @@ def verifier_broker_source( name. [Rust - `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_broker_source) + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_broker_source) If a username and password is given, then basic authentication will be used when fetching the pact file. If a token is provided, then bearer token @@ -6637,7 +6722,7 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 `https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/`). [Rust - `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) + `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) The consumer version selectors must be passed in in JSON format. @@ -6664,7 +6749,7 @@ def verifier_execute(handle: VerifierHandle) -> int: """ Runs the verification. - [Rust `pactffi_verifier_execute`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_execute) + [Rust `pactffi_verifier_execute`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_execute) # Error Handling @@ -6681,7 +6766,7 @@ def verifier_cli_args() -> str: string. [Rust - `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_cli_args) + `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_cli_args) The purpose is to then be able to use in other languages which wrap the FFI library, to implement the same CLI functionality automatically without @@ -6741,7 +6826,7 @@ def verifier_logs(handle: VerifierHandle) -> OwnedString: function call to avoid leaking memory. [Rust - `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_logs) + `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_logs) Will return a NULL pointer if the logs for the verification can not be retrieved. @@ -6758,7 +6843,7 @@ def verifier_logs_for_provider(provider_name: str) -> OwnedString: function call to avoid leaking memory. [Rust - `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_logs_for_provider) + `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_logs_for_provider) Will return a NULL pointer if the logs for the verification can not be retrieved. @@ -6774,7 +6859,7 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: call to avoid leaking memory. [Rust - `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_output) + `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_output) * `strip_ansi` - This parameter controls ANSI escape codes. Setting it to a non-zero value @@ -6793,7 +6878,7 @@ def verifier_json(handle: VerifierHandle) -> OwnedString: call to avoid leaking memory. [Rust - `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_verifier_json) + `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_json) Will return a NULL pointer if the handle is invalid. """ @@ -6815,7 +6900,7 @@ def using_plugin( otherwise you will have plugin processes left running. [Rust - `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_using_plugin) + `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_using_plugin) Args: pact: @@ -6854,7 +6939,7 @@ def cleanup_plugins(pact: PactHandle) -> None: zero). [Rust - `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_cleanup_plugins) + `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_cleanup_plugins) """ lib.pactffi_cleanup_plugins(pact._ref) @@ -6873,7 +6958,7 @@ def interaction_contents( format of the JSON contents. [Rust - `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_interaction_contents) + `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_interaction_contents) Args: interaction: @@ -6929,7 +7014,7 @@ def matches_string_value( function once it is no longer required. [Rust - `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_string_value) + `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_string_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string @@ -6960,7 +7045,7 @@ def matches_u64_value( function once it is no longer required. [Rust - `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_u64_value) + `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_u64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -6990,7 +7075,7 @@ def matches_i64_value( function once it is no longer required. [Rust - `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_i64_value) + `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_i64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7020,7 +7105,7 @@ def matches_f64_value( function once it is no longer required. [Rust - `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_f64_value) + `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_f64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7050,7 +7135,7 @@ def matches_bool_value( function once it is no longer required. [Rust - `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_bool_value) + `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_bool_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get, 0 == false and 1 == true @@ -7082,7 +7167,7 @@ def matches_binary_value( # noqa: PLR0913 function once it is no longer required. [Rust - `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_binary_value) + `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_binary_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7117,7 +7202,7 @@ def matches_json_value( function once it is no longer required. [Rust - `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.15/pact_ffi/?search=pactffi_matches_json_value) + `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_json_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string diff --git a/pact/v3/pact.py b/pact/v3/pact.py index f8e573a0b..d509f270b 100644 --- a/pact/v3/pact.py +++ b/pact/v3/pact.py @@ -325,6 +325,49 @@ def with_multipart_file( # noqa: PLR0913 ) return self + def set_key(self, key: str | None) -> Self: + """ + Sets the key for the interaction. + + This is used by V4 interactions to set the key of the interaction, which + can subsequently used to reference the interaction. + """ + pact.v3.ffi.set_key(self._handle, key) + return self + + def set_pending(self, *, pending: bool) -> Self: + """ + Mark the interaction as pending. + + This is used by V4 interactions to mark the interaction as pending, in + which case the provider is not expected to honour the interaction. + """ + pact.v3.ffi.set_pending(self._handle, pending=pending) + return self + + def set_comment(self, key: str, value: Any | None) -> Self: # noqa: ANN401 + """ + Set a comment for the interaction. + + This is used by V4 interactions to set a comment for the interaction. A + comment consists of a key-value pair, where the key is a string and the + value is anything that can be encoded as JSON. + + Args: + key: + Key for the comment. + + value: + Value for the comment. This must be encodable using + [`json.dumps(...)`][json.dumps], or an existing JSON string. The + value of `None` will remove the comment with the given key. + """ + if isinstance(value, str) or value is None: + pact.v3.ffi.set_comment(self._handle, key, value) + else: + pact.v3.ffi.set_comment(self._handle, key, json.dumps(value)) + return self + def test_name( self, name: str, @@ -518,7 +561,7 @@ def with_header( # JSON Matching Pact's matching rules are defined in the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md) + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md) and support a wide range of matching rules. These can be specified using a JSON object as a strong using `json.dumps(...)`. For example, the above rule whereby the `X-Foo` header has multiple values can be @@ -758,7 +801,7 @@ def with_query_parameter(self, name: str, value: str) -> Self: ``` For more information on the format of the JSON object, see the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.15/rust/pact_ffi/IntegrationJson.md). + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). Args: name: From 9f737f0c6802a050616706a047ef43f553aebdfd Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 28 Feb 2024 10:22:28 +1100 Subject: [PATCH 0207/1376] chore(tests): add v4 http consumer compatibility suite Signed-off-by: JP-Ellis --- .../v3/compatiblity_suite/test_v4_consumer.py | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 tests/v3/compatiblity_suite/test_v4_consumer.py diff --git a/tests/v3/compatiblity_suite/test_v4_consumer.py b/tests/v3/compatiblity_suite/test_v4_consumer.py new file mode 100644 index 000000000..fd43866fd --- /dev/null +++ b/tests/v3/compatiblity_suite/test_v4_consumer.py @@ -0,0 +1,157 @@ +"""Basic HTTP consumer feature tests.""" + +from __future__ import annotations + +import json +import logging +from typing import Any, Generator + +from pytest_bdd import given, parsers, scenario, then + +from pact.v3.pact import HttpInteraction, Pact +from tests.v3.compatiblity_suite.util import string_to_int +from tests.v3.compatiblity_suite.util.consumer import ( + the_pact_file_for_the_test_is_generated, +) + +logger = logging.getLogger(__name__) + +################################################################################ +## Scenario +################################################################################ + + +@scenario( + "definition/features/V4/http_consumer.feature", + "Sets the type for the interaction", +) +def test_sets_the_type_for_the_interaction() -> None: + """Sets the type for the interaction.""" + + +@scenario( + "definition/features/V4/http_consumer.feature", + "Supports specifying a key for the interaction", +) +def test_supports_specifying_a_key_for_the_interaction() -> None: + """Supports specifying a key for the interaction.""" + + +@scenario( + "definition/features/V4/http_consumer.feature", + "Supports specifying the interaction is pending", +) +def test_supports_specifying_the_interaction_is_pending() -> None: + """Supports specifying the interaction is pending.""" + + +@scenario( + "definition/features/V4/http_consumer.feature", + "Supports adding comments", +) +def test_supports_adding_comments() -> None: + """Supports adding comments.""" + + +################################################################################ +## Given +################################################################################ + + +@given( + "an HTTP interaction is being defined for a consumer test", + target_fixture="pact_interaction", +) +def an_http_interaction_is_being_defined_for_a_consumer_test() -> ( + Generator[tuple[Pact, HttpInteraction], Any, None] +): + """An HTTP interaction is being defined for a consumer test.""" + pact = Pact("consumer", "provider") + pact.with_specification("V4") + yield (pact, pact.upon_receiving("a request")) + + +@given(parsers.re(r'a key of "(?P[^"]+)" is specified for the HTTP interaction')) +def a_key_is_specified_for_the_http_interaction( + pact_interaction: tuple[Pact, HttpInteraction], + key: str, +) -> None: + """A key is specified for the HTTP interaction.""" + _, interaction = pact_interaction + interaction.set_key(key) + + +@given("the HTTP interaction is marked as pending") +def the_http_interaction_is_marked_as_pending( + pact_interaction: tuple[Pact, HttpInteraction], +) -> None: + """The HTTP interaction is marked as pending.""" + _, interaction = pact_interaction + interaction.set_pending(pending=True) + + +@given(parsers.re(r'a comment "(?P[^"]+)" is added to the HTTP interaction')) +def a_comment_is_added_to_the_http_interaction( + pact_interaction: tuple[Pact, HttpInteraction], + comment: str, +) -> None: + """A comment of "" is added to the HTTP interaction.""" + _, interaction = pact_interaction + interaction.set_comment("text", [comment]) + + +################################################################################ +## When +################################################################################ + + +the_pact_file_for_the_test_is_generated() + + +################################################################################ +## Then +################################################################################ + + +@then( + parsers.re( + r"the (?P[^ ]+) interaction in the Pact file" + r' will have a type of "(?P[^"]+)"' + ), + converters={"num": string_to_int}, +) +def the_interaction_in_the_pact_file_will_container_provider_states( + pact_data: dict[str, Any], + num: int, + interaction_type: str, +) -> None: + """The interaction in the Pact file will container provider states.""" + assert "interactions" in pact_data + assert len(pact_data["interactions"]) >= num + interaction = pact_data["interactions"][num - 1] + assert interaction["type"] == interaction_type + + +@then( + parsers.re( + r"the (?P[^ ]+) interaction in the Pact file" + r" will have \"(?P[^\"]+)\" = '(?P[^']+)'" + ), + converters={"num": string_to_int}, +) +def the_interaction_in_the_pact_file_will_have_a_key_of( + pact_data: dict[str, Any], + num: int, + key: str, + value: str, +) -> None: + """The interaction in the Pact file will have a key of value.""" + assert "interactions" in pact_data + assert len(pact_data["interactions"]) >= num + interaction = pact_data["interactions"][num - 1] + assert key in interaction + value = json.loads(value) + if isinstance(value, list): + assert interaction[key] in value + else: + assert interaction[key] == value From 71b039de742e8c1e0005e821d738fb153cc3744a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 28 Feb 2024 10:57:09 +1100 Subject: [PATCH 0208/1376] chore(ci): speed up wheels building on prs Use a filter to target the latest stable version of Python when building wheels on PRs. Otherwise, the workflow takes an unnecessarily long amount of time to execute. Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ec20af63..5a0a9a2de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,10 +74,23 @@ jobs: # Fetch all tags fetch-depth: 0 + - name: Filter targets + id: cibw-filter + shell: bash + # Building all wheels on PRs is too slow, so we filter them to target + # the latest stable version of Python. + run: | + if [[ "${{ github.event_name}}" == "pull_request" ]] ; then + echo "build=cp${STABLE_PYTHON_VERSION/./}-*" >> "$GITHUB_OUTPUT" + else + echo "build=*" >> "$GITHUB_OUTPUT" + fi + - name: Create wheels uses: pypa/cibuildwheel@ce3fb7832089eb3e723a0a99cab7f3eaccf074fd # v2.16.5 env: CIBW_ARCHS: ${{ matrix.archs }} + CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} - name: Upload wheels uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4 From 5fd0fb38b76a6d0fe8766d3462d4814a58063635 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 28 Feb 2024 11:33:27 +1100 Subject: [PATCH 0209/1376] chore(ci): add caching Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 23 +++++++++++++++++++++++ .github/workflows/test.yml | 3 +++ 2 files changed, 26 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a0a9a2de..a1110b0a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,6 +36,7 @@ jobs: uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} + cache: pip - name: Install hatch run: pip install --upgrade hatch @@ -74,6 +75,16 @@ jobs: # Fetch all tags fetch-depth: 0 + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ github.workflow }}-pip-${{ runner.os }} + ${{ github.workflow }}-pip + ${{ github.workflow }} + - name: Filter targets id: cibw-filter shell: bash @@ -123,6 +134,16 @@ jobs: # Fetch all tags fetch-depth: 0 + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ github.workflow }}-pip-${{ runner.os }} + ${{ github.workflow }}-pip + ${{ github.workflow }} + - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3 @@ -153,10 +174,12 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - name: Setup Python uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} + cache: pip - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 329952d73..4472882a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,6 +46,7 @@ jobs: uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: python-version: ${{ matrix.python-version }} + cache: pip - name: Install Hatch run: pip install --upgrade hatch @@ -96,6 +97,7 @@ jobs: uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} + cache: pip - name: Install Hatch run: pip install --upgrade hatch @@ -128,6 +130,7 @@ jobs: uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} + cache: pip - name: Install Hatch run: pip install --upgrade hatch From 0d99ad5be823f620f568cbbe6a33a188595333f8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Feb 2024 00:42:46 +0000 Subject: [PATCH 0210/1376] chore(deps): pin dependencies --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a1110b0a8..773de8f95 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,7 +76,7 @@ jobs: fetch-depth: 0 - name: Cache pip packages - uses: actions/cache@v4 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4 with: path: ~/.cache/pip key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} @@ -135,7 +135,7 @@ jobs: fetch-depth: 0 - name: Cache pip packages - uses: actions/cache@v4 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4 with: path: ~/.cache/pip key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} From 3ec4cbe4814cd51ac43f8946a83c763a06e9c17a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 04:54:16 +0000 Subject: [PATCH 0211/1376] chore(deps): update pypa/gh-action-pypi-publish action to v1.8.12 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 773de8f95..03ee7c149 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -207,7 +207,7 @@ jobs: path: wheels - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf # v1.8.11 + uses: pypa/gh-action-pypi-publish@e53eb8b103ffcb59469888563dc324e3c8ba6f06 # v1.8.12 with: skip-existing: true password: ${{ secrets.PYPI_TOKEN }} From ee0928295430d1617a562f2b36f333551f4b874e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 22:56:16 +0000 Subject: [PATCH 0212/1376] chore(deps): update codecov/codecov-action digest to 54bcd87 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4472882a9..8c1f81a1f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,7 +57,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@0cfda1dd0a4ad9efc75517f399d859cd1ea4ced1 # v4 + uses: codecov/codecov-action@54bcd8715eee62d40e33596ef5e8f0f48dbbccab # v4 with: token: ${{ secrets.CODECOV_TOKEN }} From 93405ea4a1eb9de84294d9e13e4c007d8b5793c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 22:56:13 +0000 Subject: [PATCH 0213/1376] chore(deps): update actions/download-artifact digest to 87c5514 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03ee7c149..2389a7f1f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -181,7 +181,7 @@ jobs: python-version: ${{ env.STABLE_PYTHON_VERSION }} cache: pip - - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4 + - uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4 with: path: wheelhouse @@ -202,7 +202,7 @@ jobs: id-token: write steps: - - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4 + - uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4 with: path: wheels From a0793b65523ef5f0192910465cc09bb47d1609bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 09:43:03 +0000 Subject: [PATCH 0214/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.16.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2e6b7544c..d59569216 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.15.0 + rev: v3.16.0 hooks: - id: commitizen stages: [commit-msg] From ca2562d75966d20a2972567bb2f16822c85d1e8f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 25 Feb 2024 00:27:40 +0000 Subject: [PATCH 0215/1376] chore(deps): update dependency test/pytest to v8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b39ccd812..dbf3fea54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ test = [ "flask[async] ~= 3.0", "httpx ~= 0.0", "mock ~= 5.0", - "pytest ~= 7.0", + "pytest ~=8.0", "pytest-asyncio ~= 0.0", "pytest-bdd ~= 7.0", "pytest-cov ~= 4.0", From 3ce053ef301e587210301bffcc50f68f5a584b76 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 29 Feb 2024 15:44:10 +1100 Subject: [PATCH 0216/1376] chore: migrate from flat to src layout The src layout helps prevent a number of issues during development. A discussion by Python's packaging group of the pros can be found at https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/. Fundamentally, it prevents `import pact` from finding the local `pact/` directory, and ensures that the package is installed correctly (typically as an editable installation). Signed-off-by: JP-Ellis --- .gitignore | 6 +- .gitmodules | 2 +- .pre-commit-config.yaml | 6 +- hatch_build.py | 41 +++++----- pyproject.toml | 81 ++++++++++++------- {pact => src/pact}/__init__.py | 0 {pact => src/pact}/broker.py | 0 {pact => src/pact}/cli/__init__.py | 0 {pact => src/pact}/cli/verify.py | 0 {pact => src/pact}/constants.py | 0 {pact => src/pact}/consumer.py | 0 {pact => src/pact}/http_proxy.py | 0 {pact => src/pact}/matchers.py | 0 {pact => src/pact}/message_consumer.py | 0 {pact => src/pact}/message_pact.py | 0 {pact => src/pact}/message_provider.py | 0 {pact => src/pact}/pact.py | 0 {pact => src/pact}/provider.py | 0 {pact => src/pact}/v3/__init__.py | 0 {pact => src/pact}/v3/_ffi.pyi | 0 {pact => src/pact}/v3/ffi.py | 0 {pact => src/pact}/v3/pact.py | 0 {pact => src/pact}/v3/py.typed | 0 {pact => src/pact}/verifier.py | 0 {pact => src/pact}/verify_wrapper.py | 0 .../__init__.py | 0 .../conftest.py | 0 .../definition | 0 .../test_v1_consumer.py | 7 +- .../test_v2_consumer.py | 7 +- .../test_v3_consumer.py | 4 +- .../test_v4_consumer.py | 4 +- .../util/__init__.py | 0 .../util/consumer.py | 4 +- 34 files changed, 98 insertions(+), 64 deletions(-) rename {pact => src/pact}/__init__.py (100%) rename {pact => src/pact}/broker.py (100%) rename {pact => src/pact}/cli/__init__.py (100%) rename {pact => src/pact}/cli/verify.py (100%) rename {pact => src/pact}/constants.py (100%) rename {pact => src/pact}/consumer.py (100%) rename {pact => src/pact}/http_proxy.py (100%) rename {pact => src/pact}/matchers.py (100%) rename {pact => src/pact}/message_consumer.py (100%) rename {pact => src/pact}/message_pact.py (100%) rename {pact => src/pact}/message_provider.py (100%) rename {pact => src/pact}/pact.py (100%) rename {pact => src/pact}/provider.py (100%) rename {pact => src/pact}/v3/__init__.py (100%) rename {pact => src/pact}/v3/_ffi.pyi (100%) rename {pact => src/pact}/v3/ffi.py (100%) rename {pact => src/pact}/v3/pact.py (100%) rename {pact => src/pact}/v3/py.typed (100%) rename {pact => src/pact}/verifier.py (100%) rename {pact => src/pact}/verify_wrapper.py (100%) rename tests/v3/{compatiblity_suite => compatibility_suite}/__init__.py (100%) rename tests/v3/{compatiblity_suite => compatibility_suite}/conftest.py (100%) rename tests/v3/{compatiblity_suite => compatibility_suite}/definition (100%) rename tests/v3/{compatiblity_suite => compatibility_suite}/test_v1_consumer.py (98%) rename tests/v3/{compatiblity_suite => compatibility_suite}/test_v2_consumer.py (98%) rename tests/v3/{compatiblity_suite => compatibility_suite}/test_v3_consumer.py (97%) rename tests/v3/{compatiblity_suite => compatibility_suite}/test_v4_consumer.py (97%) rename tests/v3/{compatiblity_suite => compatibility_suite}/util/__init__.py (100%) rename tests/v3/{compatiblity_suite => compatibility_suite}/util/consumer.py (99%) diff --git a/.gitignore b/.gitignore index f93aa99ac..763596319 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ ################################################################################ ## Pact Python Specific ################################################################################ -pact/bin -pact/data +src/pact/bin +src/pact/data # Version is determined from the VCS -pact/__version__.py +src/pact/__version__.py ################################################################################ ## Standard Templates diff --git a/.gitmodules b/.gitmodules index fa1a87278..ebf9afa34 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "compatibility-suite"] - path = tests/v3/compatiblity_suite/definition + path = tests/v3/compatibility_suite/definition url = ../pact-compatibility-suite.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d59569216..ff7e6d229 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,8 +42,8 @@ repos: hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the - # files in pact/v3/** and tests/v3/**. - exclude: ^(pact|tests)/(?!v3/).*\.py$ + # files in src/pact/v3/** and tests/v3/**. + exclude: ^(src/pact|tests)/(?!v3/).*\.py$ args: [--fix, --exit-non-zero-on-fix] - id: ruff-format exclude: ^(pact|tests)/(?!v3/).*\.py$ @@ -63,7 +63,7 @@ repos: entry: hatch run mypy language: system types: [python] - exclude: ^(pact|tests)/(?!v3/).*\.py$ + exclude: ^(src/pact|tests)/(?!v3/).*\.py$ stages: [pre-push] - repo: https://github.com/igorshubovych/markdownlint-cli diff --git a/hatch_build.py b/hatch_build.py index 303d98611..3baec67cb 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -27,7 +27,7 @@ from hatchling.builders.hooks.plugin.interface import BuildHookInterface from packaging.tags import sys_tags -ROOT_DIR = Path(__file__).parent.resolve() +PACT_ROOT_DIR = Path(__file__).parent.resolve() / "src" / "pact" # Latest version available at: # https://github.com/pact-foundation/pact-ruby-standalone/releases @@ -73,7 +73,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 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) + shutil.rmtree(PACT_ROOT_DIR / subdir, ignore_errors=True) def initialize( self, @@ -107,7 +107,7 @@ def pact_bin_install(self, version: str) -> None: """ Install the Pact standalone binaries. - The binaries are installed in `pact/bin`, and the relevant version for + The binaries are installed in `src/pact/bin`, and the relevant version for the current operating system is determined automatically. Args: @@ -188,23 +188,28 @@ def _pact_bin_extract(self, artifact: Path) -> None: """ Extract the Pact binaries. - The upstream distributables contain a lot of files which are not needed - for this library. This function ensures that only the files in - `pact/bin` are extracted to avoid unnecessary bloat. + The binaries in the `bin` directory require the underlying Ruby runtime + to be present, which is included in the `lib` directory. Args: artifact: The path to the downloaded artifact. """ - if str(artifact).endswith(".zip"): - with zipfile.ZipFile(artifact) as f: - f.extractall(ROOT_DIR) # noqa: S202 - - if str(artifact).endswith(".tar.gz"): - with tarfile.open(artifact) as f: - f.extractall(ROOT_DIR) # noqa: S202 - - # Cleanup the extract `README.md` - (ROOT_DIR / "pact" / "README.md").unlink() + with tempfile.TemporaryDirectory() as tmpdir: + if str(artifact).endswith(".zip"): + with zipfile.ZipFile(artifact) as f: + f.extractall(tmpdir) # noqa: S202 + + if str(artifact).endswith(".tar.gz"): + with tarfile.open(artifact) as f: + f.extractall(tmpdir) # noqa: S202 + + for d in ["bin", "lib"]: + if (PACT_ROOT_DIR / d).is_dir(): + shutil.rmtree(PACT_ROOT_DIR / d) + shutil.copytree( + Path(tmpdir) / "pact" / d, + PACT_ROOT_DIR / d, + ) def pact_lib_install(self, version: str) -> None: """ @@ -415,7 +420,7 @@ def _pact_lib_cffi(self, includes: list[str]) -> None: library_dirs=[str(self.tmpdir)], ) output = Path(ffibuilder.compile(verbose=True, tmpdir=str(self.tmpdir))) - shutil.copy(output, ROOT_DIR / "pact" / "v3") + shutil.copy(output, PACT_ROOT_DIR / "v3") def _download(self, url: str) -> Path: """ @@ -431,7 +436,7 @@ def _download(self, url: str) -> Path: The path to the downloaded artifact. """ filename = url.split("/")[-1] - artifact = ROOT_DIR / "pact" / "data" / filename + artifact = PACT_ROOT_DIR / "data" / filename artifact.parent.mkdir(parents=True, exist_ok=True) if not artifact.exists(): diff --git a/pyproject.toml b/pyproject.toml index dbf3fea54..ac01fd8cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,15 +100,33 @@ build-backend = "hatchling.build" source = "vcs" [tool.hatch.build.hooks.vcs] -version-file = "pact/__version__.py" - -[tool.hatch.build] -include = ["**/py.typed", "**/*.md", "LICENSE", "pact/**/*.py", "pact/**/*.pyi"] +version-file = "src/pact/__version__.py" + +[tool.hatch.build.targets.sdist] +include = [ + # Source + "/src/pact/**/*.py", + "/src/pact/**/*.pyi", + "/src/pact/**/py.typed", + + # Metadata + "*.md", + "LICENSE", +] [tool.hatch.build.targets.wheel] -# Ignore the data files in the wheel as their contents are already included -# in the package. -artifacts = ["pact/bin/*", "pact/lib/*", "pact/v3/_ffi.*"] +packages = ["/src/pact"] +include = [ + # Source + "/src/pact/**/*.py", + "/src/pact/**/*.pyi", + "/src/pact/**/py.typed", +] +artifacts = [ + "/src/pact/bin/*", # Ruby executables + "/src/pact/lib/*", # Ruby library + "/src/pact/v3/_ffi.*", # Rust library +] [tool.hatch.build.targets.wheel.hooks.custom] @@ -135,8 +153,8 @@ format = "ruff format {args}" test = "pytest tests/ {args}" example = "pytest examples/ {args}" all = ["format", "lint", "typecheck", "test", "example"] -docs = ["mkdocs serve {args}"] -docs-build = ["mkdocs build {args}"] +docs = "mkdocs serve {args}" +docs-build = "mkdocs build {args}" # Test environment for running unit tests. This automatically tests against all # supported Python versions. @@ -151,6 +169,7 @@ python = ["3.8", "3.9", "3.10", "3.11", "3.12"] ################################################################################ [tool.pytest.ini_options] +pythonpath = "." addopts = [ "--import-mode=importlib", "--cov-config=pyproject.toml", @@ -173,6 +192,10 @@ markers = [ ## Coverage ################################################################################ +[tool.coverage.paths] +pact = ["/src/pact"] +tests = ["/examples", "/tests"] + [tool.coverage.report] exclude_lines = [ "if __name__ == .__main__.:", # Ignore non-runnable code @@ -191,25 +214,25 @@ target-version = "py38" # TODO: Remove the explicity extend-exclude once astral-sh/ruff#6262 is fixed. # https://github.com/pact-foundation/pact-python/issues/458 extend-exclude = [ - # "pact/*.py", - # "pact/cli/*.py", - # "tests/*.py", - # "tests/cli/*.py", - "pact/__init__.py", - "pact/__version__.py", - "pact/broker.py", - "pact/cli/*.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", + # "src/pact/*.py", + # "src/pact/cli/*.py", + # "src/tests/*.py", + # "src/tests/cli/*.py", + "src/pact/__init__.py", + "src/pact/__version__.py", + "src/pact/broker.py", + "src/pact/cli/*.py", + "src/pact/constants.py", + "src/pact/consumer.py", + "src/pact/http_proxy.py", + "src/pact/matchers.py", + "src/pact/message_consumer.py", + "src/pact/message_pact.py", + "src/pact/message_provider.py", + "src/pact/pact.py", + "src/pact/provider.py", + "src/pact/verifier.py", + "src/pact/verify_wrapper.py", "tests/__init__.py", "tests/cli/*.py", "tests/conftest.py", @@ -264,7 +287,7 @@ docstring-code-format = true ################################################################################ [tool.mypy] -exclude = '^(pact|tests)/(?!v3).+\.py$' +exclude = '^(src/pact|tests)/(?!v3).+\.py$' ################################################################################ ## CI Build Wheel diff --git a/pact/__init__.py b/src/pact/__init__.py similarity index 100% rename from pact/__init__.py rename to src/pact/__init__.py diff --git a/pact/broker.py b/src/pact/broker.py similarity index 100% rename from pact/broker.py rename to src/pact/broker.py diff --git a/pact/cli/__init__.py b/src/pact/cli/__init__.py similarity index 100% rename from pact/cli/__init__.py rename to src/pact/cli/__init__.py diff --git a/pact/cli/verify.py b/src/pact/cli/verify.py similarity index 100% rename from pact/cli/verify.py rename to src/pact/cli/verify.py diff --git a/pact/constants.py b/src/pact/constants.py similarity index 100% rename from pact/constants.py rename to src/pact/constants.py diff --git a/pact/consumer.py b/src/pact/consumer.py similarity index 100% rename from pact/consumer.py rename to src/pact/consumer.py diff --git a/pact/http_proxy.py b/src/pact/http_proxy.py similarity index 100% rename from pact/http_proxy.py rename to src/pact/http_proxy.py diff --git a/pact/matchers.py b/src/pact/matchers.py similarity index 100% rename from pact/matchers.py rename to src/pact/matchers.py diff --git a/pact/message_consumer.py b/src/pact/message_consumer.py similarity index 100% rename from pact/message_consumer.py rename to src/pact/message_consumer.py diff --git a/pact/message_pact.py b/src/pact/message_pact.py similarity index 100% rename from pact/message_pact.py rename to src/pact/message_pact.py diff --git a/pact/message_provider.py b/src/pact/message_provider.py similarity index 100% rename from pact/message_provider.py rename to src/pact/message_provider.py diff --git a/pact/pact.py b/src/pact/pact.py similarity index 100% rename from pact/pact.py rename to src/pact/pact.py diff --git a/pact/provider.py b/src/pact/provider.py similarity index 100% rename from pact/provider.py rename to src/pact/provider.py diff --git a/pact/v3/__init__.py b/src/pact/v3/__init__.py similarity index 100% rename from pact/v3/__init__.py rename to src/pact/v3/__init__.py diff --git a/pact/v3/_ffi.pyi b/src/pact/v3/_ffi.pyi similarity index 100% rename from pact/v3/_ffi.pyi rename to src/pact/v3/_ffi.pyi diff --git a/pact/v3/ffi.py b/src/pact/v3/ffi.py similarity index 100% rename from pact/v3/ffi.py rename to src/pact/v3/ffi.py diff --git a/pact/v3/pact.py b/src/pact/v3/pact.py similarity index 100% rename from pact/v3/pact.py rename to src/pact/v3/pact.py diff --git a/pact/v3/py.typed b/src/pact/v3/py.typed similarity index 100% rename from pact/v3/py.typed rename to src/pact/v3/py.typed diff --git a/pact/verifier.py b/src/pact/verifier.py similarity index 100% rename from pact/verifier.py rename to src/pact/verifier.py diff --git a/pact/verify_wrapper.py b/src/pact/verify_wrapper.py similarity index 100% rename from pact/verify_wrapper.py rename to src/pact/verify_wrapper.py diff --git a/tests/v3/compatiblity_suite/__init__.py b/tests/v3/compatibility_suite/__init__.py similarity index 100% rename from tests/v3/compatiblity_suite/__init__.py rename to tests/v3/compatibility_suite/__init__.py diff --git a/tests/v3/compatiblity_suite/conftest.py b/tests/v3/compatibility_suite/conftest.py similarity index 100% rename from tests/v3/compatiblity_suite/conftest.py rename to tests/v3/compatibility_suite/conftest.py diff --git a/tests/v3/compatiblity_suite/definition b/tests/v3/compatibility_suite/definition similarity index 100% rename from tests/v3/compatiblity_suite/definition rename to tests/v3/compatibility_suite/definition diff --git a/tests/v3/compatiblity_suite/test_v1_consumer.py b/tests/v3/compatibility_suite/test_v1_consumer.py similarity index 98% rename from tests/v3/compatiblity_suite/test_v1_consumer.py rename to tests/v3/compatibility_suite/test_v1_consumer.py index 3710db730..084429630 100644 --- a/tests/v3/compatiblity_suite/test_v1_consumer.py +++ b/tests/v3/compatibility_suite/test_v1_consumer.py @@ -7,8 +7,11 @@ import pytest from pytest_bdd import given, parsers, scenario -from tests.v3.compatiblity_suite.util import InteractionDefinition, parse_markdown_table -from tests.v3.compatiblity_suite.util.consumer import ( +from tests.v3.compatibility_suite.util import ( + InteractionDefinition, + parse_markdown_table, +) +from tests.v3.compatibility_suite.util.consumer import ( a_response_is_returned, request_n_is_made_to_the_mock_server, request_n_is_made_to_the_mock_server_with_the_following_changes, diff --git a/tests/v3/compatiblity_suite/test_v2_consumer.py b/tests/v3/compatibility_suite/test_v2_consumer.py similarity index 98% rename from tests/v3/compatiblity_suite/test_v2_consumer.py rename to tests/v3/compatibility_suite/test_v2_consumer.py index 61f0d02c9..cd1447cfe 100644 --- a/tests/v3/compatiblity_suite/test_v2_consumer.py +++ b/tests/v3/compatibility_suite/test_v2_consumer.py @@ -6,8 +6,11 @@ from pytest_bdd import given, parsers, scenario -from tests.v3.compatiblity_suite.util import InteractionDefinition, parse_markdown_table -from tests.v3.compatiblity_suite.util.consumer import ( +from tests.v3.compatibility_suite.util import ( + InteractionDefinition, + parse_markdown_table, +) +from tests.v3.compatibility_suite.util.consumer import ( a_response_is_returned, request_n_is_made_to_the_mock_server, request_n_is_made_to_the_mock_server_with_the_following_changes, diff --git a/tests/v3/compatiblity_suite/test_v3_consumer.py b/tests/v3/compatibility_suite/test_v3_consumer.py similarity index 97% rename from tests/v3/compatiblity_suite/test_v3_consumer.py rename to tests/v3/compatibility_suite/test_v3_consumer.py index bffb78706..b7014d6f7 100644 --- a/tests/v3/compatiblity_suite/test_v3_consumer.py +++ b/tests/v3/compatibility_suite/test_v3_consumer.py @@ -10,8 +10,8 @@ from pytest_bdd import given, parsers, scenario, then from pact.v3.pact import HttpInteraction, Pact -from tests.v3.compatiblity_suite.util import parse_markdown_table -from tests.v3.compatiblity_suite.util.consumer import ( +from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util.consumer import ( the_pact_file_for_the_test_is_generated, ) diff --git a/tests/v3/compatiblity_suite/test_v4_consumer.py b/tests/v3/compatibility_suite/test_v4_consumer.py similarity index 97% rename from tests/v3/compatiblity_suite/test_v4_consumer.py rename to tests/v3/compatibility_suite/test_v4_consumer.py index fd43866fd..7b7ab019d 100644 --- a/tests/v3/compatiblity_suite/test_v4_consumer.py +++ b/tests/v3/compatibility_suite/test_v4_consumer.py @@ -9,8 +9,8 @@ from pytest_bdd import given, parsers, scenario, then from pact.v3.pact import HttpInteraction, Pact -from tests.v3.compatiblity_suite.util import string_to_int -from tests.v3.compatiblity_suite.util.consumer import ( +from tests.v3.compatibility_suite.util import string_to_int +from tests.v3.compatibility_suite.util.consumer import ( the_pact_file_for_the_test_is_generated, ) diff --git a/tests/v3/compatiblity_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py similarity index 100% rename from tests/v3/compatiblity_suite/util/__init__.py rename to tests/v3/compatibility_suite/util/__init__.py diff --git a/tests/v3/compatiblity_suite/util/consumer.py b/tests/v3/compatibility_suite/util/consumer.py similarity index 99% rename from tests/v3/compatiblity_suite/util/consumer.py rename to tests/v3/compatibility_suite/util/consumer.py index 2488b0cd9..f9834187d 100644 --- a/tests/v3/compatiblity_suite/util/consumer.py +++ b/tests/v3/compatibility_suite/util/consumer.py @@ -15,7 +15,7 @@ from yarl import URL from pact.v3 import Pact -from tests.v3.compatiblity_suite.util import ( +from tests.v3.compatibility_suite.util import ( FIXTURES_ROOT, parse_markdown_table, string_to_int, @@ -27,7 +27,7 @@ from pathlib import Path from pact.v3.pact import HttpInteraction, PactServer - from tests.v3.compatiblity_suite.util import InteractionDefinition + from tests.v3.compatibility_suite.util import InteractionDefinition logger = logging.getLogger(__name__) From 8eeb25fa4949ca89ec38d88e3d267801e24cf88e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 20:20:27 +0000 Subject: [PATCH 0217/1376] chore(deps): update actions/cache digest to ab5e6d0 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2389a7f1f..9142d3949 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,7 +76,7 @@ jobs: fetch-depth: 0 - name: Cache pip packages - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4 with: path: ~/.cache/pip key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} @@ -135,7 +135,7 @@ jobs: fetch-depth: 0 - name: Cache pip packages - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4 with: path: ~/.cache/pip key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} From f080ca9c6c4bd0f40771620a08c8d7465e6eba85 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:47:52 +0000 Subject: [PATCH 0218/1376] chore(deps): update dependency dev/ruff to v0.3.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ac01fd8cc..1333cd9e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ test = [ "pytest-cov ~= 4.0", "testcontainers ~= 3.0", ] -dev = ["pact-python[types]", "pact-python[test]", "ruff==0.2.2"] +dev = ["pact-python[types]", "pact-python[test]", "ruff==0.3.0"] ################################################################################ ## Hatch Build Configuration From de7c9553159bc341b6d8a3fd3e4827b74d8e2d77 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:47:56 +0000 Subject: [PATCH 0219/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.3.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff7e6d229..ccffae2ba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.2 + rev: v0.3.0 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 94e2db7830b4a4fd4a051f8090d65ab7ae8d386d Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 1 Mar 2024 09:58:33 +1100 Subject: [PATCH 0220/1376] refactor(v3): split interactions into modules The `pact` module was getting a little crowded. As a result, the following classes have been split into their own modules: - `Interaction` -> `interaction/__init__.py` - `HttpInteraction` -> `interaction/http_interaction.py` - `SyncMessageInteraction` -> `interaction/sync_message_interaction.py` - `AsyncMessageInteraction` -> `interaction/async_message_interaction.py` I am also removing the exporting of `Interaction` within the root `pact.v3` module, as end-users should never be instantiating interactions directly. Signed-off-by: JP-Ellis --- src/pact/v3/__init__.py | 3 +- src/pact/v3/interaction/__init__.py | 460 +++++++++ .../interaction/async_message_interaction.py | 60 ++ src/pact/v3/interaction/http_interaction.py | 426 ++++++++ .../interaction/sync_message_interaction.py | 685 +++++++++++++ src/pact/v3/pact.py | 935 +----------------- 6 files changed, 1638 insertions(+), 931 deletions(-) create mode 100644 src/pact/v3/interaction/__init__.py create mode 100644 src/pact/v3/interaction/async_message_interaction.py create mode 100644 src/pact/v3/interaction/http_interaction.py create mode 100644 src/pact/v3/interaction/sync_message_interaction.py diff --git a/src/pact/v3/__init__.py b/src/pact/v3/__init__.py index 6edcda7b6..137cba447 100644 --- a/src/pact/v3/__init__.py +++ b/src/pact/v3/__init__.py @@ -20,9 +20,8 @@ considered deprecated, and will be removed in a future release. """ -from pact.v3.pact import Interaction, Pact +from pact.v3.pact import Pact __all__ = [ "Pact", - "Interaction", ] diff --git a/src/pact/v3/interaction/__init__.py b/src/pact/v3/interaction/__init__.py new file mode 100644 index 000000000..8b50b3aa5 --- /dev/null +++ b/src/pact/v3/interaction/__init__.py @@ -0,0 +1,460 @@ +""" +Pact between a consumer and a provider. + +This module defines the classes that are used to define a Pact between a +consumer and a provider. It defines the interactions between the two parties, +and provides the functionality to verify that the interactions are satisfied. + +For the roles of consumer and provider, see the documentation for the +`pact.v3.service` module. +""" + +from __future__ import annotations + +import abc +import json +from typing import TYPE_CHECKING, Any, Literal, overload + +import pact.v3.ffi + +if TYPE_CHECKING: + from pathlib import Path + + try: + from typing import Self + except ImportError: + from typing_extensions import Self + + +__all__ = [ + "Interaction", +] + + +class Interaction(abc.ABC): + """ + Interaction between a consumer and a provider. + + This abstract class defines an interaction between a consumer and a + provider. The concrete subclasses define the type of interaction, and include: + + - [`HttpInteraction`][pact.v3.pact.interaction.HttpInteraction] + - [`AsyncMessageInteraction`][pact.v3.pact.interaction.AsyncMessageInteraction] + - [`SyncMessageInteraction`][pact.v3.pact.interaction.SyncMessageInteraction] + + A set of interactions between a consumer and a provider is called a Pact. + """ + + def __init__(self, description: str) -> None: + """ + Create a new Interaction. + + As this class is abstract, this function should not be called directly + but should instead be called through one of the concrete subclasses. + + Args: + description: + Description of the interaction. This must be unique within the + Pact. + """ + self._description = description + + def __str__(self) -> str: + """ + Nice representation of the Interaction. + """ + return f"{self.__class__.__name__}({self._description})" + + def __repr__(self) -> str: + """ + Debugging representation of the Interaction. + """ + return f"{self.__class__.__name__}({self._handle!r})" + + @property + @abc.abstractmethod + def _handle(self) -> pact.v3.ffi.InteractionHandle: + """ + Handle for the Interaction. + + This is used internally by the library to pass the Interaction to the + underlying Pact library. + """ + + @property + @abc.abstractmethod + def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + """ + Interaction part. + + Where interactions have multiple parts, this property keeps track + of which part is currently being set. + """ + + def _parse_interaction_part( + self, + part: Literal["Request", "Response", None], + ) -> pact.v3.ffi.InteractionPart: + """ + Convert the input into an InteractionPart. + """ + if part == "Request": + return pact.v3.ffi.InteractionPart.REQUEST + if part == "Response": + return pact.v3.ffi.InteractionPart.RESPONSE + if part is None: + return self._interaction_part + msg = f"Invalid part: {part}" + raise ValueError(msg) + + @overload + def given(self, state: str) -> Self: ... + + @overload + def given(self, state: str, *, name: str, value: str) -> Self: ... + + @overload + def given(self, state: str, *, parameters: dict[str, Any] | str) -> Self: ... + + def given( + self, + state: str, + *, + name: str | None = None, + value: str | None = None, + parameters: dict[str, Any] | str | None = None, + ) -> Self: + """ + Set the provider state. + + This is the state that the provider should be in when the Interaction is + executed. When the provider is being verified, the provider state is + passed to the provider so that its internal state can be set to match + the provider state. + + In its simplest form, the provider state is a string. For example, to + match a provider state of `a user exists`, you would use: + + ```python + pact.upon_receiving("a request").given("a user exists") + ``` + + It is also possible to specify a parameter that will be used to match + the provider state. For example, to match a provider state of `a user + exists` with a parameter `id` that has the value `123`, you would use: + + ```python + ( + pact.upon_receiving("a request").given( + "a user exists", name="id", value="123" + ) + ) + ``` + + Lastly, it is possible to specify multiple parameters that will be used + to match the provider state. For example, to match a provider state of + `a user exists` with a parameter `id` that has the value `123` and a + parameter `name` that has the value `John`, you would use: + + ```python + ( + pact.upon_receiving("a request").given( + "a user exists", + parameters={ + "id": "123", + "name": "John", + }, + ) + ) + ``` + + This function can be called repeatedly to specify multiple provider + states for the same Interaction. If the same `state` is specified with + different parameters, then the parameters are merged together. The above + example with multiple parameters can equivalently be specified as: + + ```python + ( + pact.upon_receiving("a request") + .given("a user exists", name="id", value="123") + .given("a user exists", name="name", value="John") + ) + ``` + + Args: + state: + Provider state for the Interaction. + + name: + Name of the parameter. This must be specified in conjunction + with `value`. + + value: + Value of the parameter. This must be specified in conjunction + with `name`. + + parameters: + Key-value pairs of parameters to use for the provider state. + These must be encodable using [`json.dumps(...)`][json.dumps]. + Alternatively, a string contained the JSON object can be passed + directly. + + If the string does not contain a valid JSON object, then the + string is passed directly as follows: + + ```python + ( + pact.upon_receiving("a request").given( + "a user exists", name="value", value=parameters + ) + ) + ``` + + Raises: + ValueError: + If the combination of arguments is invalid or inconsistent. + """ + if name is not None and value is not None and parameters is None: + pact.v3.ffi.given_with_param(self._handle, state, name, value) + elif name is None and value is None and parameters is not None: + if isinstance(parameters, dict): + pact.v3.ffi.given_with_params( + self._handle, + state, + json.dumps(parameters), + ) + else: + pact.v3.ffi.given_with_params(self._handle, state, parameters) + elif name is None and value is None and parameters is None: + pact.v3.ffi.given(self._handle, state) + else: + msg = "Invalid combination of arguments." + raise ValueError(msg) + return self + + def with_body( + self, + body: str | None = None, + content_type: str | None = None, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Set the body of the request or response. + + Args: + body: + Body of the request. If this is `None`, then the body is + empty. + + content_type: + Content type of the body. This is ignored if the `Content-Type` + header has already been set. + + part: + Whether the body should be added to the request or the response. + If `None`, then the function intelligently determines whether + the body should be added to the request or the response, based + on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + pact.v3.ffi.with_body( + self._handle, + self._parse_interaction_part(part), + content_type, + body, + ) + return self + + def with_binary_body( + self, + body: bytes | None, + content_type: str | None = None, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Adds a binary body to the request or response. + + Note that for HTTP interactions, this function will overwrite the body + if it has been set using + [`with_body(...)`][pact.v3.Interaction.with_body]. + + Args: + part: + Whether the body should be added to the request or the response. + If `None`, then the function intelligently determines whether + the body should be added to the request or the response, based + on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + + content_type: + Content type of the body. This is ignored if the `Content-Type` + header has already been set. + + body: + Body of the request. + """ + pact.v3.ffi.with_binary_file( + self._handle, + self._parse_interaction_part(part), + content_type, + body, + ) + return self + + def with_multipart_file( # noqa: PLR0913 + self, + part_name: str, + path: Path | None, + content_type: str | None = None, + part: Literal["Request", "Response"] | None = None, + boundary: str | None = None, + ) -> Self: + """ + Adds a binary file as the body of a multipart request or response. + + The content type of the body will be set to a MIME multipart message. + """ + pact.v3.ffi.with_multipart_file_v2( + self._handle, + self._parse_interaction_part(part), + content_type, + path, + part_name, + boundary, + ) + return self + + def set_key(self, key: str | None) -> Self: + """ + Sets the key for the interaction. + + This is used by V4 interactions to set the key of the interaction, which + can subsequently used to reference the interaction. + """ + pact.v3.ffi.set_key(self._handle, key) + return self + + def set_pending(self, *, pending: bool) -> Self: + """ + Mark the interaction as pending. + + This is used by V4 interactions to mark the interaction as pending, in + which case the provider is not expected to honour the interaction. + """ + pact.v3.ffi.set_pending(self._handle, pending=pending) + return self + + def set_comment(self, key: str, value: Any | None) -> Self: # noqa: ANN401 + """ + Set a comment for the interaction. + + This is used by V4 interactions to set a comment for the interaction. A + comment consists of a key-value pair, where the key is a string and the + value is anything that can be encoded as JSON. + + Args: + key: + Key for the comment. + + value: + Value for the comment. This must be encodable using + [`json.dumps(...)`][json.dumps], or an existing JSON string. The + value of `None` will remove the comment with the given key. + """ + if isinstance(value, str) or value is None: + pact.v3.ffi.set_comment(self._handle, key, value) + else: + pact.v3.ffi.set_comment(self._handle, key, json.dumps(value)) + return self + + def test_name( + self, + name: str, + ) -> Self: + """ + Set the test name annotation for the interaction. + + This is used by V4 interactions to set the name of the test. + + Args: + name: + Name of the test. + """ + pact.v3.ffi.interaction_test_name(self._handle, name) + return self + + def with_plugin_contents( + self, + contents: dict[str, Any] | str, + content_type: str, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Set the interaction content using a plugin. + + The value of `contents` is passed directly to the plugin as a JSON + string. The plugin will document the format of the JSON content. + + Args: + contents: + Body of the request. If this is `None`, then the body is empty. + + content_type: + Content type of the body. This is ignored if the `Content-Type` + header has already been set. + + part: + Whether the body should be added to the request or the response. + If `None`, then the function intelligently determines whether + the body should be added to the request or the response, based + on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + if isinstance(contents, dict): + contents = json.dumps(contents) + + pact.v3.ffi.interaction_contents( + self._handle, + self._parse_interaction_part(part), + content_type, + contents, + ) + return self + + def with_matching_rules( + self, + rules: dict[str, Any] | str, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Add matching rules to the interaction. + + Matching rules are used to specify how the request or response should be + matched. This is useful for specifying that certain parts of the request + or response are flexible, such as the date or time. + + Args: + rules: + Matching rules to add to the interaction. This must be + encodable using [`json.dumps(...)`][json.dumps], or a string. + + part: + Whether the matching rules should be added to the request or the + response. If `None`, then the function intelligently determines + whether the matching rules should be added to the request or the + response, based on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + if isinstance(rules, dict): + rules = json.dumps(rules) + + pact.v3.ffi.with_matching_rules( + self._handle, + self._parse_interaction_part(part), + rules, + ) + return self diff --git a/src/pact/v3/interaction/async_message_interaction.py b/src/pact/v3/interaction/async_message_interaction.py new file mode 100644 index 000000000..19436c857 --- /dev/null +++ b/src/pact/v3/interaction/async_message_interaction.py @@ -0,0 +1,60 @@ +""" +Pact between a consumer and a provider. + +This module defines the classes that are used to define a Pact between a +consumer and a provider. It defines the interactions between the two parties, +and provides the functionality to verify that the interactions are satisfied. + +For the roles of consumer and provider, see the documentation for the +`pact.v3.service` module. +""" + +from __future__ import annotations + +import pact.v3.ffi +from pact.v3.interaction import Interaction + + +class AsyncMessageInteraction(Interaction): + """ + An asynchronous message interaction. + + This class defines an asynchronous message interaction between a consumer + and a provider. It defines the kind of messages a consumer can accept, and + the is agnostic of the underlying protocol, be it a message queue, Apache + Kafka, or some other asynchronous protocol. + """ + + def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: + """ + Initialise a new Asynchronous Message Interaction. + + This function should not be called directly. Instead, an + AsyncMessageInteraction should be created using the + [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a + [`Pact`][pact.v3.Pact] instance using the `"Async"` interaction type. + + Args: + pact_handle: + Handle for the Pact. + + description: + Description of the interaction. This must be unique within the + Pact. + """ + super().__init__(description) + self.__handle = pact.v3.ffi.new_message_interaction(pact_handle, description) + + @property + def _handle(self) -> pact.v3.ffi.InteractionHandle: + """ + Handle for the Interaction. + + This is used internally by the library to pass the Interaction to the + underlying Pact library. + """ + return self.__handle + + @property + def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + return pact.v3.ffi.InteractionPart.REQUEST diff --git a/src/pact/v3/interaction/http_interaction.py b/src/pact/v3/interaction/http_interaction.py new file mode 100644 index 000000000..8fdf64e7e --- /dev/null +++ b/src/pact/v3/interaction/http_interaction.py @@ -0,0 +1,426 @@ +""" +Pact between a consumer and a provider. + +This module defines the classes that are used to define a Pact between a +consumer and a provider. It defines the interactions between the two parties, +and provides the functionality to verify that the interactions are satisfied. + +For the roles of consumer and provider, see the documentation for the +`pact.v3.service` module. +""" + +from __future__ import annotations + +from collections import defaultdict +from typing import TYPE_CHECKING, Iterable, Literal + +import pact.v3.ffi +from pact.v3.interaction import Interaction + +if TYPE_CHECKING: + try: + from typing import Self + except ImportError: + from typing_extensions import Self + + +class HttpInteraction(Interaction): + """ + A synchronous HTTP interaction. + + This class defines a synchronous HTTP interaction between a consumer and a + provider. It defines a specific request that the consumer makes to the + provider, and the response that the provider should return. + """ + + def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: + """ + Initialise a new HTTP Interaction. + + This function should not be called directly. Instead, an Interaction + should be created using the + [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a + [`Pact`][pact.v3.Pact] instance. + """ + super().__init__(description) + self.__handle = pact.v3.ffi.new_interaction(pact_handle, description) + self.__interaction_part = pact.v3.ffi.InteractionPart.REQUEST + self._request_indices: dict[ + tuple[pact.v3.ffi.InteractionPart, str], + int, + ] = defaultdict(int) + self._parameter_indices: dict[str, int] = defaultdict(int) + + @property + def _handle(self) -> pact.v3.ffi.InteractionHandle: + """ + Handle for the Interaction. + + This is used internally by the library to pass the Interaction to the + underlying Pact library. + """ + return self.__handle + + @property + def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + """ + Interaction part. + + Keeps track whether we are setting by default the request or the + response in the HTTP interaction. + """ + return self.__interaction_part + + def with_request(self, method: str, path: str) -> Self: + """ + Set the request. + + This is the request that the consumer will make to the provider. + + Args: + method: + HTTP method for the request. + path: + Path for the request. + """ + pact.v3.ffi.with_request(self._handle, method, path) + return self + + def with_header( + self, + name: str, + value: str, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + r""" + Add a header to the request. + + # Repeated Headers + + If the same header has multiple values ([see RFC9110 + §5.2](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.2)), then + the same header must be specified multiple times with _order being + preserved_. For example + + ```python + ( + pact.upon_receiving("a request") + .with_header("X-Foo", "bar") + .with_header("X-Foo", "baz") + ) + ``` + + will expect a request with the following headers: + + ```http + X-Foo: bar + X-Foo: baz + # Or, equivalently: + X-Foo: bar, baz + ``` + + Note that repeated headers are _case insensitive_ in accordance with + [RFC 9110 + §5.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.1). + + # JSON Matching + + Pact's matching rules are defined in the [upstream + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md) + and support a wide range of matching rules. These can be specified + using a JSON object as a strong using `json.dumps(...)`. For example, + the above rule whereby the `X-Foo` header has multiple values can be + specified as: + + ```python + ( + pact.upon_receiving("a request") + .with_header( + "X-Foo", + json.dumps({ + "value": ["bar", "baz"], + }), + ) + ) + ``` + + It is also possible to have a more complicated Regex pattern for the + header. For example, a pattern for an `Accept-Version` header might be + specified as: + + ```python + ( + pact.upon_receiving("a request").with_header( + "Accept-Version", + json.dumps({ + "value": "1.2.3", + "pact:matcher:type": "regex", + "regex": r"\d+\.\d+\.\d+", + }), + ) + ) + ``` + + If the value of the header is expected to be a JSON object and clashes + with the above syntax, then it is recommended to make use of the + [`set_header(...)`][pact.v3.Interaction.set_header] method instead. + + Args: + name: + Name of the header. + + value: + Value of the header. + + part: + Whether the header should be added to the request or the + response. If `None`, then the function intelligently determines + whether the header should be added to the request or the + response, based on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + interaction_part = self._parse_interaction_part(part) + name_lower = name.lower() + index = self._request_indices[(interaction_part, name_lower)] + self._request_indices[(interaction_part, name_lower)] += 1 + pact.v3.ffi.with_header_v2( + self._handle, + interaction_part, + name, + index, + value, + ) + return self + + def with_headers( + self, + headers: dict[str, str] | Iterable[tuple[str, str]], + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Add multiple headers to the request. + + Note that due to the requirement of Python dictionaries to + have unique keys, it is _not_ possible to specify a header multiple + times to create a multi-valued header. Instead, you may: + + - Use an alternative data structure. Any iterable of key-value pairs + is accepted, including a list of tuples, a list of lists, or a + dictionary view. + + - Make multiple calls to + [`with_header(...)`][pact.v3.Interaction.with_header] or + [`with_headers(...)`][pact.v3.Interaction.with_headers]. + + - Specify the multiple values in a JSON object of the form: + + ```python + ( + pact.upon_receiving("a request") + .with_headers({ + "X-Foo": json.dumps({ + "value": ["bar", "baz"], + }), + ) + ) + ``` + + See [`with_header(...)`][pact.v3.Interaction.with_header] for more + information. + + Args: + headers: + Headers to add to the request. + + part: + Whether the header should be added to the request or the + response. If `None`, then the function intelligently determines + whether the header should be added to the request or the + response, based on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.with_header(name, value, part) + return self + + def set_header( + self, + name: str, + value: str, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + r""" + Add a header to the request. + + Unlike [`with_header(...)`][pact.v3.Interaction.with_header], this + function does no additional processing of the header value. This is + useful for headers that contain a JSON object. + + Args: + name: + Name of the header. + + value: + Value of the header. + + part: + Whether the header should be added to the request or the + response. If `None`, then the function intelligently determines + whether the header should be added to the request or the + response, based on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + pact.v3.ffi.set_header( + self._handle, + self._parse_interaction_part(part), + name, + value, + ) + return self + + def set_headers( + self, + headers: dict[str, str] | Iterable[tuple[str, str]], + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Add multiple headers to the request. + + This function intelligently determines whether the header should be + added to the request or the response, based on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] method + has been called. + + See [`set_header(...)`][pact.v3.Interaction.set_header] for more + information. + + Args: + headers: + Headers to add to the request. + + part: + Whether the headers should be added to the request or the + response. If `None`, then the function intelligently determines + whether the header should be added to the request or the + response, based on whether the + [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + method has been called. + """ + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.set_header(name, value, part) + return self + + def with_query_parameter(self, name: str, value: str) -> Self: + r""" + Add a query to the request. + + This is the query parameter(s) that the consumer will send to the + provider. + + If the same parameter can support multiple values, then the same + parameter can be specified multiple times: + + ```python + ( + pact.upon_receiving("a request") + .with_query_parameter("name", "John") + .with_query_parameter("name", "Mary") + ) + ``` + + The above can equivalently be specified as: + + ```python + ( + pact.upon_receiving("a request").with_query_parameter( + "name", + json.dumps({ + "value": ["John", "Mary"], + }), + ) + ) + ``` + + It is also possible to have a more complicated Regex pattern for the + paramater. For example, a pattern for an `version` parameter might be + specified as: + + ```python + ( + pact.upon_receiving("a request").with_query_parameter( + "version", + json.dumps({ + "value": "1.2.3", + "pact:matcher:type": "regex", + "regex": r"\d+\.\d+\.\d+", + }), + ) + ) + ``` + + For more information on the format of the JSON object, see the [upstream + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). + + Args: + name: + Name of the query parameter. + + value: + Value of the query parameter. + """ + index = self._parameter_indices[name] + self._parameter_indices[name] += 1 + pact.v3.ffi.with_query_parameter_v2( + self._handle, + name, + index, + value, + ) + return self + + def with_query_parameters( + self, + parameters: dict[str, str] | Iterable[tuple[str, str]], + ) -> Self: + """ + Add multiple query parameters to the request. + + See [`with_query_parameter(...)`][pact.v3.Interaction.with_query_parameter] + for more information. + + Args: + parameters: + Query parameters to add to the request. + """ + if isinstance(parameters, dict): + parameters = parameters.items() + for name, value in parameters: + self.with_query_parameter(name, value) + return self + + def will_respond_with(self, status: int) -> Self: + """ + Set the response status. + + Ideally, this function is called once all of the request information has + been set. This allows functions such as + [`with_header(...)`][pact.v3.Interaction.with_header] to intelligently + determine whether this is a request or response header. + + Alternatively, the `part` argument can be used to explicitly specify + whether the header should be added to the request or the response. + + Args: + status: + Status for the response. + """ + pact.v3.ffi.response_status(self._handle, status) + self.__interaction_part = pact.v3.ffi.InteractionPart.RESPONSE + return self diff --git a/src/pact/v3/interaction/sync_message_interaction.py b/src/pact/v3/interaction/sync_message_interaction.py new file mode 100644 index 000000000..59e32487e --- /dev/null +++ b/src/pact/v3/interaction/sync_message_interaction.py @@ -0,0 +1,685 @@ +""" +Pact between a consumer and a provider. + +This module defines the classes that are used to define a Pact between a +consumer and a provider. It defines the interactions between the two parties, +and provides the functionality to verify that the interactions are satisfied. + +For the roles of consumer and provider, see the documentation for the +`pact.v3.service` module. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal, Set, overload + +from yarl import URL + +import pact.v3.ffi +from pact.v3.interaction import Interaction +from pact.v3.interaction.async_message_interaction import AsyncMessageInteraction +from pact.v3.interaction.http_interaction import HttpInteraction + +if TYPE_CHECKING: + from types import TracebackType + + try: + from typing import Self + except ImportError: + from typing_extensions import Self + + +class SyncMessageInteraction(Interaction): + """ + A synchronous message interaction. + + This class defines a synchronous message interaction between a consumer and + a provider. As with [`HttpInteraction`][pact.v3.pact.HttpInteraction], it + defines a specific request that the consumer makes to the provider, and the + response that the provider should return. + """ + + def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: + """ + Initialise a new Synchronous Message Interaction. + + This function should not be called directly. Instead, an + AsyncMessageInteraction should be created using the + [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a + [`Pact`][pact.v3.Pact] instance using the `"Sync"` interaction type. + + Args: + pact_handle: + Handle for the Pact. + + description: + Description of the interaction. This must be unique within the + Pact. + """ + super().__init__(description) + self.__handle = pact.v3.ffi.new_sync_message_interaction( + pact_handle, + description, + ) + self.__interaction_part = pact.v3.ffi.InteractionPart.REQUEST + + @property + def _handle(self) -> pact.v3.ffi.InteractionHandle: + """ + Handle for the Interaction. + + This is used internally by the library to pass the Interaction to the + underlying Pact library. + """ + return self.__handle + + @property + def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + return self.__interaction_part + + +class Pact: + """ + A Pact between a consumer and a provider. + + This class defines a Pact between a consumer and a provider. It is the + central class in Pact's framework, and is responsible for defining the + interactions between the two parties. + + One Pact instance should be created for each provider that a consumer + interacts with. This instance can then be used to define the interactions + between the two parties. + """ + + def __init__( + self, + consumer: str, + provider: str, + ) -> None: + """ + Initialise a new Pact. + + Args: + consumer: + Name of the consumer. + + provider: + Name of the provider. + """ + if not consumer: + msg = "Consumer name cannot be empty." + raise ValueError(msg) + if not provider: + msg = "Provider name cannot be empty." + raise ValueError(msg) + + self._consumer = consumer + self._provider = provider + self._interactions: Set[Interaction] = set() + self._handle: pact.v3.ffi.PactHandle = pact.v3.ffi.new_pact( + consumer, + provider, + ) + + def __str__(self) -> str: + """ + Informal string representation of the Pact. + """ + return f"{self.consumer} -> {self.provider}" + + def __repr__(self) -> str: + """ + Information-rich string representation of the Pact. + """ + return "".format( + ", ".join( + [ + f"consumer={self.consumer!r}", + f"provider={self.provider!r}", + f"handle={self._handle!r}", + ], + ), + ) + + @property + def consumer(self) -> str: + """ + Consumer name. + """ + return self._consumer + + @property + def provider(self) -> str: + """ + Provider name. + """ + return self._provider + + def with_specification( + self, + version: str | pact.v3.ffi.PactSpecification, + ) -> Self: + """ + Set the Pact specification version. + + The Pact specification version indicates the features which are + supported by the Pact, and certain default behaviours. + + Args: + version: + Pact specification version. The can be either a string or a + [`PactSpecification`][pact.v3.ffi.PactSpecification] instance. + + The version string is case insensitive and has an optional `v` + prefix. + """ + if isinstance(version, str): + version = version.upper().replace(".", "_") + if version.startswith("V"): + version = pact.v3.ffi.PactSpecification[version] + else: + version = pact.v3.ffi.PactSpecification["V" + version] + pact.v3.ffi.with_specification(self._handle, version) + return self + + def using_plugin(self, name: str, version: str | None = None) -> Self: + """ + Add a plugin to be used by the test. + + Plugins extend the functionality of Pact. + + Args: + name: + Name of the plugin. + + version: + Version of the plugin. This is optional and can be `None`. + """ + pact.v3.ffi.using_plugin(self._handle, name, version) + return self + + def with_metadata( + self, + namespace: str, + metadata: dict[str, str], + ) -> Self: + """ + Set additional metadata for the Pact. + + A common use for this function is to add information about the client + library (name, version, hash, etc.) to the Pact. + + Args: + namespace: + Namespace for the metadata. This is used to group the metadata + together. + + metadata: + Key-value pairs of metadata to add to the Pact. + """ + for k, v in metadata.items(): + pact.v3.ffi.with_pact_metadata(self._handle, namespace, k, v) + return self + + @overload + def upon_receiving( + self, + description: str, + interaction: Literal["HTTP"] = ..., + ) -> HttpInteraction: ... + + @overload + def upon_receiving( + self, + description: str, + interaction: Literal["Async"], + ) -> AsyncMessageInteraction: ... + + @overload + def upon_receiving( + self, + description: str, + interaction: Literal["Sync"], + ) -> SyncMessageInteraction: ... + + def upon_receiving( + self, + description: str, + interaction: Literal["HTTP", "Sync", "Async"] = "HTTP", + ) -> HttpInteraction | AsyncMessageInteraction | SyncMessageInteraction: + """ + Create a new Interaction. + + This is an alias for [`interaction(...)`][pact.v3.Pact.interaction]. + + Args: + description: + Description of the interaction. This must be unique + within the Pact. + + interaction: + Type of interaction. Defaults to `HTTP`. This must be one of + `HTTP`, `Async`, or `Sync`. + """ + if interaction == "HTTP": + return HttpInteraction(self._handle, description) + if interaction == "Async": + return AsyncMessageInteraction(self._handle, description) + if interaction == "Sync": + return SyncMessageInteraction(self._handle, description) + + msg = f"Invalid interaction type: {interaction}" + raise ValueError(msg) + + def serve( # noqa: PLR0913 + self, + addr: str = "localhost", + port: int = 0, + transport: str = "http", + transport_config: str | None = None, + *, + raises: bool = True, + ) -> PactServer: + """ + Return a mock server for the Pact. + + This function configures a mock server for the Pact. The mock server + is then started when the Pact is entered into a `with` block: + + ```python + pact = Pact("consumer", "provider") + with pact.serve() as srv: + ... + ``` + + Args: + addr: + Address to bind the mock server to. Defaults to `localhost`. + + port: + Port to bind the mock server to. Defaults to `0`, which will + select a random port. + + transport: + Transport to use for the mock server. Defaults to `HTTP`. + + transport_config: + Configuration for the transport. This is specific to the + transport being used and should be a JSON string. + + raises: Whether to raise an exception if there are mismatches + between the Pact and the server. If set to `False`, then the + mismatches must be handled manually. + + Returns: + A [`PactServer`][pact.v3.pact.PactServer] instance. + """ + return PactServer( + self._handle, + addr, + port, + transport, + transport_config, + raises=raises, + ) + + def messages(self) -> pact.v3.ffi.PactMessageIterator: + """ + Iterate over the messages in the Pact. + + This function returns an iterator over the messages in the Pact. This + is useful for validating the Pact against the provider. + + ```python + pact = Pact("consumer", "provider") + with pact.serve() as srv: + for message in pact.messages(): + # Validate the message against the provider. + ... + ``` + + Note that the Pact must be written to a file before the messages can be + iterated over. This is because the messages are not stored in memory, + but rather are streamed directly from the file. + """ + return pact.v3.ffi.pact_handle_get_message_iter(self._handle) + + @overload + def interactions( + self, + kind: Literal["HTTP"], + ) -> pact.v3.ffi.PactSyncHttpIterator: ... + + @overload + def interactions( + self, + kind: Literal["Sync"], + ) -> pact.v3.ffi.PactSyncMessageIterator: ... + + @overload + def interactions( + self, + kind: Literal["Async"], + ) -> pact.v3.ffi.PactMessageIterator: ... + + def interactions( + self, + kind: str = "HTTP", + ) -> ( + pact.v3.ffi.PactSyncHttpIterator + | pact.v3.ffi.PactSyncMessageIterator + | pact.v3.ffi.PactMessageIterator + ): + """ + Return an iterator over the Pact's interactions. + + The kind is used to specify the type of interactions that will be + iterated over. + """ + # TODO: Add an iterator for `All` interactions. + # https://github.com/pact-foundation/pact-python/issues/451 + if kind == "HTTP": + return pact.v3.ffi.pact_handle_get_sync_http_iter(self._handle) + if kind == "Sync": + return pact.v3.ffi.pact_handle_get_sync_message_iter(self._handle) + if kind == "Async": + return pact.v3.ffi.pact_handle_get_message_iter(self._handle) + msg = f"Unknown interaction type: {kind}" + raise ValueError(msg) + + def write_file( + self, + directory: Path | str | None = None, + *, + overwrite: bool = False, + ) -> None: + """ + Write out the pact to a file. + + This function should be called once all of the consumer tests have been + run. It writes the Pact to a file, which can then be used to validate + the provider. + + Args: + directory: + The directory to write the pact to. If the directory does not + exist, it will be created. The filename will be + automatically generated from the underlying Pact. + + overwrite: + If set to True, the file will be overwritten if it already + exists. Otherwise, the contents of the file will be merged with + the existing file. + """ + if directory is None: + directory = Path.cwd() + pact.v3.ffi.pact_handle_write_file( + self._handle, + directory, + overwrite=overwrite, + ) + + +class MismatchesError(Exception): + """ + Exception raised when there are mismatches between the Pact and the server. + """ + + def __init__(self, mismatches: list[dict[str, Any]]) -> None: + """ + Initialise a new MismatchesError. + + Args: + mismatches: + Mismatches between the Pact and the server. + """ + super().__init__(f"Mismatched interaction (count: {len(mismatches)})") + self._mismatches = mismatches + + @property + def mismatches(self) -> list[dict[str, Any]]: + """ + Mismatches between the Pact and the server. + """ + return self._mismatches + + +class PactServer: + """ + Pact Server. + + This class handles the lifecycle of the Pact mock server. It is responsible + for starting the mock server when the Pact is entered into a `with` block, + and stopping the mock server when the block is exited. + """ + + def __init__( # noqa: PLR0913 + self, + pact_handle: pact.v3.ffi.PactHandle, + host: str = "localhost", + port: int = 0, + transport: str = "HTTP", + transport_config: str | None = None, + *, + raises: bool = True, + ) -> None: + """ + Initialise a new Pact Server. + + This function should not be called directly. Instead, a Pact Server + should be created using the + [`serve(...)`][pact.v3.Pact.serve] method of a + [`Pact`][pact.v3.Pact] instance: + + ```python + pact = Pact("consumer", "provider") + with pact.serve(...) as srv: + ... + ``` + + Args: + pact_handle: + Handle for the Pact. + + host: + Hostname of IP for the mock server. + + port: + Port to bind the mock server to. The value of `0` will select a + random available port. + + transport: + Transport to use for the mock server. + + transport_config: + Configuration for the transport. This is specific to the + transport being used and should be a JSON string. + + raises: Whether or not to raise an exception if the server + is not matched upon exit. + """ + self._host = host + self._port = port + self._transport = transport + self._transport_config = transport_config + self._pact_handle = pact_handle + self._handle: None | pact.v3.ffi.PactServerHandle = None + self._raises = raises + + @property + def port(self) -> int: + """ + Port on which the server is running. + + If the server is not running, then this will be `0`. + """ + # Unlike the other properties, this value might be different to what was + # passed in to the constructor as the server can be started on a random + # port. + return self._handle.port if self._handle else 0 + + @property + def host(self) -> str: + """ + Address to which the server is bound. + """ + return self._host + + @property + def transport(self) -> str: + """ + Transport method. + """ + return self._transport + + @property + def url(self) -> URL: + """ + Base URL for the server. + """ + return URL(str(self)) + + @property + def matched(self) -> bool: + """ + Whether or not the server has been matched. + + This is `True` if the server has been matched, and `False` otherwise. + """ + if not self._handle: + msg = "The server is not running." + raise RuntimeError(msg) + return pact.v3.ffi.mock_server_matched(self._handle) + + @property + def mismatches(self) -> list[dict[str, Any]]: + """ + Mismatches between the Pact and the server. + + This is a string containing the mismatches between the Pact and the + server. If there are no mismatches, then this is an empty string. + """ + if not self._handle: + msg = "The server is not running." + raise RuntimeError(msg) + return pact.v3.ffi.mock_server_mismatches(self._handle) + + @property + def logs(self) -> str | None: + """ + Logs from the server. + + This is a string containing the logs from the server. If there are no + logs, then this is `None`. For this to be populated, the logging must + be configured to make use of the internal buffer. + """ + if not self._handle: + msg = "The server is not running." + raise RuntimeError(msg) + + try: + return pact.v3.ffi.mock_server_logs(self._handle) + except RuntimeError: + return None + + def __str__(self) -> str: + """ + URL for the server. + """ + return f"{self.transport}://{self.host}:{self.port}" + + def __repr__(self) -> str: + """ + Information-rich string representation of the Pact Server. + """ + return "".format( + ", ".join( + [ + f"transport={self.transport!r}", + f"host={self.host!r}", + f"port={self.port!r}", + f"handle={self._handle!r}", + f"pact={self._pact_handle!r}", + ], + ), + ) + + def __enter__(self) -> Self: + """ + Launch the server. + + Once the server is running, it is generally no possible to make + modifications to the underlying Pact. + """ + self._handle = pact.v3.ffi.create_mock_server_for_transport( + self._pact_handle, + self._host, + self._port, + self._transport, + self._transport_config, + ) + + return self + + def __exit__( + self, + _exc_type: type[BaseException] | None, + _exc_value: BaseException | None, + _traceback: TracebackType | None, + ) -> None: + """ + Stop the server. + + Raises: + MismatchesError: + If the server has not been fully matched and the server is + configured to raise an exception. + """ + if self._handle: + if self._raises and not self.matched: + raise MismatchesError(self.mismatches) + self._handle = None + + def __truediv__(self, other: str | object) -> URL: + """ + URL for the server. + """ + if isinstance(other, str): + return self.url / other + return NotImplemented + + def write_file( + self, + directory: str | Path | None = None, + *, + overwrite: bool = False, + ) -> None: + """ + Write out the pact to a file. + + Args: + directory: + The directory to write the pact to. If the directory does not + exist, it will be created. The filename will be + automatically generated from the underlying Pact. + + overwrite: + Whether or not to overwrite the file if it already exists. + """ + if not self._handle: + msg = "The server is not running." + raise RuntimeError(msg) + + directory = Path(directory) if directory else Path.cwd() + if not directory.exists(): + directory.mkdir(parents=True) + elif not directory.is_dir(): + msg = f"{directory} is not a directory" + raise ValueError(msg) + + pact.v3.ffi.write_pact_file( + self._handle, + str(directory), + overwrite=overwrite, + ) diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index d509f270b..d1ac9b027 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -11,950 +11,27 @@ from __future__ import annotations -import abc -import json -from collections import defaultdict from pathlib import Path -from typing import TYPE_CHECKING, Any, Iterable, Literal, Set, overload +from typing import TYPE_CHECKING, Any, Literal, Set, overload from yarl import URL import pact.v3.ffi +from pact.v3.interaction.async_message_interaction import AsyncMessageInteraction +from pact.v3.interaction.http_interaction import HttpInteraction +from pact.v3.interaction.sync_message_interaction import SyncMessageInteraction if TYPE_CHECKING: from types import TracebackType + from pact.v3.interaction import Interaction + try: from typing import Self except ImportError: from typing_extensions import Self -class Interaction(abc.ABC): - """ - Interaction between a consumer and a provider. - - This abstract class defines an interaction between a consumer and a - provider. The concrete subclasses define the type of interaction, and include: - - - [`HttpInteraction`][pact.v3.pact.HttpInteraction] - - [`AsyncMessageInteraction`][pact.v3.pact.AsyncMessageInteraction] - - [`SyncMessageInteraction`][pact.v3.pact.SyncMessageInteraction] - - A set of interactions between a consumer and a provider is called a Pact. - """ - - def __init__(self, description: str) -> None: - """ - Create a new Interaction. - - As this class is abstract, this function should not be called directly - but should instead be called through one of the concrete subclasses. - - Args: - description: - Description of the interaction. This must be unique within the - Pact. - """ - self._description = description - - def __str__(self) -> str: - """ - Nice representation of the Interaction. - """ - return f"{self.__class__.__name__}({self._description})" - - def __repr__(self) -> str: - """ - Debugging representation of the Interaction. - """ - return f"{self.__class__.__name__}({self._handle!r})" - - @property - @abc.abstractmethod - def _handle(self) -> pact.v3.ffi.InteractionHandle: - """ - Handle for the Interaction. - - This is used internally by the library to pass the Interaction to the - underlying Pact library. - """ - - @property - @abc.abstractmethod - def _interaction_part(self) -> pact.v3.ffi.InteractionPart: - """ - Interaction part. - - Where interactions have multiple parts, this property keeps track - of which part is currently being set. - """ - - def _parse_interaction_part( - self, - part: Literal["Request", "Response", None], - ) -> pact.v3.ffi.InteractionPart: - """ - Convert the input into an InteractionPart. - """ - if part == "Request": - return pact.v3.ffi.InteractionPart.REQUEST - if part == "Response": - return pact.v3.ffi.InteractionPart.RESPONSE - if part is None: - return self._interaction_part - msg = f"Invalid part: {part}" - raise ValueError(msg) - - @overload - def given(self, state: str) -> Self: ... - - @overload - def given(self, state: str, *, name: str, value: str) -> Self: ... - - @overload - def given(self, state: str, *, parameters: dict[str, Any] | str) -> Self: ... - - def given( - self, - state: str, - *, - name: str | None = None, - value: str | None = None, - parameters: dict[str, Any] | str | None = None, - ) -> Self: - """ - Set the provider state. - - This is the state that the provider should be in when the Interaction is - executed. When the provider is being verified, the provider state is - passed to the provider so that its internal state can be set to match - the provider state. - - In its simplest form, the provider state is a string. For example, to - match a provider state of `a user exists`, you would use: - - ```python - pact.upon_receiving("a request").given("a user exists") - ``` - - It is also possible to specify a parameter that will be used to match - the provider state. For example, to match a provider state of `a user - exists` with a parameter `id` that has the value `123`, you would use: - - ```python - ( - pact.upon_receiving("a request").given( - "a user exists", name="id", value="123" - ) - ) - ``` - - Lastly, it is possible to specify multiple parameters that will be used - to match the provider state. For example, to match a provider state of - `a user exists` with a parameter `id` that has the value `123` and a - parameter `name` that has the value `John`, you would use: - - ```python - ( - pact.upon_receiving("a request").given( - "a user exists", - parameters={ - "id": "123", - "name": "John", - }, - ) - ) - ``` - - This function can be called repeatedly to specify multiple provider - states for the same Interaction. If the same `state` is specified with - different parameters, then the parameters are merged together. The above - example with multiple parameters can equivalently be specified as: - - ```python - ( - pact.upon_receiving("a request") - .given("a user exists", name="id", value="123") - .given("a user exists", name="name", value="John") - ) - ``` - - Args: - state: - Provider state for the Interaction. - - name: - Name of the parameter. This must be specified in conjunction - with `value`. - - value: - Value of the parameter. This must be specified in conjunction - with `name`. - - parameters: - Key-value pairs of parameters to use for the provider state. - These must be encodable using [`json.dumps(...)`][json.dumps]. - Alternatively, a string contained the JSON object can be passed - directly. - - If the string does not contain a valid JSON object, then the - string is passed directly as follows: - - ```python - ( - pact.upon_receiving("a request").given( - "a user exists", name="value", value=parameters - ) - ) - ``` - - Raises: - ValueError: - If the combination of arguments is invalid or inconsistent. - """ - if name is not None and value is not None and parameters is None: - pact.v3.ffi.given_with_param(self._handle, state, name, value) - elif name is None and value is None and parameters is not None: - if isinstance(parameters, dict): - pact.v3.ffi.given_with_params( - self._handle, - state, - json.dumps(parameters), - ) - else: - pact.v3.ffi.given_with_params(self._handle, state, parameters) - elif name is None and value is None and parameters is None: - pact.v3.ffi.given(self._handle, state) - else: - msg = "Invalid combination of arguments." - raise ValueError(msg) - return self - - def with_body( - self, - body: str | None = None, - content_type: str | None = None, - part: Literal["Request", "Response"] | None = None, - ) -> Self: - """ - Set the body of the request or response. - - Args: - body: - Body of the request. If this is `None`, then the body is - empty. - - content_type: - Content type of the body. This is ignored if the `Content-Type` - header has already been set. - - part: - Whether the body should be added to the request or the response. - If `None`, then the function intelligently determines whether - the body should be added to the request or the response, based - on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - """ - pact.v3.ffi.with_body( - self._handle, - self._parse_interaction_part(part), - content_type, - body, - ) - return self - - def with_binary_body( - self, - body: bytes | None, - content_type: str | None = None, - part: Literal["Request", "Response"] | None = None, - ) -> Self: - """ - Adds a binary body to the request or response. - - Note that for HTTP interactions, this function will overwrite the body - if it has been set using - [`with_body(...)`][pact.v3.Interaction.with_body]. - - Args: - part: - Whether the body should be added to the request or the response. - If `None`, then the function intelligently determines whether - the body should be added to the request or the response, based - on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - - content_type: - Content type of the body. This is ignored if the `Content-Type` - header has already been set. - - body: - Body of the request. - """ - pact.v3.ffi.with_binary_file( - self._handle, - self._parse_interaction_part(part), - content_type, - body, - ) - return self - - def with_multipart_file( # noqa: PLR0913 - self, - part_name: str, - path: Path | None, - content_type: str | None = None, - part: Literal["Request", "Response"] | None = None, - boundary: str | None = None, - ) -> Self: - """ - Adds a binary file as the body of a multipart request or response. - - The content type of the body will be set to a MIME multipart message. - """ - pact.v3.ffi.with_multipart_file_v2( - self._handle, - self._parse_interaction_part(part), - content_type, - path, - part_name, - boundary, - ) - return self - - def set_key(self, key: str | None) -> Self: - """ - Sets the key for the interaction. - - This is used by V4 interactions to set the key of the interaction, which - can subsequently used to reference the interaction. - """ - pact.v3.ffi.set_key(self._handle, key) - return self - - def set_pending(self, *, pending: bool) -> Self: - """ - Mark the interaction as pending. - - This is used by V4 interactions to mark the interaction as pending, in - which case the provider is not expected to honour the interaction. - """ - pact.v3.ffi.set_pending(self._handle, pending=pending) - return self - - def set_comment(self, key: str, value: Any | None) -> Self: # noqa: ANN401 - """ - Set a comment for the interaction. - - This is used by V4 interactions to set a comment for the interaction. A - comment consists of a key-value pair, where the key is a string and the - value is anything that can be encoded as JSON. - - Args: - key: - Key for the comment. - - value: - Value for the comment. This must be encodable using - [`json.dumps(...)`][json.dumps], or an existing JSON string. The - value of `None` will remove the comment with the given key. - """ - if isinstance(value, str) or value is None: - pact.v3.ffi.set_comment(self._handle, key, value) - else: - pact.v3.ffi.set_comment(self._handle, key, json.dumps(value)) - return self - - def test_name( - self, - name: str, - ) -> Self: - """ - Set the test name annotation for the interaction. - - This is used by V4 interactions to set the name of the test. - - Args: - name: - Name of the test. - """ - pact.v3.ffi.interaction_test_name(self._handle, name) - return self - - def with_plugin_contents( - self, - contents: dict[str, Any] | str, - content_type: str, - part: Literal["Request", "Response"] | None = None, - ) -> Self: - """ - Set the interaction content using a plugin. - - The value of `contents` is passed directly to the plugin as a JSON - string. The plugin will document the format of the JSON content. - - Args: - contents: - Body of the request. If this is `None`, then the body is empty. - - content_type: - Content type of the body. This is ignored if the `Content-Type` - header has already been set. - - part: - Whether the body should be added to the request or the response. - If `None`, then the function intelligently determines whether - the body should be added to the request or the response, based - on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - """ - if isinstance(contents, dict): - contents = json.dumps(contents) - - pact.v3.ffi.interaction_contents( - self._handle, - self._parse_interaction_part(part), - content_type, - contents, - ) - return self - - def with_matching_rules( - self, - rules: dict[str, Any] | str, - part: Literal["Request", "Response"] | None = None, - ) -> Self: - """ - Add matching rules to the interaction. - - Matching rules are used to specify how the request or response should be - matched. This is useful for specifying that certain parts of the request - or response are flexible, such as the date or time. - - Args: - rules: - Matching rules to add to the interaction. This must be - encodable using [`json.dumps(...)`][json.dumps], or a string. - - part: - Whether the matching rules should be added to the request or the - response. If `None`, then the function intelligently determines - whether the matching rules should be added to the request or the - response, based on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - """ - if isinstance(rules, dict): - rules = json.dumps(rules) - - pact.v3.ffi.with_matching_rules( - self._handle, - self._parse_interaction_part(part), - rules, - ) - return self - - -class HttpInteraction(Interaction): - """ - A synchronous HTTP interaction. - - This class defines a synchronous HTTP interaction between a consumer and a - provider. It defines a specific request that the consumer makes to the - provider, and the response that the provider should return. - """ - - def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: - """ - Initialise a new HTTP Interaction. - - This function should not be called directly. Instead, an Interaction - should be created using the - [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a - [`Pact`][pact.v3.Pact] instance. - """ - super().__init__(description) - self.__handle = pact.v3.ffi.new_interaction(pact_handle, description) - self.__interaction_part = pact.v3.ffi.InteractionPart.REQUEST - self._request_indices: dict[ - tuple[pact.v3.ffi.InteractionPart, str], - int, - ] = defaultdict(int) - self._parameter_indices: dict[str, int] = defaultdict(int) - - @property - def _handle(self) -> pact.v3.ffi.InteractionHandle: - """ - Handle for the Interaction. - - This is used internally by the library to pass the Interaction to the - underlying Pact library. - """ - return self.__handle - - @property - def _interaction_part(self) -> pact.v3.ffi.InteractionPart: - """ - Interaction part. - - Keeps track whether we are setting by default the request or the - response in the HTTP interaction. - """ - return self.__interaction_part - - def with_request(self, method: str, path: str) -> Self: - """ - Set the request. - - This is the request that the consumer will make to the provider. - - Args: - method: - HTTP method for the request. - path: - Path for the request. - """ - pact.v3.ffi.with_request(self._handle, method, path) - return self - - def with_header( - self, - name: str, - value: str, - part: Literal["Request", "Response"] | None = None, - ) -> Self: - r""" - Add a header to the request. - - # Repeated Headers - - If the same header has multiple values ([see RFC9110 - §5.2](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.2)), then - the same header must be specified multiple times with _order being - preserved_. For example - - ```python - ( - pact.upon_receiving("a request") - .with_header("X-Foo", "bar") - .with_header("X-Foo", "baz") - ) - ``` - - will expect a request with the following headers: - - ```http - X-Foo: bar - X-Foo: baz - # Or, equivalently: - X-Foo: bar, baz - ``` - - Note that repeated headers are _case insensitive_ in accordance with - [RFC 9110 - §5.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.1). - - # JSON Matching - - Pact's matching rules are defined in the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md) - and support a wide range of matching rules. These can be specified - using a JSON object as a strong using `json.dumps(...)`. For example, - the above rule whereby the `X-Foo` header has multiple values can be - specified as: - - ```python - ( - pact.upon_receiving("a request") - .with_header( - "X-Foo", - json.dumps({ - "value": ["bar", "baz"], - }), - ) - ) - ``` - - It is also possible to have a more complicated Regex pattern for the - header. For example, a pattern for an `Accept-Version` header might be - specified as: - - ```python - ( - pact.upon_receiving("a request").with_header( - "Accept-Version", - json.dumps({ - "value": "1.2.3", - "pact:matcher:type": "regex", - "regex": r"\d+\.\d+\.\d+", - }), - ) - ) - ``` - - If the value of the header is expected to be a JSON object and clashes - with the above syntax, then it is recommended to make use of the - [`set_header(...)`][pact.v3.Interaction.set_header] method instead. - - Args: - name: - Name of the header. - - value: - Value of the header. - - part: - Whether the header should be added to the request or the - response. If `None`, then the function intelligently determines - whether the header should be added to the request or the - response, based on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - """ - interaction_part = self._parse_interaction_part(part) - name_lower = name.lower() - index = self._request_indices[(interaction_part, name_lower)] - self._request_indices[(interaction_part, name_lower)] += 1 - pact.v3.ffi.with_header_v2( - self._handle, - interaction_part, - name, - index, - value, - ) - return self - - def with_headers( - self, - headers: dict[str, str] | Iterable[tuple[str, str]], - part: Literal["Request", "Response"] | None = None, - ) -> Self: - """ - Add multiple headers to the request. - - Note that due to the requirement of Python dictionaries to - have unique keys, it is _not_ possible to specify a header multiple - times to create a multi-valued header. Instead, you may: - - - Use an alternative data structure. Any iterable of key-value pairs - is accepted, including a list of tuples, a list of lists, or a - dictionary view. - - - Make multiple calls to - [`with_header(...)`][pact.v3.Interaction.with_header] or - [`with_headers(...)`][pact.v3.Interaction.with_headers]. - - - Specify the multiple values in a JSON object of the form: - - ```python - ( - pact.upon_receiving("a request") - .with_headers({ - "X-Foo": json.dumps({ - "value": ["bar", "baz"], - }), - ) - ) - ``` - - See [`with_header(...)`][pact.v3.Interaction.with_header] for more - information. - - Args: - headers: - Headers to add to the request. - - part: - Whether the header should be added to the request or the - response. If `None`, then the function intelligently determines - whether the header should be added to the request or the - response, based on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - """ - if isinstance(headers, dict): - headers = headers.items() - for name, value in headers: - self.with_header(name, value, part) - return self - - def set_header( - self, - name: str, - value: str, - part: Literal["Request", "Response"] | None = None, - ) -> Self: - r""" - Add a header to the request. - - Unlike [`with_header(...)`][pact.v3.Interaction.with_header], this - function does no additional processing of the header value. This is - useful for headers that contain a JSON object. - - Args: - name: - Name of the header. - - value: - Value of the header. - - part: - Whether the header should be added to the request or the - response. If `None`, then the function intelligently determines - whether the header should be added to the request or the - response, based on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - """ - pact.v3.ffi.set_header( - self._handle, - self._parse_interaction_part(part), - name, - value, - ) - return self - - def set_headers( - self, - headers: dict[str, str] | Iterable[tuple[str, str]], - part: Literal["Request", "Response"] | None = None, - ) -> Self: - """ - Add multiple headers to the request. - - This function intelligently determines whether the header should be - added to the request or the response, based on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] method - has been called. - - See [`set_header(...)`][pact.v3.Interaction.set_header] for more - information. - - Args: - headers: - Headers to add to the request. - - part: - Whether the headers should be added to the request or the - response. If `None`, then the function intelligently determines - whether the header should be added to the request or the - response, based on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - """ - if isinstance(headers, dict): - headers = headers.items() - for name, value in headers: - self.set_header(name, value, part) - return self - - def with_query_parameter(self, name: str, value: str) -> Self: - r""" - Add a query to the request. - - This is the query parameter(s) that the consumer will send to the - provider. - - If the same parameter can support multiple values, then the same - parameter can be specified multiple times: - - ```python - ( - pact.upon_receiving("a request") - .with_query_parameter("name", "John") - .with_query_parameter("name", "Mary") - ) - ``` - - The above can equivalently be specified as: - - ```python - ( - pact.upon_receiving("a request").with_query_parameter( - "name", - json.dumps({ - "value": ["John", "Mary"], - }), - ) - ) - ``` - - It is also possible to have a more complicated Regex pattern for the - paramater. For example, a pattern for an `version` parameter might be - specified as: - - ```python - ( - pact.upon_receiving("a request").with_query_parameter( - "version", - json.dumps({ - "value": "1.2.3", - "pact:matcher:type": "regex", - "regex": r"\d+\.\d+\.\d+", - }), - ) - ) - ``` - - For more information on the format of the JSON object, see the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). - - Args: - name: - Name of the query parameter. - - value: - Value of the query parameter. - """ - index = self._parameter_indices[name] - self._parameter_indices[name] += 1 - pact.v3.ffi.with_query_parameter_v2( - self._handle, - name, - index, - value, - ) - return self - - def with_query_parameters( - self, - parameters: dict[str, str] | Iterable[tuple[str, str]], - ) -> Self: - """ - Add multiple query parameters to the request. - - See [`with_query_parameter(...)`][pact.v3.Interaction.with_query_parameter] - for more information. - - Args: - parameters: - Query parameters to add to the request. - """ - if isinstance(parameters, dict): - parameters = parameters.items() - for name, value in parameters: - self.with_query_parameter(name, value) - return self - - def will_respond_with(self, status: int) -> Self: - """ - Set the response status. - - Ideally, this function is called once all of the request information has - been set. This allows functions such as - [`with_header(...)`][pact.v3.Interaction.with_header] to intelligently - determine whether this is a request or response header. - - Alternatively, the `part` argument can be used to explicitly specify - whether the header should be added to the request or the response. - - Args: - status: - Status for the response. - """ - pact.v3.ffi.response_status(self._handle, status) - self.__interaction_part = pact.v3.ffi.InteractionPart.RESPONSE - return self - - -class AsyncMessageInteraction(Interaction): - """ - An asynchronous message interaction. - - This class defines an asynchronous message interaction between a consumer - and a provider. It defines the kind of messages a consumer can accept, and - the is agnostic of the underlying protocol, be it a message queue, Apache - Kafka, or some other asynchronous protocol. - """ - - def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: - """ - Initialise a new Asynchronous Message Interaction. - - This function should not be called directly. Instead, an - AsyncMessageInteraction should be created using the - [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a - [`Pact`][pact.v3.Pact] instance using the `"Async"` interaction type. - - Args: - pact_handle: - Handle for the Pact. - - description: - Description of the interaction. This must be unique within the - Pact. - """ - super().__init__(description) - self.__handle = pact.v3.ffi.new_message_interaction(pact_handle, description) - - @property - def _handle(self) -> pact.v3.ffi.InteractionHandle: - """ - Handle for the Interaction. - - This is used internally by the library to pass the Interaction to the - underlying Pact library. - """ - return self.__handle - - @property - def _interaction_part(self) -> pact.v3.ffi.InteractionPart: - return pact.v3.ffi.InteractionPart.REQUEST - - -class SyncMessageInteraction(Interaction): - """ - A synchronous message interaction. - - This class defines a synchronous message interaction between a consumer and - a provider. As with [`HttpInteraction`][pact.v3.pact.HttpInteraction], it - defines a specific request that the consumer makes to the provider, and the - response that the provider should return. - """ - - def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: - """ - Initialise a new Synchronous Message Interaction. - - This function should not be called directly. Instead, an - AsyncMessageInteraction should be created using the - [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a - [`Pact`][pact.v3.Pact] instance using the `"Sync"` interaction type. - - Args: - pact_handle: - Handle for the Pact. - - description: - Description of the interaction. This must be unique within the - Pact. - """ - super().__init__(description) - self.__handle = pact.v3.ffi.new_sync_message_interaction( - pact_handle, - description, - ) - self.__interaction_part = pact.v3.ffi.InteractionPart.REQUEST - - @property - def _handle(self) -> pact.v3.ffi.InteractionHandle: - """ - Handle for the Interaction. - - This is used internally by the library to pass the Interaction to the - underlying Pact library. - """ - return self.__handle - - @property - def _interaction_part(self) -> pact.v3.ffi.InteractionPart: - return self.__interaction_part - - class Pact: """ A Pact between a consumer and a provider. From ce1b6dcfaeb0cb4e8485ae05a0bdfb484e42d856 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 1 Mar 2024 14:49:15 +1100 Subject: [PATCH 0221/1376] feat(v3): add specification attribute to pacts Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 23 ++++++++++++++++++++++- src/pact/v3/pact.py | 13 ++++++++----- tests/v3/test_pact.py | 2 ++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index f860d68d6..52cfbc5e2 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -675,6 +675,27 @@ class PactSpecification(Enum): V3 = lib.PactSpecification_V3 V4 = lib.PactSpecification_V4 + @classmethod + def from_str(cls, version: str) -> PactSpecification: + """ + Instantiate a Pact Specification from a string. + + This method is case-insensitive, and allows for the version to be + specified with or without a leading "V", and with either a dot or an + underscore as the separator. + + Args: + version: + The version of the Pact Specification. + + Returns: + The Pact Specification. + """ + version = version.upper().replace(".", "_") + if version.startswith("V"): + return cls[version] + return cls["V" + version] + def __str__(self) -> str: """ Informal string representation of the Pact Specification. @@ -5280,7 +5301,7 @@ def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: Returns: The spec version for the Pact model. """ - raise NotImplementedError + return PactSpecification(lib.pactffi_handle_get_pact_spec_version(handle._ref)) def with_pact_metadata( diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index d1ac9b027..045145a4c 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -109,6 +109,13 @@ def provider(self) -> str: """ return self._provider + @property + def specification(self) -> pact.v3.ffi.PactSpecification: + """ + Pact specification version. + """ + return pact.v3.ffi.handle_get_pact_spec_version(self._handle) + def with_specification( self, version: str | pact.v3.ffi.PactSpecification, @@ -128,11 +135,7 @@ def with_specification( prefix. """ if isinstance(version, str): - version = version.upper().replace(".", "_") - if version.startswith("V"): - version = pact.v3.ffi.PactSpecification[version] - else: - version = pact.v3.ffi.PactSpecification["V" + version] + version = pact.v3.ffi.PactSpecification.from_str(version) pact.v3.ffi.with_specification(self._handle, version) return self diff --git a/tests/v3/test_pact.py b/tests/v3/test_pact.py index 56a6d1f58..5f32e7d80 100644 --- a/tests/v3/test_pact.py +++ b/tests/v3/test_pact.py @@ -10,6 +10,7 @@ import pytest from pact.v3 import Pact +from pact.v3.ffi import PactSpecification if TYPE_CHECKING: from pathlib import Path @@ -131,6 +132,7 @@ def test_write_file(pact: Pact, temp_dir: Path) -> None: ) def test_specification(pact: Pact, version: str) -> None: pact.with_specification(version) + assert pact.specification == PactSpecification.from_str(version) def test_server_log(pact: Pact) -> None: From 00087f7f244e97d5bb44e8e2486fd4d769e9687e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 23:08:05 +0000 Subject: [PATCH 0222/1376] chore(deps): update actions/download-artifact digest to c850b93 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9142d3949..52e64ce8b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -181,7 +181,7 @@ jobs: python-version: ${{ env.STABLE_PYTHON_VERSION }} cache: pip - - uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4 + - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4 with: path: wheelhouse @@ -202,7 +202,7 @@ jobs: id-token: write steps: - - uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4 + - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4 with: path: wheels From e9898faf71b48f08f52b52648e963bb34af3405a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 28 Feb 2024 18:31:59 +1100 Subject: [PATCH 0223/1376] chore(docs): update changelog In anticipation of generating changelogs from conventional commits, this contains a minor change to the formatting of older log entries, and rewrites the changelog for the most recent release to make usre of the new format. Signed-off-by: JP-Ellis --- CHANGELOG.md | 166 +++++++++++++++++++++++++-------------------------- 1 file changed, 81 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63e10b051..5a04e47bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,31 +1,27 @@ -### 2.1.0 - -- 82df76f - feat: bump pact standalone to 2.0.7 (JP-Ellis, Mon Sep 25 11:12:45 2023 +1000) -- 9896320 - chore: address pr comments (JP-Ellis, Wed Sep 27 10:31:56 2023 +1000) -- e86b7eb - chore(example): avoid changing python path (JP-Ellis, Fri Sep 22 13:28:26 2023 +1000) -- 045083b - chore(ci): split tests examples and lints (JP-Ellis, Thu Sep 21 12:46:44 2023 +1000) -- 3a59235 - chore(example): migrate message pact example (JP-Ellis, Wed Sep 20 13:47:26 2023 +1000) -- aa0f07e - chore(example): update readme (JP-Ellis, Wed Sep 20 13:20:06 2023 +1000) -- 9488f0e - chore(example): migrate flask provider example (JP-Ellis, Mon Sep 18 15:46:38 2023 +1000) -- dd8827a - chore(example): migrate fastapi provider example (JP-Ellis, Mon Sep 18 14:20:39 2023 +1000) -- 4c47843 - chore(example): migrate consumer example (JP-Ellis, Fri Sep 15 16:30:20 2023 +1000) -- 8ee2eb0 - feat(example): simplify docker-compose (JP-Ellis, Thu Sep 14 17:00:14 2023 +1000) -- 7c60dd5 - docs: incorporate suggestions from @YOU54F (JP-Ellis, Fri Sep 22 11:37:22 2023 +1000) -- 6d223fc - docs: add issue and pr templates (JP-Ellis, Wed Sep 20 18:04:43 2023 +1000) -- 3634a5c - docs: rewrite contributing.md (JP-Ellis, Wed Sep 20 18:00:19 2023 +1000) -- 093d9b8 - chore(ci): migrate cicd to hatch (JP-Ellis, Wed Sep 13 13:21:36 2023 +1000) -- 5b19665 - chore!: migrate to pyproject.toml and hatch (JP-Ellis, Tue Sep 12 16:13:24 2023 +1000) -- 04deeec - chore: update pre-commit config (JP-Ellis, Wed Sep 13 10:57:51 2023 +1000) -- d5017f8 - style: add pre-commit hooks and editorconfig (JP-Ellis, Thu Sep 14 11:22:58 2023 +1000) -- ed5f86c - chore: add pact-foundation triage automation (Matt Fellows, Fri Aug 4 16:37:05 2023 +1000) - -### 2.0.1 +## v2.1.0 (2023-10-04) + +### BREAKING CHANGE + +- Drop support for Python 3.6 and 3.7 + +### Feat + +- bump pact standalone to 2.0.7 +- **example**: simplify docker-compose + +### Fix + +- **ci**: pypi publish +- **github**: fix typo in template +- migrate to pyproject.toml and hatch + +## 2.0.1 - d3397b7 - chore(examples): update docker setup for non linux os (Yousaf Nabi, Tue Jul 25 14:55:42 2023 +0100) - ef12e56 - feat: update standalone to 2.0.3 (Yousaf Nabi, Tue Jul 25 14:00:38 2023 +0100) - 1429d2f - chore: update MANIFEST file to note 2.0.2 standalone (Yousaf Nabi, Tue Jul 25 13:56:08 2023 +0100) -### 2.0.0 +## 2.0.0 - 2a244ea - chore: update to 2.0.2 pact-ruby-standalone (Yousaf Nabi, Sat Jul 8 14:12:18 2023 +0100) - 819f0a7 - test: v2.0.1 (pact-2.0.1) - pact-ruby-standalone (Yousaf Nabi, Thu May 18 23:30:30 2023 +0100) @@ -61,7 +57,7 @@ - a219f49 - fix: actualize doc on how to make contributions (Serghei Iakovlev, Thu Mar 2 08:56:48 2023 +0100) - 4919772 - feat: add matchers for ISO 8601 date format (Serghei Iakovlev, Sun Mar 12 16:03:44 2023 +0100) -### 1.7.0 +## 1.7.0 - 44cda33 - chore: /s/Pactflow/PactFlow (Yousaf Nabi, Thu Jan 26 16:11:54 2023 +0000) - 1bbdd37 - feat: Enhance provider states for pact-message (#322) (nsfrias, Tue Jan 24 17:04:29 2023 +0000) @@ -69,7 +65,7 @@ - d87d54b - fix: setup security issue (#318) (Elliott Murray, Mon Nov 21 09:39:41 2022 +0000) - 55f2a64 - fix: requirements_dev.txt to reduce vulnerabilities (#317) (Matt Fellows, Sun Nov 6 02:12:30 2022 +1100) -### 1.6.0 +## 1.6.0 - ceff89b - Publish verify branches (#306) (Yousaf Nabi, Sun Sep 11 11:33:44 2022 +0100) - 89733d6 - feat: Support verify with branch (#302) (B3nnyL, Sun Sep 11 20:14:13 2022 +1000) @@ -78,22 +74,22 @@ - 2015f72 - build: Correct download logic when installing. Add a helper target to setup a pyenv via make (#297) (mikegeeves, Sun Jun 19 09:27:07 2022 +0100) - c17ac70 - docs: Update docs to reflect usage for native Python (#227) (Jiayun Fang, Wed Apr 27 10:00:50 2022 -0700) -### 1.5.2 +## 1.5.2 - 25823ae - chore: update PACT_STANDALONE_VERSION to 1.88.83 (#292) (Yousaf Nabi, Mon Mar 21 22:14:40 2022 +0000) -### 1.5.1 +## 1.5.1 - e645b24 - feat: message_pact -> with_metadata() updated to accept term (#289) (sunsathish88, Tue Mar 8 12:08:34 2022 -0500) - b981865 - docs(examples-consumer): add pip install requirements to the consumer… (#291) (mikegeeves, Sun Mar 6 10:12:32 2022 +0000) - 4c76ae8 - test(examples): move shared fixtures to a common folder so they can b… (#280) (mikegeeves, Sun Mar 6 10:10:11 2022 +0000) -### 1.5.0 +## 1.5.0 - 8085be0 - feat: No include pending (#284) (Abraham Gonzalez, Wed Feb 2 13:20:39 2022 +0100) - f169f3b - ci: python36-support-removed (#283) (mikegeeves, Sat Jan 22 10:26:44 2022 +0000) -### 1.4.6 +## 1.4.6 - 6c25844 - chore: flake8 config to ignore direnv (Elliott Murray, Mon Jan 3 18:33:47 2022 +0000) - 891134a - feat(matcher): Allow bytes type in from_term function (#281) (joshua-badger, Mon Jan 3 11:23:40 2022 -0700) @@ -101,19 +97,19 @@ - 02643d4 - test(examples-fastapi): tidy FastAPI example, making consistent with Flask (#274) (mikegeeves, Sun Oct 31 21:52:54 2021 +0000) - bf110e2 - docs: Docs/examples (#273) (Elliott Murray, Tue Oct 26 21:54:00 2021 +0100) -### 1.4.5 +## 1.4.5 - 695d51f - fix: update standalone to 1.88.77 to fix Let's Encrypt CA issue (Matt Fellows, Mon Oct 11 13:29:34 2021 +1100) -### 1.4.4 +## 1.4.4 - b90cf3d - fix(ruby): update ruby standalone to support disabling SSL verification via an environment variable (m-aciek, Sat Oct 2 03:04:14 2021 +0200) -### 1.4.3 +## 1.4.3 - 08f0dc0 - feat: added support for message provider using pact broker (#257) (Fabio Pulvirenti, Sun Sep 5 22:49:51 2021 +0200) -### 1.4.2 +## 1.4.2 - f2230b6 - chore: Bundle Ruby standalones into dist artifact. (#256) (Taj Pereira, Sun Aug 22 19:53:53 2021 +0930) - e370786 - chore: Releasing version 1.4.1 (Elliott Murray, Tue Aug 17 18:55:53 2021 +0100) @@ -123,17 +119,17 @@ - 903371b - feat: added support for message provider (#251) (Fabio Pulvirenti, Sat Jul 31 13:24:19 2021 +0200) - 2c81029 - chore(snyk): update fastapi (#239) (Elliott Murray, Fri Jun 11 09:12:38 2021 +0100) -### 1.4.1 +## 1.4.1 - 7dc8864 - fix: make uvicorn versions over 0.14 (#255) (Elliott Murray, Tue Aug 17 18:51:52 2021 +0100) -### 1.4.0 +## 1.4.0 - 0089937 - fix: issue originating from snyk with requests and urllib (#252) (Elliott Murray, Sat Jul 31 12:46:15 2021 +0100) - 903371b - feat: added support for message provider (#251) (Fabio Pulvirenti, Sat Jul 31 13:24:19 2021 +0200) - 2c81029 - chore(snyk): update fastapi (#239) (Elliott Murray, Fri Jun 11 09:12:38 2021 +0100) -### 1.3.9 +## 1.3.9 - 98d9a4b - chore(ruby): update ruby standalen (#233) (Elliott Murray, Thu May 13 20:21:10 2021 +0100) - 657e770 - fix: change default from empty string to empty list (#235) (Vasile Tofan, Thu May 13 22:20:47 2021 +0300) @@ -141,12 +137,12 @@ - 3c909f1 - docs: example uses date matcher (#231) (Elliott Murray, Sat May 1 11:51:28 2021 +0100) - 6390144 - fix: fix datetime serialization issues in Format (#230) (Syed Muhammad Dawoud Sheraz Ali, Thu Apr 29 01:49:53 2021 +0500) -### 1.3.8 +## 1.3.8 - 3c909f1 - docs: example uses date matcher (#231) (Elliott Murray, Sat May 1 11:51:28 2021 +0100) - 6390144 - fix: fix datetime serialization issues in Format (#230) (Syed Muhammad Dawoud Sheraz Ali, Thu Apr 29 01:49:53 2021 +0500) -### 1.3.7 +## 1.3.7 - 20f828f - fix(broker): token added to verify steps (#226) (Elliott Murray, Sat Apr 24 13:47:22 2021 +0100) - c4fe422 - chore: Releasing version 1.3.6 (Elliott Murray, Tue Apr 20 20:58:50 2021 +0100) @@ -160,7 +156,7 @@ - f293dbb - Merge pull request #215 from pact-foundation/snyk-fix-ab489d8931bdf95d6ef0d217aa1b2eb6 (Elliott Murray, Wed Mar 31 09:27:02 2021 +0100) - 5946872 - fix: docker/py36.Dockerfile to reduce vulnerabilities (snyk-bot, Tue Mar 30 21:41:35 2021 +0000) -### 1.3.6 +## 1.3.6 - 34160a8 - fix: publish verification results was wrong (#222) (Elliott Murray, Tue Apr 20 20:58:20 2021 +0100) - 2c0252c - Merge pull request #219 from pact-foundation/ci/revert_snyk_36 (Elliott Murray, Sat Apr 3 11:13:49 2021 +0100) @@ -245,17 +241,17 @@ - 8f9a925 - Merge pull request #195 from cdambo/pass-pact-dir-to-cli (Elliott Murray, Sat Jan 23 10:39:53 2021 +0000) - 545fc37 - fix: send to cli pact_files with the pact_dir in their path (Chanan Damboritz, Tue Jan 19 18:51:17 2021 +0200) -### 1.3.5 +## 1.3.5 - 5864e47 - Merge pull request #213 from pact-foundation/fix/revert_some_publish (Elliott Murray, Sun Mar 28 15:27:50 2021 +0100) - 94e597a - fix(publish): fixing the fix. Pact Python api uses only publish_version and ensures it follows that (Elliott Murray, Sun Mar 28 15:20:04 2021 +0100) -### 1.3.4 +## 1.3.4 - c778c71 - Merge pull request #212 from pact-foundation/fix/verify_in_provider (Elliott Murray, Sat Mar 27 18:59:25 2021 +0000) - ea0b64a - fix: verifier should now publish (Elliott Murray, Sat Mar 27 16:21:25 2021 +0000) -### 1.3.3 +## 1.3.3 - 5e282ff - Merge pull request #211 from anneschuth/fix/pass-pact-dir (Elliott Murray, Thu Mar 25 21:21:56 2021 +0000) - 987c4fc - fix: pass pact_dir to publish() (Anne Schuth, Thu Mar 25 10:06:54 2021 +0100) @@ -276,12 +272,12 @@ - 5a0934c - chore: update ci stuff (Elliott Murray, Sat Feb 27 09:57:31 2021 +0000) - 66e79e2 - chore: move from nose to pytests as we are now 3.6+ (Elliott Murray, Sat Feb 27 09:34:37 2021 +0000) -### 1.3.1 +## 1.3.1 - 4440022 - Merge pull request #203 from pact-foundation/fix/version_confusion (Elliott Murray, Sat Feb 27 09:15:08 2021 +0000) - 9cac2d7 - fix: introduced and renamed specification version (Elliott Murray, Tue Feb 23 21:22:36 2021 +0000) -### 1.3.0 +## 1.3.0 - eaa90e1 - Merge pull request #194 from williaminfante/feat/pact-message-2 (Elliott Murray, Mon Jan 25 08:48:00 2021 +0000) - 5ed73db - test: consider publish to broker with no pact_dir argument (William Infante, Mon Jan 25 17:19:08 2021 +1100) @@ -330,7 +326,7 @@ - 8f9a925 - Merge pull request #195 from cdambo/pass-pact-dir-to-cli (Elliott Murray, Sat Jan 23 10:39:53 2021 +0000) - 545fc37 - fix: send to cli pact_files with the pact_dir in their path (Chanan Damboritz, Tue Jan 19 18:51:17 2021 +0200) -### 1.2.11 +## 1.2.11 - ba10318 - Merge pull request #192 from pact-foundation/fix/deploy_wheel_fix (Elliott Murray, Tue Dec 29 20:05:52 2020 +0000) - 289e784 - fix: not creating wheel (Elliott Murray, Tue Dec 29 20:00:19 2020 +0000) @@ -351,7 +347,7 @@ - 37e2f3a - chore: wqshell script to run flask in exmaples (Elliott Murray, Sun Nov 1 11:41:59 2020 +0000) - b5d9d7b - chore(upgrade): upgrade python version to 3.8 (Elliott Murray, Sun Nov 1 11:12:10 2020 +0000) -### 1.2.10 +## 1.2.10 - 9438449 - Merge pull request #191 from pact-foundation/build-and-test-with-github-actions (Elliott Murray, Sat Dec 19 12:38:23 2020 +0000) - 2796ef5 - docs: Added badge to README (Elliott Murray, Sat Dec 19 09:37:22 2020 +0000) @@ -369,7 +365,7 @@ - 37e2f3a - chore: wqshell script to run flask in exmaples (Elliott Murray, Sun Nov 1 11:41:59 2020 +0000) - b5d9d7b - chore(upgrade): upgrade python version to 3.8 (Elliott Murray, Sun Nov 1 11:12:10 2020 +0000) -### 1.2.9 +## 1.2.9 - 4430681 - Merge pull request #183 from thatguysimon/feat/verifier-class-consumer-version-selectors (Elliott Murray, Mon Oct 19 15:35:47 2020 +0100) - 683a931 - fix: Fix flaky tests using OrderedDict (Simon Nizov, Mon Oct 19 17:21:21 2020 +0300) @@ -377,7 +373,7 @@ - e7c87ce - style: Fix linting issues (Simon Nizov, Mon Oct 19 11:16:59 2020 +0300) - ee2eda0 - feat(verifier): Allow setting consumer_version_selectors on Verifier (Simon Nizov, Mon Oct 19 11:01:18 2020 +0300) -### 1.2.8 +## 1.2.8 - 4c68fd4 - Merge pull request #182 from thatguysimon/feat/enable-wip-pacts (Elliott Murray, Sat Oct 17 16:00:50 2020 +0100) - 9ea14d3 - refactor: Extract input validation in call_verify out into a dedicated method (Simon Nizov, Sat Oct 17 17:27:49 2020 +0300) @@ -389,19 +385,19 @@ - 186f4f4 - Merge pull request #179 from pact-foundation/docs/example_readme (Elliott Murray, Thu Oct 15 10:13:13 2020 +0100) - 2f66618 - docs(examples): tweak to readme (Elliott Murray, Thu Oct 15 10:08:52 2020 +0100) -### 1.2.7 +## 1.2.7 - 90b71d2 - Merge pull request #178 from pact-foundation/fix/custom_header_typo (Elliott Murray, Fri Oct 9 12:47:37 2020 +0100) - b07ef69 - fix(verifier): headers not propogated properly (Elliott Murray, Fri Oct 9 12:24:25 2020 +0100) - 0e9b71c - Merge pull request #177 from pact-foundation/docs/remove_handcrafted_broker (Elliott Murray, Fri Oct 9 12:01:24 2020 +0100) - 2db7008 - docs(examples): removed manaul publish to broker (Elliott Murray, Fri Oct 9 11:54:30 2020 +0100) -### 1.2.6 +## 1.2.6 - 1192bd6 - Merge pull request #173 from copalco/master (Elliott Murray, Thu Sep 10 15:30:07 2020 +0100) - 5db7100 - feat(verifier): allow to use unauthenticated brokers (Piotr Kopalko, Thu Sep 10 14:12:12 2020 +0200) -### 1.2.5 +## 1.2.5 - 46372c7 - Merge pull request #171 from m-aciek/enable-pending (Elliott Murray, Wed Sep 9 10:03:02 2020 +0100) - e840587 - fix(verifier): remove superfluous verbose mentions (Maciej Olko, Sat Sep 5 21:33:52 2020 +0200) @@ -413,14 +409,14 @@ - fc6c365 - fix(verifier): remove superfluous option from verify CLI command (Maciej Olko, Thu Sep 3 13:30:57 2020 +0200) - fbbd5fa - ci(pre-commit): add commitizen to pre-commit configuration (Maciej Olko, Thu Sep 3 17:19:45 2020 +0200) -### 1.2.4 +## 1.2.4 - a594e22 - Merge pull request #170 from alecgerona/feat/consumer-version-selector (Elliott Murray, Thu Aug 27 15:21:45 2020 +0100) - 05c5e41 - docs(cli): improve cli help grammar (Alexandre Gerona, Thu Aug 27 06:28:56 2020 +0800) - 49d5f7c - docs: update README.md with relevant option documentation (Alexandre Gerona, Thu Aug 27 06:22:37 2020 +0800) - 5a99528 - feat(cli): add consumer-version-selector option (Alexandre Gerona, Thu Aug 27 06:22:07 2020 +0800) -### 1.2.3 +## 1.2.3 - 8188d88 - chore: fix release script (Elliott Murray, Wed Aug 26 12:46:10 2020 +0100) - e0e5106 - Merge pull request #169 from pact-foundation/chore/update_pr_scripts (Elliott Murray, Wed Aug 26 10:24:47 2020 +0100) @@ -429,14 +425,14 @@ - 468e4ad - Merge pull request #168 from pact-foundation/chore/upgrade-to-pact-ruby-standalone-1-88-3 (Elliott Murray, Wed Aug 26 09:49:33 2020 +0100) - ce944fe - feat: update standalone to 1.88.3 (Elliott Murray, Wed Aug 26 09:08:27 2020 +0100) -### 1.2.2 +## 1.2.2 - 2c52053 - Merge pull request #167 from pact-foundation/feat/add_env_vars_verify (Elliott Murray, Mon Aug 24 16:08:04 2020 +0100) - ce62588 - feat: added env vars for broker verify (Elliott Murray, Mon Aug 24 16:03:44 2020 +0100) - 880fff2 - Merge pull request #165 from pact-foundation/docs/https_fix (Elliott Murray, Thu Aug 20 12:43:12 2020 +0100) - 1a3605e - docs: https svg (Elliott Murray, Thu Aug 20 12:37:01 2020 +0100) -### 1.2.1 +## 1.2.1 - 69a4a9a - Merge pull request #163 from elliottmurray/fix/custom_header (Elliott Murray, Sat Aug 8 10:17:20 2020 +0100) - 88b7d9f - fix: custom headers had a typo (Elliott Murray, Sat Aug 1 11:08:54 2020 +0100) @@ -444,7 +440,7 @@ - 9875c71 - docs: merged 2 examples (Elliott Murray, Fri Jul 24 12:00:37 2020 +0100) - 6f0d3ac - docs: Example code verifier (Elliott Murray, Fri Jul 24 11:31:17 2020 +0100) -### 1.2.0 +## 1.2.0 - 2b844c5 - Merge pull request #159 from pact-foundation/feat/fix_provider_classs (Elliott Murray, Fri Jul 24 09:47:46 2020 +0100) - 9c565bb - feat: fixing up tests and examples and code for provider class (Elliott Murray, Mon Jul 20 15:57:49 2020 +0100) @@ -454,7 +450,7 @@ - ff9894a - Merge pull request #154 from elliottmurray/style/git_message (Elliott Murray, Sat Jun 27 13:31:16 2020 +0100) - be6697f - fix: change to head from master (Elliott Murray, Sat Jun 27 13:08:08 2020 +0100) -### 1.1.0 +## 1.1.0 - 1079417 - test (Elliott Murray, Thu Jun 25 10:02:14 2020 +0100) - 7fe1ef4 - Releasing version 1.1.0 (Elliott Murray, Thu Jun 25 09:41:42 2020 +0100) @@ -467,7 +463,7 @@ - aee95ed - Merge pull request #144 from pact-foundation/chore_cleanup (Elliott Murray, Wed Jun 10 21:38:12 2020 +0100) - 9c71ea0 - chore: removed some files and moved a few things around (Elliott Murray, Wed Jun 10 21:33:37 2020 +0100) -### v1.0.1 +## v1.0.1 - 8c78ff7 - Releasing version 1.0.1 (Elliott Murray, Wed Jun 3 11:01:39 2020 +0100) - 63f0e3e - Merge pull request #142 from elliottmurray/ssl_verify (Elliott Murray, Wed Jun 3 09:50:10 2020 +0100) @@ -480,7 +476,7 @@ - 60c9f5a - Fix deploy to pypi2 (Elliott Murray, Fri May 22 13:50:41 2020 +0100) - e2c7e4e - Fix deploy to pypi (Elliott Murray, Fri May 22 13:41:27 2020 +0100) -### v1.0.0 +## v1.0.0 - 2c6e4eb - Releasing version 1.0.0 (Elliott Murray, Fri May 22 13:30:49 2020 +0100) - c68ccb7 - Merge pull request #140 from elliottmurray/python2_deprecate (Elliott Murray, Fri May 22 13:29:38 2020 +0100) @@ -498,7 +494,7 @@ - a21118c - Use raw strings to avoid deprecated escape sequence (Peter Yasi, Thu May 14 08:54:01 2020 -0400) - 715d10f - Initial implementation with example unit tests (Peter Yasi, Thu May 14 00:00:30 2020 -0400) -### 0.22.0 +## 0.22.0 - d112a4a - Merge pull request #134 from elliottmurray/multiple-custom-provider-header (Elliott Murray, Mon May 11 16:32:49 2020 +0100) - 58f8e6b - Fix some style issues (Elliott Murray, Wed Apr 29 12:35:00 2020 +0100) @@ -522,7 +518,7 @@ - 5785782 - Move tests to standard tests dir (Peter Yasi, Fri Apr 17 14:03:04 2020 -0400) - 88ea23d - docs: update RELEASING.md (Beth Skurrie, Tue Feb 18 10:46:11 2020 +1100) -### 0.21.0 +## 0.21.0 - 6352dda - feat: update to pact-ruby-standalone-1.79.0 (#127) (Beth Skurrie, Tue Feb 18 10:25:59 2020 +1100) - 758d6ea - Converting to kwargs (Elliott Murray, Sat Feb 1 16:24:49 2020 +1100) @@ -531,7 +527,7 @@ - 5dcb56c - Add broker_token parameter for authentication (mikahjc, Tue Jun 11 16:16:46 2019 -0600) - 1bdfb42 - Integrate the Ruby pact broker client to allow for automatic publishing of pacts (mikahjc, Tue Jun 11 11:13:18 2019 -0600) -### 0.20.0 +## 0.20.0 - 978d9f3 - fix typo (Jingjing Duan, Wed May 24 15:48:43 2017 -0700) - 4ede7d5 - Merge pull request #117 from dlmiddlecote/feature/expose-more-options (Matt Fellows, Fri Jan 17 10:00:56 2020 +1100) @@ -544,7 +540,7 @@ - 9a0eaa7 - Merge pull request #109 from pact-foundation/dependabot/pip/flask-1.0 (Matthew Balvanz, Mon Sep 30 21:35:20 2019 -0500) - 6f70a28 - Bump flask from 0.11.1 to 1.0 (dependabot[bot], Sat Sep 28 19:20:11 2019 +0000) -### 0.19.0 +## 0.19.0 - fed5fba - Start testing in Python 3.7 (Matthew Balvanz, Sat Sep 28 15:18:17 2019 -0500) - 19aa689 - Adjust tests to support click 2.0.0 to 7.0.0 (Matthew Balvanz, Sat Sep 28 15:04:53 2019 -0500) @@ -561,12 +557,12 @@ - a5c8146 - Update README.md (bvccaneer, Fri Aug 24 19:23:26 2018 +0200) - 4d40485 - adding documentation around #52 and fixing dead link for Matching docs (szekar1, Fri Aug 24 19:19:10 2018 +0200) -### 0.18.0 +## 0.18.0 - 4e8bb85 - Upgrade pact-ruby-standalone (Matthew Balvanz, Tue Aug 21 08:56:53 2018 -0500) - 8a44feb - chore(docs): update contact information (Matt Fellows, Thu Aug 2 17:18:43 2018 +1000) -### 0.17.0 +## 0.17.0 - cf5d5bc - Merge pull request #87 from acabelloj/custom-provider-header-support (Matthew Balvanz, Fri Jul 20 22:27:33 2018 -0500) - cc61427 - Fixes #83 The verifier always returns exit code 0 (Matthew Balvanz, Fri Jul 20 22:08:26 2018 -0500) @@ -574,17 +570,17 @@ - 273b3fd - Remove Python 3.3 testing (Matthew Balvanz, Wed Jul 4 10:36:01 2018 -0500) - 01c6763 - Add support to custom provider header (Alejandro Cabello Jiménez, Fri Jun 1 11:40:32 2018 +0200) -### 0.16.1 +## 0.16.1 - eecbb60 - Merge pull request #79 from shahha/fix-stopping-mock-service-on-windows (Matthew Balvanz, Fri Mar 16 08:45:19 2018 -0500) - 4115264 - Added windows specific code to check if mock service is stopped. (Hardik Shah, Wed Mar 7 10:44:33 2018 +1100) -### 0.16.0 +## 0.16.0 - 30af240 - Merge pull request #78 from pact-foundation/standalone-1-29-2 (Matthew Balvanz☃, Fri Mar 2 22:05:12 2018 -0600) - d428951 - Update to pact-ruby-standalone 1.29.2 (Matthew Balvanz, Fri Mar 2 21:59:08 2018 -0600) -### 0.15.0 +## 0.15.0 - eb925c3 - Merge pull request #77 from pact-foundation/standalone-1-9-1 (Matthew Balvanz☃, Fri Mar 2 21:22:35 2018 -0600) - 2a2dcd1 - Upgrade to pact-ruby-standalone 1.9.1 (Matthew Balvanz, Fri Mar 2 21:18:25 2018 -0600) @@ -593,12 +589,12 @@ - 589224a - Hide Ruby stack traces by default (Matthew Balvanz, Fri Mar 2 20:56:59 2018 -0600) - e952b37 - Reduce timeout in \_wait_for_server_start to 25s (Fabian Büchler, Fri Feb 9 11:04:01 2018 +0100) -### 0.14.0 +## 0.14.0 - 3070638 - Merge pull request #71 from pact-foundation/update-standalone-1-9-0 (Matthew Balvanz, Sat Feb 3 23:25:37 2018 -0600) - 475703c - Resolves #58: Update to pact-ruby-standalone 1.9.0 (Matthew Balvanz, Sat Feb 3 23:12:22 2018 -0600) -### 0.13.0 +## 0.13.0 - 3316743 - Merge pull request #69 from jawu/#52-helper-function-for-assertion-with-matchers (Matthew Balvanz, Sat Jan 20 16:43:56 2018 -0600) - ae7f333 - Merge pull request #70 from bethesque/issues/pact-provider-verifier-19 (Matthew Balvanz, Sat Jan 20 16:40:31 2018 -0600) @@ -606,7 +602,7 @@ - 8bedfd4 - removed local files (Janneck Wullschleger, Wed Dec 20 05:12:08 2017 +0100) - 5ab2648 - solves #52 added get_generated_values to resolve Mathers to their generated value for assertion (Janneck Wullschleger, Wed Dec 20 05:06:33 2017 +0100) -### 0.12.0 +## 0.12.0 - 149dfc7 - Merge pull request #67 from jawu/enable-possibility-to-use-mathers-in-path (Matthew Balvanz, Sun Dec 17 10:32:34 2017 -0600) - fb97d2f - fixed doc string of Request (Janneck Wullschleger, Sat Dec 16 20:44:11 2017 +0100) @@ -614,7 +610,7 @@ - 697a6a2 - fixed port parameter in e2e test for python 2.7 (Janneck Wullschleger, Thu Dec 14 15:08:05 2017 +0100) - ca2eb92 - added from_term call in Request constructor to process path property for json transport (Janneck Wullschleger, Thu Dec 14 14:45:11 2017 +0100) -### 0.11.0 +## 0.11.0 - ad69039 - Merge pull request #63 from pact-foundation/run-specific-interactions (Matthew Balvanz, Sun Dec 17 09:53:35 2017 -0600) - eb63864 - Output a rerun command when a verification fails (Matthew Balvanz, Sun Nov 19 20:44:06 2017 -0600) @@ -625,12 +621,12 @@ - c1a5402 - Merge pull request #2 from pact-foundation/master (dhoomakethu, Tue Oct 31 12:15:53 2017 +0530) - b91f6c3 - Merge pull request #1 from pact-foundation/master (dhoomakethu, Mon Aug 21 12:36:15 2017 +0530) -### 0.10.0 +## 0.10.0 - 821671e - Merge pull request #53 from pact-foundation/verify-directories (Matthew Balvanz, Sat Nov 18 23:26:05 2017 -0600) - 8291bb7 - Resolve #22: --pact-url accepts directories (Matthew Balvanz, Sat Oct 7 11:35:37 2017 -0500) -### 0.9.0 +## 0.9.0 - 735aa87 - Set new project minimum requirements (Matthew Balvanz, Sun Oct 22 16:30:12 2017 -0500) - 295f17c - Merge pull request #60 from ftobia/requirements (Matthew Balvanz, Sun Oct 22 16:09:59 2017 -0500) @@ -648,7 +644,7 @@ - b5e1f95 - allow later versions of requests (Chris Hannam, Tue Aug 29 13:38:42 2017 +0100) - 08fe123 - make setup-url name format match above reference (Chris Hannam, Fri Aug 25 11:03:35 2017 +0100) -### 0.8.0 +## 0.8.0 - edb6c72 - Merge pull request #41 from pact-foundation/fix-running-on-windows (Matthew Balvanz, Thu Aug 10 21:39:27 2017 -0500) - 244fff1 - Merge pull request #42 from pact-foundation/deprecate-provider-states-url (Matthew Balvanz, Thu Aug 10 21:38:44 2017 -0500) @@ -656,7 +652,7 @@ - 4661406 - Move to using the `service` command with pact-mock-service (Matthew Balvanz, Sat Jul 29 10:00:47 2017 -0500) - 04107db - Remove the PyPi server declaration to use the defaults (Matthew Balvanz, Sun Jul 16 09:05:30 2017 -0500) -### v0.7.0 +## v0.7.0 - 223ea76 - Merge pull request #32 from SimKev2/pacturls (Matthew Balvanz, Sun Jul 16 08:41:14 2017 -0500) - e382eb4 - Add tests for #36 SomethingLike not supporting Terms (Matthew Balvanz, Sun Jul 16 08:36:58 2017 -0500) @@ -667,12 +663,12 @@ - 65b493d - Merge pull request #33 from bethesque/reamde (Matthew Balvanz, Tue Jun 27 08:58:08 2017 -0500) - f5a5958 - Update README.md (Beth Skurrie, Sun Jun 25 10:37:03 2017 +1000) -### v0.6.2 +## v0.6.2 - 69caa40 - Merge pull request #35 from pact-foundation/fix-broker-credentials (Matt Fellows, Tue Jun 27 20:49:35 2017 +1000) - d60f37f - Fix the use of broker credentials (Matthew Balvanz, Mon Jun 26 21:14:53 2017 -0500) -### v0.6.1 +## v0.6.1 - 14968ea - Merge pull request #34 from hartror/rh_version_fix (Matthew Balvanz, Mon Jun 26 20:23:29 2017 -0500) - aca520f - pydocstyle is fussy, should have run it before pushing (Rory Hart, Sun Jun 25 20:11:26 2017 +1000) @@ -685,19 +681,19 @@ - 3198817 - Update CONTRIBUTING.md (Beth Skurrie, Thu Jun 22 08:36:57 2017 +1000) - 7a08bb2 - Update CONTRIBUTING.md (Beth Skurrie, Thu Jun 22 08:35:27 2017 +1000) -### v0.6.0 +## v0.6.0 - 10aaaf6 - Merge pull request #27 from pact-foundation/download-pre-package-mock-service-and-verifier (Matthew Balvanz, Tue Jun 20 21:51:40 2017 -0500) - a9b991b - Update to pact-ruby-standalone 1.0.0 (Matthew Balvanz, Mon Jun 19 10:17:09 2017 -0500) - ab43c8b - Switch to installing the packages from pact-ruby-standalone (Matthew Balvanz, Wed May 31 21:00:51 2017 -0500) - db3e7c3 - Use the compiled Ruby applications from pact-mock-service and pact-provider-verifier (Matthew Balvanz, Mon May 29 22:18:47 2017 -0500) -### v0.5.0 +## v0.5.0 - c085a01 - Merge pull request #26 from AnObfuscator/stub-multiple-requests (Matthew Balvanz, Mon Jun 19 09:14:51 2017 -0500) - 22c0272 - Add support for stubbing multiple requests at the same time (AnObfuscator, Fri Jun 16 23:18:01 2017 -0500) -### v0.4.1 +## v0.4.1 - 66cf151 - Add RELEASING.md closes #18 (Matthew Balvanz, Tue May 30 22:41:06 2017 -0500) - 3f61c91 - Add support for request bodies that are False in Python (Matthew Balvanz, Tue May 30 21:57:46 2017 -0500) @@ -706,7 +702,7 @@ - dd3c703 - Merge pull request #16 from jduan/master (Jose Salvatierra, Thu May 25 09:20:10 2017 +0100) - 978d9f3 - fix typo (Jingjing Duan, Wed May 24 15:48:43 2017 -0700) -### v0.4.0 +## v0.4.0 - 8bec271 - Setup Travis CI to publish to PyPi (Matthew Balvanz, Wed May 24 16:51:05 2017 -0500) - d67a015 - Merge pull request #14 from pact-foundation/verify-pacts (Matthew Balvanz, Wed May 24 16:46:49 2017 -0500) @@ -717,12 +713,12 @@ - 51eb338 - Command line application for verifying pacts (Matthew Balvanz, Fri May 19 22:24:06 2017 -0500) - 4b0bbd7 - Update the developer instructions (Matthew Balvanz, Fri May 19 22:05:54 2017 -0500) -### v0.3.0 +## v0.3.0 - 3130f9a - Merge pull request #11 from pact-foundation/update-mock-service (Matthew Balvanz, Sun May 14 09:03:43 2017 -0500) - 9b20d36 - Updated Versions of Pact Ruby applications (Matthew Balvanz, Sat May 13 09:43:44 2017 -0500) -### v0.2.0 +## v0.2.0 - 140f583 - Merge pull request #8 from pact-foundation/manage-mock-service (Matthew Balvanz, Sat May 13 09:18:40 2017 -0500) - 5994c3a - pact-python manages the mock service for the user (Matthew Balvanz, Tue May 9 21:58:08 2017 -0500) @@ -731,7 +727,7 @@ - fd68b41 - Merge pull request #2 from pact-foundation/package-ruby-apps (Matthew Balvanz, Sat Apr 22 10:55:48 2017 -0500) - 75a96dc - Package the Ruby Mock Service and Verifier (Matthew Balvanz, Tue Apr 4 23:14:11 2017 -0500) -### v0.1.0 +## v0.1.0 - 189c647 - Merge pull request #3 from pact-foundation/travis-ci (Jose Salvatierra, Fri Apr 7 21:40:02 2017 +0100) - 559efb8 - Add Travis CI build (Matthew Balvanz, Fri Apr 7 11:12:01 2017 -0500) From 960483902eab542cc667ac3d19bd6acaf0d7a4a0 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 28 Feb 2024 19:00:35 +1100 Subject: [PATCH 0224/1376] chore(ci): automate release process This PR significant changes the release process, by mostly automating it all. The docs on creating releases has been updated and moved into the `docs/` folder (anticipating that we will use MkDocs eventually). The check step in the build pipeline has been removed, as it is redundant given that cibuildwheel already does checks. Signed-off-by: JP-Ellis --- .github/CHANGELOG.md.j2 | 19 +++++++++++++ .github/workflows/build.yml | 57 ++++++++++++++++++++++++++++++++++--- RELEASING.md | 54 ----------------------------------- docs/releases.md | 49 +++++++++++++++++++++++++++++++ script/commit_message.py | 40 -------------------------- script/release_prep.sh | 30 ------------------- 6 files changed, 121 insertions(+), 128 deletions(-) create mode 100644 .github/CHANGELOG.md.j2 delete mode 100644 RELEASING.md create mode 100644 docs/releases.md delete mode 100755 script/commit_message.py delete mode 100755 script/release_prep.sh diff --git a/.github/CHANGELOG.md.j2 b/.github/CHANGELOG.md.j2 new file mode 100644 index 000000000..806a23c24 --- /dev/null +++ b/.github/CHANGELOG.md.j2 @@ -0,0 +1,19 @@ +{% for entry in tree %} + +## {{ entry.version }}{% if entry.date %} ({{ entry.date }}){% endif %} + +{% for change_key, changes in entry.changes.items() %} + +{% if change_key %} +### {{ change_key }} +{% endif %} + +{% for change in changes %} +{% if change.scope %} +- **{{ change.scope }}**: {{ change.message }} +{% elif change.message %} +- {{ change.message }} +{% endif %} +{% endfor %} +{% endfor %} +{% endfor %} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52e64ce8b..fbeff39f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ env: CIBW_BUILD_FRONTEND: build jobs: - build-sdit: + build-sdist: name: Build source distribution if: github.event_name == 'push' || ! github.event.pull_request.draft @@ -114,7 +114,7 @@ jobs: build-arm64: name: Build wheels on ${{ matrix.os }} (arm64) - # As this requires emulation, it's not worth running on PRs + # As this requires emulation, it's not worth running on PRs or master if: >- github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') @@ -189,22 +189,60 @@ jobs: pipx run twine check --strict wheelhouse/* publish: - name: Publish wheels + name: Publish wheels and sdist if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') runs-on: ubuntu-latest environment: pypi - needs: [check] + needs: + - build-sdist + - build-x86_64 + - build-arm64 permissions: # Required for trusted publishing id-token: write + # Required for release creation + contents: write steps: - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4 with: path: wheels + merge-multiple: true + + - name: Setup Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 + with: + python-version: ${{ env.STABLE_PYTHON_VERSION }} + + - name: Update changelog + id: changelog + run: | + pip install --upgrade commitizen + + cz changelog \ + --incremental \ + --template .github/CHANGELOG.md.j2 \ + --dry-run \ + | tail -n+2 \ + > ${{ runner.temp }}/changelog + echo -e "\n\n## Pull Requests\n\n" >> ${{ runner.temp }}/changelog + + cz changelog \ + --incremental \ + --template .github/CHANGELOG.md.j2 + + - name: Generate release + id: release + uses: softprops/action-gh-release@v1 + with: + files: wheels/* + body_path: ${{ runner.temp }}/changelog + draft: false + prerelease: false + generate_release_notes: true - name: Push build artifacts to PyPI uses: pypa/gh-action-pypi-publish@e53eb8b103ffcb59469888563dc324e3c8ba6f06 # v1.8.12 @@ -212,3 +250,14 @@ jobs: skip-existing: true password: ${{ secrets.PYPI_TOKEN }} packages-dir: wheels + + - name: Create PR for changelog update + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GH_TOKEN }} + commit-message: "chore: update changelog ${{ github.ref_name }}" + title: "chore: update changelog" + body: | + This PR updates the changelog for ${{ github.ref_name }}. + branch: chore/update-changelog + base: master diff --git a/RELEASING.md b/RELEASING.md deleted file mode 100644 index 5d04b574c..000000000 --- a/RELEASING.md +++ /dev/null @@ -1,54 +0,0 @@ -# Releasing - -## Preparing the release - -The easiest way is to just run the following command from the root folder with the HEAD commit on trunk and the appropriate version. We follow `..` versioning. - -```shell -script/release_prep.sh X.Y.Z -``` - -This script effectively runs the following: - -1. Increment the version according to semantic versioning rules in `pact/__version__.py` - -2. Update the `CHANGELOG.md` using: - - ```shell - git log --pretty=format:' * %h - %s (%an, %ad)' vX.Y.Z..HEAD - ``` - -3. Add files to git - - ```shell - git add CHANGELOG.md pact/__version__.py - ``` - -4. Commit - - ```shell - git commit -m "Releasing version X.Y.Z" - ``` - -5. Tag - - ```shell - git tag -a vX.Y.Z -m "Releasing version X.Y.Z" - git push origin master --tags - ``` - -## Updating Pact Ruby - -To upgrade the versions of `pact-mock_service` and `pact-provider-verifier`, change the `PACT_STANDALONE_VERSION` in `setup.py` to match the latest version available from the [pact-ruby-standalone](https://github.com/pact-foundation/pact-ruby-standalone/releases) repository. Do this before preparing the release. - -## Publishing to pypi - -1. Wait until GitHub Actions have run and the new tag is available at `https://github.com/pact-foundation/pact-python/releases/tag/vX.Y.Z` - -2. Set the title to `pact-python-X.Y.Z` - -3. Save - -4. Go to GitHub Actions for Pact Python and you should see an 'Upload Python Package' action blocked for your version. - -5. Click this and then 'Review deployments'. Select 'Upload Python Package' and Approve deploy. If you can't do this you may need an administrator to give you permissions or do it for you. You should see in Slack #pact-python that the release has happened. Verify in [pypi](https://pypi.org/project/pact-python/) diff --git a/docs/releases.md b/docs/releases.md new file mode 100644 index 000000000..de0ed18ff --- /dev/null +++ b/docs/releases.md @@ -0,0 +1,49 @@ +# Releases + +Pact Python is made available through both GitHub releases and PyPI. The GitHub releases also come with a summary of changes and contributions since the last release. + +The entire process is automated through the [build](https://github.com/pact-foundation/pact-python/actions?query=workflow%3Abuild) GitHub Action. A description of the process is provided [below](#build-pipeline). + +## Versioning + +Pact Python follows [semantic versioning](https://semver.org/). Breaking changes are indicated by a major version bump, new features by a minor version bump, and bug fixes by a patch version bump. + +There are a couple of exceptions to the [semantic versioning](https://semver.org/) rules: + +- Dropping support for a Python version is not considered a breaking change and is not necessarily accompanied by a major version bump. +- Private APIs are not considered part of the public API and are not subject to the same rules as the public API. They can be changed at any time without a major version bump. Private APIs are denoted by a leading underscore in the name. Please be aware that the distinction between public and private APIs will be made concrete from version 3 onwards, and best judgement is used in the meantime to determine what is public and what is private. +- Deprecations are not considered breaking changes and are not necessarily accompanied by a major version bump. Their removal is considered a breaking change and is accompanied by a major version bump. + +Any deviation from the the standard semantic versioning rules will be clearly documented in the release notes. + +The version is stored in `pact/__version__.py`. This file is automatically generated by [`hatch-vcs`](https://pypi.org/project/hatch-vcs/) and generates a version based on the latest version tag and the number of commits since that tag. Specifically: + +- If the latest tag is `v1.2.3` and there have been no commits since then and the repository is clean, the version will be `1.2.3`. +- Otherwise, the version will take the form of `1.2.3.dev{N}+g{hash}` (or `1.2.3.dev{N}+g{hash}.d{date}` if there's a dirty repository) where `N` is the number of commits since the latest tag, `hash` is the short hash of the latest commit. + +## Build Pipeline + +The build pipeline is defined in `.github/workflows/build.yml`. It is triggered on PRs targeting `master`, pushes to the `master` branch, and on every new tag. The pipeline is responsible for building the package (both as source distribution, and compiled wheels), creating the GitHub release, and uploading artifacts to PyPI. + +### Build Steps + +The build steps generates the source distribution and wheels. This is done using [cibuildwheel](https://cibuildwheel.readthedocs.io/) to ensure that the wheels are compatible with a wide range of Python versions and platforms. + +In order to reduce the build time, the pipeline builds different sets of wheels depending on the trigger: + +| Trigger | Platforms | Wheels | +| ------------ | ----------------- | --------- | +| Tag | `x86_64`, `arm64` | all | +| `master` | `x86_64` | all | +| Pull Request | `x86_64` | `cp312-*` | + +### Publish Step + +The publish step uses the `pypi` GitHub environment, and is gated behind a manual approval. The publish step is responsible for the following: + +- Generating a changelog based on the conventional commits since the latest release. +- Generating a new GitHub release with the changelog. +- Uploading the source distribution and wheels to PyPI. +- Creating a PR to update the `CHANGELOD.md` file with the new release notes. + +While the generated changelog should be accurate, it may require some manual adjustments on the release page and in the PR. diff --git a/script/commit_message.py b/script/commit_message.py deleted file mode 100755 index 840443536..000000000 --- a/script/commit_message.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -# ruff: noqa -import re -import subprocess -import sys - -examples = """+ 61c8ca9 fix: navbar not responsive on mobile -+ 479c48b test: prepared test cases for user authentication -+ a992020 chore: moved to semantic versioning -+ b818120 fix: button click even handler firing twice -+ c6e9a97 fix: login page css -+ dfdc715 feat(auth): added social login using twitter -""" - - -def main(): - cmd_tag = "git describe --abbrev=0" - tag = subprocess.check_output(cmd_tag, shell=True).decode("utf-8").split("\n")[0] - - cmd = f"git log --pretty=format:'%s' {tag}..HEAD" - commits = subprocess.check_output(cmd, shell=True) - commits = commits.decode("utf-8").split("\n") - for commit in commits: - pattern = r"((build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert)(\([\w\-]+\))?:\s.*)|((Merge|Fixed)(\([\w\-]+\))?\s.*)" - m = re.match(pattern, commit) - if m is None: - print(f"\nError with git message '{commit}' style") - print( - "\nPlease change commit message to the conventional format and try to" - " commit again. Examples:", - ) - - print("\n" + examples) - sys.exit(1) - - print("Commit messages valid") - - -if __name__ == "__main__": - main() diff --git a/script/release_prep.sh b/script/release_prep.sh deleted file mode 100755 index d664b125b..000000000 --- a/script/release_prep.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -VERSION=$1 - -if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]*$ ]]; then - echo "Updating version $VERSION." -else - echo "Invalid version number $VERSION" - exit 1; -fi - -TAG_NAME="v$VERSION" -LAST_TAG=`git describe --abbrev=0` - - -cat pact/__version__.py | sed "s/__version__ = .*/__version__ = '${VERSION}'/" > tmp-version -mv tmp-version pact/__version__.py - -echo "Releasing $TAG_NAME" - -LOG_ENTRIES="$(git log --pretty=format:' * %h - %s (%an, %ad)' $LAST_TAG..HEAD | grep -v 'Merge pull request')" -echo -e "${LOG_ENTRIES}\n$(cat CHANGELOG.md)" > CHANGELOG.md -echo -e "### $VERSION\n$(cat CHANGELOG.md)" > CHANGELOG.md - -echo "Appended Changelog to $VERSION" - -git add CHANGELOG.md pact/__version__.py -git commit -m "chore: Releasing version $VERSION" - -git tag -a "$TAG_NAME" -m "Releasing version $VERSION" && git push origin master --tags From 9c345f0c6c110879ff7018c5c4039d5aa135f89f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 5 Mar 2024 10:17:58 +1100 Subject: [PATCH 0225/1376] chore(v3): add warning on pact.v3 import As the `pact.v3` module is still a work in progress, importing it raises a warning. Signed-off-by: JP-Ellis --- src/pact/v3/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pact/v3/__init__.py b/src/pact/v3/__init__.py index 137cba447..20701bd7f 100644 --- a/src/pact/v3/__init__.py +++ b/src/pact/v3/__init__.py @@ -20,8 +20,17 @@ considered deprecated, and will be removed in a future release. """ +import warnings + from pact.v3.pact import Pact __all__ = [ "Pact", ] + +warnings.warn( + "The `pact.v3` module is not yet stable. Use at your own risk, and expect " + "breaking changes in future releases.", + stacklevel=2, + category=ImportWarning, +) From 8895276ad6574366542c9729d21cbf97c315a0b6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 23:31:32 +0000 Subject: [PATCH 0226/1376] chore(deps): pin dependencies --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fbeff39f4..d765f4fe2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -236,7 +236,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 with: files: wheels/* body_path: ${{ runner.temp }}/changelog @@ -252,7 +252,7 @@ jobs: packages-dir: wheels - name: Create PR for changelog update - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc # v6 with: token: ${{ secrets.GH_TOKEN }} commit-message: "chore: update changelog ${{ github.ref_name }}" From 604360334199eb59521c882063e34de2063a9638 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 5 Mar 2024 14:56:32 +1100 Subject: [PATCH 0227/1376] feat: add support for musllinux_aarch64 Signed-off-by: JP-Ellis --- hatch_build.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hatch_build.py b/hatch_build.py index 3baec67cb..5e6489add 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -283,6 +283,8 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 os = "linux" if platform.endswith("x86_64"): machine = "x86_64-musl" + elif platform.endswith("aarch64"): + machine = "aarch64-musl" else: raise UnsupportedPlatformError(platform) return PACT_LIB_URL.format( From 09d339f062c61f476b61ee5abe5f9582fe5ad0f7 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 5 Mar 2024 13:32:49 +1100 Subject: [PATCH 0228/1376] chore(ci): remove check of wheels The cibuildwheel process does a check that the wheels are valid, and also performs an installation of the wheel and a basic check that the import is working fine. It seems redundant to be doing a separate check. This was intended in commit 9604839, but a rebase inadvertently re-intruced the check. Ref: 02eab542cc667ac3d19bd6acaf0d7a4a0 Ref: #567 Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d765f4fe2..c1ff87590 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -163,31 +163,6 @@ jobs: if-no-files-found: error compression-level: 0 - check: - name: Check wheels - - runs-on: ubuntu-latest - - needs: - - build-x86_64 - - build-arm64 - - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - - name: Setup Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 - with: - python-version: ${{ env.STABLE_PYTHON_VERSION }} - cache: pip - - - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4 - with: - path: wheelhouse - - - run: | - pipx run twine check --strict wheelhouse/* - publish: name: Publish wheels and sdist From 36cd85c45918f0cd5d780d1b6a2c64974512ee47 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 5 Mar 2024 15:52:20 +1100 Subject: [PATCH 0229/1376] chore(ci): speed up build pipeline The building of the `aarch64` wheels takes a _very_ long time. Until such time that GitHub provides ARM Linux runners, we unfortunately have to resort to emulation. This is made worse specifically for Linux due the doubling of the number of targets from having both `manylinux` and `musllinux` wheels. This commit simply parallelises the `musllinux` and `manylinux` builds. Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1ff87590..a809edc4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -125,8 +125,13 @@ jobs: include: - os: ubuntu-latest archs: aarch64 + build: manylinux + - os: ubuntu-latest + archs: aarch64 + build: musllinx - os: macos-latest archs: arm64 + build: "" steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 @@ -154,11 +159,12 @@ jobs: uses: pypa/cibuildwheel@ce3fb7832089eb3e723a0a99cab7f3eaccf074fd # v2.16.5 env: CIBW_ARCHS: ${{ matrix.archs }} + CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} - name: Upload wheels uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4 with: - name: wheels-${{ matrix.os }}-${{ matrix.archs }} + name: wheels-${{ matrix.os }}-${{ matrix.archs }}-${{ matrix.build }} path: ./wheelhouse/*.whl if-no-files-found: error compression-level: 0 From 37d96de468f24bfcde0278aed46ccf92fb0bc881 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 5 Mar 2024 19:59:40 +1100 Subject: [PATCH 0230/1376] chore(ci): another build pipeline fix For some reason, the rebase done as part of #567 really did not go well, and some entire steps were inadvertently replaced. It is my bad, and here's another (and hopefully final) fix. Ref: #567 Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a809edc4f..633924a0c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -188,7 +188,14 @@ jobs: contents: write steps: - - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4 + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + with: + # Fetch all tags + fetch-depth: 0 + + - name: Download wheels and sdist + uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4 with: path: wheels merge-multiple: true From 9fe10c200efcddde00ef8ae1c17e69d339145e9c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 5 Mar 2024 21:39:42 +1100 Subject: [PATCH 0231/1376] chore(ci): typo (yes..., I am facepalming at my typo) Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 633924a0c..3a0ce8087 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -128,7 +128,7 @@ jobs: build: manylinux - os: ubuntu-latest archs: aarch64 - build: musllinx + build: musllinux - os: macos-latest archs: arm64 build: "" From 4614d972471a910622f39da9c42eb0d237e4db43 Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Tue, 5 Mar 2024 14:49:18 +0000 Subject: [PATCH 0232/1376] chore: update changelog v2.1.2 Signed-off-by: JP-Ellis --- CHANGELOG.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a04e47bb..08a6582c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,50 @@ +## v2.1.2 (2024-03-05) + +### BREAKING CHANGE + +- The public functions within the constants module have been removed. If you previously used them, please make use of the constants. For example, instead of `pact.constants.broker_client_exe()` use `pact.constants.BROKER_CLIENT_PATH` instead. +- It is possible to use the system installed Pact executables by setting `PACT_USE_SYSTEM_BINS` to `True` or `Yes` (case insensitive). + +### Feat + +- add support for musllinux_aarch64 +- **v3**: add specification attribute to pacts +- **v3**: upgrade ffi to 0.4.18 +- determine version from vcs +- **v3**: add with_matching_rules +- add python 3.12 support +- **v3**: implement server log methods +- **v3**: add mock server mismatches +- **v3**: implement Pact Handle methods +- **ffi**: add OwnedString class +- **v3**: implement interaction methods +- **v3**: implement pact class +- **v3**: add v3.ffi module + +### Fix + +- clean pact interactions on exception +- **v3**: incorrect arg order +- **v3**: rename `with_binary_file` +- **example**: publish message pact +- **example**: publish_verification_results typo +- **example**: unknown action +- **v3**: add `__next__` implementation +- **deps**: add yarl dependency +- **v3**: unconventional `__repr__` implementation +- **build**: include omitted `lib` dir +- **test**: ignore internal deprecation warnings +- **ci**: add missing environment + +### Refactor + +- **v3**: split interactions into modules +- refactor constants + +## v2.1.1 (2023-10-04) + +Identical to 2.1.0, but with a fix to the publication process to PyPI. + ## v2.1.0 (2023-10-04) ### BREAKING CHANGE From 9d2f91a72797f4b2a0a6b58a6e2c38c941793796 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 5 Mar 2024 14:01:41 +0000 Subject: [PATCH 0233/1376] chore(deps): update pactfoundation/pact-broker:latest docker digest to 8f10947 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8c1f81a1f..9c99e3a9a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:2c5277c6c3b2e0903868546eeff4f95b6b40ccafb61ece8375fd93de0c37c743 + image: pactfoundation/pact-broker:latest@sha256:8f10947f230f661ef21f270a4abcf53214ba27cd68063db81de555fcd93e07dd ports: - "9292:9292" env: From 57c52006f003113f50b8ca2baa8bfcbed65b656c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 6 Mar 2024 11:07:07 +1100 Subject: [PATCH 0234/1376] docs: fix repository link typo Signed-off-by: JP-Ellis --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1333cd9e2..bf1db0fb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ dependencies = [ [project.urls] "Homepage" = "https://pact.io" -"Repository" = "https://github.com/pact-foundation-pact-python" +"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" From e8dc1d444ccbc3d9908ca4a0de781d23e3cdabe3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 6 Mar 2024 13:32:13 +1100 Subject: [PATCH 0235/1376] fix: avoid wheel bloat In the release of `2.1.2`, it was discovered that [cibuildwheel](https://cibuildwheel.readthedocs.io/) does not clean the repository between each build. This resulted in each wheel containing the binary artifacts of all previous wheels; resulting in in unnecessarily bloated wheels. Signed-off-by: JP-Ellis --- hatch_build.py | 5 +++++ pyproject.toml | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/hatch_build.py b/hatch_build.py index 5e6489add..b135d3809 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -75,6 +75,11 @@ def clean(self, versions: list[str]) -> None: # noqa: ARG002 for subdir in ["bin", "lib", "data"]: shutil.rmtree(PACT_ROOT_DIR / subdir, ignore_errors=True) + for ffi in (PACT_ROOT_DIR / "v3").glob("_ffi.*"): + if ffi.name == "_ffi.pyi": + continue + ffi.unlink() + def initialize( self, version: str, # noqa: ARG002 diff --git a/pyproject.toml b/pyproject.toml index bf1db0fb3..c47fa2d28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,6 +293,10 @@ exclude = '^(src/pact|tests)/(?!v3).+\.py$' ## CI Build Wheel ################################################################################ [tool.cibuildwheel] +before-build = """ +python -m pip install hatch +hatch clean +""" test-command = """ python -c \ "from pact import EachLike; \ From 3a8a0c1945d84130bcbe0dc7b1903ca95cedd585 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 08:05:43 +0000 Subject: [PATCH 0236/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.17.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ccffae2ba..984d02805 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.16.0 + rev: v3.17.0 hooks: - id: commitizen stages: [commit-msg] From 6f6b0e28f0af2c150f3e941db6483b26ed74027c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 08:05:39 +0000 Subject: [PATCH 0237/1376] chore(deps): update ubuntu:22.04 docker digest to 77906da --- Dockerfile.ubuntu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index e2f9580b7..6c89305ab 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ubuntu:22.04@sha256:f9d633ff6640178c2d0525017174a688e2c1aef28f0a0130b26bd5554491f0da +FROM ubuntu:22.04@sha256:77906da86b60585ce12215807090eb327e7386c8fafb5402369e421f44eff17e ENV DEBIAN_FRONTEND=noninteractive ARG PYTHON_VERSION 3.9 From eb8e91d5957cc485b616206b7ebad9096fbe503e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Mar 2024 08:22:59 +1100 Subject: [PATCH 0238/1376] docs: fix links to `CONTRIBUTING.md` A few of the `CONTRIBUTING.md` link were invalid, either due to referring to the `main` branch instead of `master`, or missing the `/blob/` segment in the path. Resolves: #592 Signed-off-by: JP-Ellis --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- .github/ISSUE_TEMPLATE/feature.yml | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index be7292c2a..7b48a6274 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -27,7 +27,7 @@ body: attributes: label: Have you read the Contributing Guidelines on issues? options: - - label: I have read the [Contributing Guidelines on issues](https://github.com/pact-foundation/pact-python/main/CONTRIBUTING.md#issues). + - label: I have read the [Contributing Guidelines on issues](https://github.com/pact-foundation/pact-python/blob/master/CONTRIBUTING.md#issues). required: true - type: checkboxes diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index abcba7276..2cfa45f03 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -15,7 +15,7 @@ body: attributes: label: Have you read the Contributing Guidelines on issues? options: - - label: I have read the [Contributing Guidelines on issues](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md#issues). + - label: I have read the [Contributing Guidelines on issues](https://github.com/pact-foundation/pact-python/blob/master/CONTRIBUTING.md#issues). required: true - type: textarea diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f43b17a19..7c92dd2e7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,13 @@ ## :airplane: Pre-flight checklist -- [ ] I have read the [Contributing Guidelines on pull requests](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md#pull-requests). +- [ ] I have read the [Contributing Guidelines on pull requests](https://github.com/pact-foundation/pact-python/blob/master/CONTRIBUTING.md#pull-requests). - [ ] **If this is a code change**: I have written unit tests and/or added dogfooding pages to fully verify the new behavior. - [ ] **If this is a new API or substantial change**: the PR has an accompanying issue (closes #0000) and the maintainers have approved on my working plan. From a97a0acee6c32330a906b9af4451902d8f717324 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Mar 2024 08:43:18 +1100 Subject: [PATCH 0239/1376] chore(ci): fix pypy before-build Rust needs to be manually installed within the pypy build image; however, with the addition of `before-build`, it would appear that the existing `before-build` no longer works. The installation of the Rust toolchain has been moved to the `before-all` step. Signed-off-by: JP-Ellis --- pyproject.toml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c47fa2d28..112ee336c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -296,6 +296,13 @@ exclude = '^(src/pact|tests)/(?!v3).+\.py$' before-build = """ python -m pip install hatch hatch clean +if ! command -v cargo &> /dev/null; then + curl -sSf https://sh.rustup.rs | sh -s -- --profile=minimal -y + for bin in $(ls "$HOME/.cargo/bin"); do + ln -v "$HOME/.cargo/bin/$bin" "/usr/bin/$bin" + done + rustup show +fi """ test-command = """ python -c \ @@ -310,14 +317,3 @@ assert isinstance(pact.v3.ffi.version(), str);\"""" # The repair tool unfortunately did not like the bundled Ruby distributable. # TODO: Check whether delocate-wheel can be configured. repair-wheel-command = "" - -[[tool.cibuildwheel.overrides]] -# Pydantic for pypy needs to be built from source, which requires Rust. -select = "pp*-*linux*" -before-test = """ -curl -sSf https://sh.rustup.rs -sSf | sh -s -- --profile=minimal -y -for bin in $(ls "$HOME/.cargo/bin"); do - ln -v "$HOME/.cargo/bin/$bin" "/usr/bin/$bin" -done -rustup show -""" From aac189797e5397a8ef64a7f11fa520e7f42b19a6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Mar 2024 10:11:39 +1100 Subject: [PATCH 0240/1376] chore(ci): pin os to older versions To ensure that the wheels generated are compatible with as many devices as possible, I am pinning the operating systems of the runners to the oldest version currently supported. Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a0ce8087..866fed002 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: name: Build source distribution if: github.event_name == 'push' || ! github.event.pull_request.draft - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 @@ -62,11 +62,11 @@ jobs: fail-fast: false matrix: include: - - os: ubuntu-latest + - os: ubuntu-20.04 archs: x86_64 - - os: macos-latest + - os: macos-12 archs: x86_64 - - os: windows-latest + - os: windows-2019 archs: AMD64 steps: @@ -123,13 +123,13 @@ jobs: fail-fast: false matrix: include: - - os: ubuntu-latest + - os: ubuntu-20.04 archs: aarch64 build: manylinux - - os: ubuntu-latest + - os: ubuntu-20.04 archs: aarch64 build: musllinux - - os: macos-latest + - os: macos-12 archs: arm64 build: "" @@ -150,7 +150,7 @@ jobs: ${{ github.workflow }} - name: Set up QEMU - if: matrix.os == 'ubuntu-latest' + if: startsWith(matrix.os, 'ubuntu-') uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3 with: platforms: arm64 @@ -173,7 +173,7 @@ jobs: name: Publish wheels and sdist if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 environment: pypi needs: From 4bc898daa97793dbc5ddaee9a3bce53ea7b4d361 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Mar 2024 10:13:34 +1100 Subject: [PATCH 0241/1376] chore(ci): set osx deployment target To avoid ambiguity, setting the macOS deployment target explicitly. Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 866fed002..9c78ac9cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -97,6 +97,11 @@ jobs: echo "build=*" >> "$GITHUB_OUTPUT" fi + - name: Set macOS deployment target + if: startsWith(matrix.os, 'macos-') + run: | + echo "MACOSX_DEPLOYMENT_TARGET=10.12" >> "$GITHUB_ENV" + - name: Create wheels uses: pypa/cibuildwheel@ce3fb7832089eb3e723a0a99cab7f3eaccf074fd # v2.16.5 env: From 4de36d3105304ac687f487178c87281f0f3313eb Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Mar 2024 10:31:49 +1100 Subject: [PATCH 0242/1376] chore(ci): replace hatch clean with rm Install hatch on the PyPy images seems unnecessarily difficult, just to run a few `rm` commands. So I am replacing the before-build step with the required `rm` commands. Signed-off-by: JP-Ellis --- pyproject.toml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 112ee336c..984aca2f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,16 +293,14 @@ exclude = '^(src/pact|tests)/(?!v3).+\.py$' ## CI Build Wheel ################################################################################ [tool.cibuildwheel] +skip = "pp38-*" before-build = """ -python -m pip install hatch -hatch clean -if ! command -v cargo &> /dev/null; then - curl -sSf https://sh.rustup.rs | sh -s -- --profile=minimal -y - for bin in $(ls "$HOME/.cargo/bin"); do - ln -v "$HOME/.cargo/bin/$bin" "/usr/bin/$bin" - done - rustup show -fi +rm -rvf src/pact/v3/bin +rm -rvf src/pact/v3/data +rm -rvf src/pact/v3/lib +mv -v src/pact/v3/_ffi.pyi _ffi.pyi +rm -rvf src/pact/v3/_ffi.* +mv -v _ffi.pyi src/pact/v3/_ffi.pyi """ test-command = """ python -c \ From 845262740695e8690540f74f5b70f03259929893 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 23:15:46 +0000 Subject: [PATCH 0243/1376] chore(deps): update dependency dev/ruff to v0.3.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 984aca2f3..3f6eca4fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ test = [ "pytest-cov ~= 4.0", "testcontainers ~= 3.0", ] -dev = ["pact-python[types]", "pact-python[test]", "ruff==0.3.0"] +dev = ["pact-python[types]", "pact-python[test]", "ruff==0.3.1"] ################################################################################ ## Hatch Build Configuration From 3cc8e12b9762905d28ffa991fd82d9adbd4706b7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 23:15:49 +0000 Subject: [PATCH 0244/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.3.1 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 984d02805..030114e1e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.0 + rev: v0.3.1 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From c37e839f1731d55c585ae47c70b26c555dfc60c3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Mar 2024 11:16:10 +1100 Subject: [PATCH 0245/1376] chore(ci): update concurrency group Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c78ac9cc..e58ea187c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: - master concurrency: - group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }} + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} cancel-in-progress: true env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c99e3a9a..0695b7ff2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ on: - master concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} cancel-in-progress: true env: From 7a19532660c489a2a6d8c89ed842ae3e031e4b1f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Mar 2024 11:50:43 +1100 Subject: [PATCH 0246/1376] chore(ci): adapt before-build for windows The `rm` and `mv` commands don't work well within Windows' default shell. As a result, a separate script for Windows is required. Signed-off-by: JP-Ellis --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3f6eca4fb..0277a750c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -315,3 +315,11 @@ assert isinstance(pact.v3.ffi.version(), str);\"""" # The repair tool unfortunately did not like the bundled Ruby distributable. # TODO: Check whether delocate-wheel can be configured. repair-wheel-command = "" + +[tool.cibuildwheel.windows] +before-build = [ + 'FOR /R src\pact\v3 %G IN (_ffi.*) DO IF NOT %~nxG == _ffi.pyi DEL /F /Q "%G"', + 'IF EXIST src\pact\v3\bin\ RMDIR /S /Q src\pact\v3\bin', + 'IF EXIST src\pact\v3\data\ RMDIR /S /Q src\pact\v3\data', + 'IF EXIST src\pact\v3\lib\ RMDIR /S /Q src\pact\v3\lib', +] From 7d686f21f38d1f3a1c784aca7fd4c84a25aaaf14 Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Thu, 7 Mar 2024 04:11:07 +0000 Subject: [PATCH 0247/1376] chore: update changelog v2.1.3 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a6582c4..9c316a168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v2.1.3 (2024-03-07) + +### Fix + +- avoid wheel bloat + ## v2.1.2 (2024-03-05) ### BREAKING CHANGE From beb71df65b8f530ee66f54838728f878e8900517 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Mar 2024 17:33:58 +0000 Subject: [PATCH 0248/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.18.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 030114e1e..d4a3b2549 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.17.0 + rev: v3.18.0 hooks: - id: commitizen stages: [commit-msg] From 36fef618ff2f8195da56cdcb2b65c001e672aa92 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 01:57:38 +0000 Subject: [PATCH 0249/1376] chore(deps): update pypa/gh-action-pypi-publish action to v1.8.14 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e58ea187c..34b298139 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -238,7 +238,7 @@ jobs: generate_release_notes: true - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@e53eb8b103ffcb59469888563dc324e3c8ba6f06 # v1.8.12 + uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14 with: skip-existing: true password: ${{ secrets.PYPI_TOKEN }} From 769f24b5eb96d1334dc599db33f24c6270cd652e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 16:23:48 +0000 Subject: [PATCH 0250/1376] chore(deps): update dependency types/mypy to v1.9.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0277a750c..dcecbd756 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ pact-verifier = "pact.cli.verify:main" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. types = [ - "mypy ==1.8.0", + "mypy ==1.9.0", "types-cffi ~= 1.0", "types-requests ~= 2.0", ] From fcf133659096e535b33bcd3d70957c499c68eff3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 Mar 2024 01:07:15 +0000 Subject: [PATCH 0251/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.3.2 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d4a3b2549..7bd58bfcb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.1 + rev: v0.3.2 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From f8eb5281df8cbc0a5fe4cbe562ccf80d15a48b76 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 Mar 2024 01:07:11 +0000 Subject: [PATCH 0252/1376] chore(deps): update dependency dev/ruff to v0.3.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dcecbd756..67b0cd5ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ test = [ "pytest-cov ~= 4.0", "testcontainers ~= 3.0", ] -dev = ["pact-python[types]", "pact-python[test]", "ruff==0.3.1"] +dev = ["pact-python[types]", "pact-python[test]", "ruff==0.3.2"] ################################################################################ ## Hatch Build Configuration From ada6db73418aaf9437096d5ab7cbca49869143d0 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 11 Mar 2024 18:15:15 +1100 Subject: [PATCH 0253/1376] fix: delay pytest 8.1 Unfortunately, pytest-bdd is not yet compatible with some of the internal changes introduced in PyTest 8.1. Signed-off-by: JP-Ellis --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 67b0cd5ff..f00c782c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,8 @@ test = [ "flask[async] ~= 3.0", "httpx ~= 0.0", "mock ~= 5.0", - "pytest ~=8.0", + # pytest 8.1.0 is not compatible with pytest-bdd yet + "pytest ~= 8.0.0", "pytest-asyncio ~= 0.0", "pytest-bdd ~= 7.0", "pytest-cov ~= 4.0", From 87f99f59cce2217c7e7d038ed77f1134f9642b67 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 11 Mar 2024 16:22:49 +1100 Subject: [PATCH 0254/1376] chore(ci): remove cirrus Cirrus is always out of credit, so I am removing it. GitHub is slowly adding support for ARM architecture. Signed-off-by: JP-Ellis --- .cirrus.yml | 55 ----------------------------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 .cirrus.yml diff --git a/.cirrus.yml b/.cirrus.yml deleted file mode 100644 index dbd648868..000000000 --- a/.cirrus.yml +++ /dev/null @@ -1,55 +0,0 @@ -TEST_TEMPLATE: &TEST_TEMPLATE - skip: $CIRRUS_PR_DRAFT == "true" - arch_check_script: - - uname -am - test_script: - - python --version - # TODO: Fix lints before enabling - - echo hatch run lint - - echo hatch run typecheck - - echo hatch run format - - hatch run test - -linux_arm64_task: - env: - PATH: ${HOME}/.local/bin:${PATH} - CIRRUS_CLONE_SUBMODULES: "true" - HATCH_VERBOSE: 1 - matrix: - - IMAGE: "python:3.8-slim" - - IMAGE: "python:3.9-slim" - - IMAGE: "python:3.10-slim" - - IMAGE: "python:3.11-slim" - - IMAGE: "python:3.12-slim" - arm_container: - image: $IMAGE - install_script: - - apt update --yes - - apt install --yes gcc make g++ - - python -m pip install --upgrade pip pipx - - pipx install hatch - <<: *TEST_TEMPLATE - -macosx_arm64_task: - macos_instance: - image: ghcr.io/cirruslabs/macos-ventura-base:latest - env: - PATH: ${HOME}/.local/bin:${HOME}/.pyenv/shims:${PATH} - CIRRUS_CLONE_SUBMODULES: "true" - matrix: - - PYTHON: "3.8" - - PYTHON: "3.9" - - PYTHON: "3.10" - - PYTHON: "3.11" - - PYTHON: "3.12" - install_script: - - brew update - - brew install pyenv - - pyenv install ${PYTHON} - - pyenv global ${PYTHON} - - pyenv rehash - - python -m pip install --upgrade pip pipx - - pyenv rehash - - pipx install hatch - - pyenv rehash - <<: *TEST_TEMPLATE From 46fa5b33750ce282273b424220729bd7eb02485b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 07:16:51 +0000 Subject: [PATCH 0255/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.18.1 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7bd58bfcb..272198893 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.18.0 + rev: v3.18.1 hooks: - id: commitizen stages: [commit-msg] From 60596d2b2b4df516084537a73d820843af6005c2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 10 Mar 2024 08:05:06 +0000 Subject: [PATCH 0256/1376] chore(deps): update softprops/action-gh-release action to v2 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 34b298139..48e08ae36 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -229,7 +229,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 + uses: softprops/action-gh-release@d99959edae48b5ffffd7b00da66dcdb0a33a52ee # v2 with: files: wheels/* body_path: ${{ runner.temp }}/changelog From d6e3b31896d857fb643afe568932b15a83455ec8 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 1 Mar 2024 10:36:02 +1100 Subject: [PATCH 0257/1376] chore(ffi): implement verifier handle Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 88 ++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 52cfbc5e2..c6893c754 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -528,7 +528,40 @@ class SynchronousHttp: ... class SynchronousMessage: ... -class VerifierHandle: ... +class VerifierHandle: + """ + Handle to a Verifier. + + [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/verifier/handle/struct.VerifierHandle.html) + """ + + def __init__(self, ref: int) -> None: + """ + Initialise a new Verifier Handle. + + Args: + ref: + Rust library reference to the Verifier Handle. + """ + self._ref: int = ref + + def __del__(self) -> None: + """ + Destructor for the Verifier Handle. + """ + verifier_shutdown(self) + + def __str__(self) -> str: + """ + String representation of the Verifier Handle. + """ + return f"PactHandle({self._ref})" + + def __repr__(self) -> str: + """ + String representation of the Verifier Handle. + """ + return f"PactHandle({self._ref!r})" class ExpressionValueType(Enum): @@ -6328,55 +6361,20 @@ def verify(args: str) -> int: raise NotImplementedError -def verifier_new() -> VerifierHandle: - """ - Get a Handle to a newly created verifier. - - You should call `pactffi_verifier_shutdown` when done with the verifier to - free all allocated resources. - - [Rust - `pactffi_verifier_new`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_new) - - Deprecated: This function is deprecated. Use - `pactffi_verifier_new_for_application` which allows the calling - application/framework name and version to be specified. - - # Safety - - This function is safe. - - # Error Handling - - Returns NULL on error. - """ - warnings.warn( - "This function is deprecated, use verifier_new_for_application instead", - DeprecationWarning, - stacklevel=2, - ) - raise NotImplementedError - - -def verifier_new_for_application(name: str, version: str) -> VerifierHandle: +def verifier_new_for_application() -> VerifierHandle: """ Get a Handle to a newly created verifier. - You should call `pactffi_verifier_shutdown` when done with the verifier to - free all allocated resources - [Rust `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_new_for_application) - - # Safety - - This function is safe. - - # Error Handling - - Returns NULL on error. """ - raise NotImplementedError + from pact import __version__ + + result: int = lib.pactffi_verifier_new_for_application( + b"pact-python", + __version__.encode("utf-8"), + ) + return VerifierHandle(result) def verifier_shutdown(handle: VerifierHandle) -> None: @@ -6385,7 +6383,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None: [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_shutdown) """ - raise NotImplementedError + lib.pactffi_verifier_shutdown(handle._ref) def verifier_set_provider_info( # noqa: PLR0913 From 00d6fbbc9fb78d63cc814fda9677e6e71b6219db Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 5 Mar 2024 10:05:58 +1100 Subject: [PATCH 0258/1376] feat(v3): add verifier class Signed-off-by: JP-Ellis --- src/pact/v3/__init__.py | 2 + src/pact/v3/ffi.py | 552 +++++++++++++++---------- src/pact/v3/verifier.py | 881 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1229 insertions(+), 206 deletions(-) create mode 100644 src/pact/v3/verifier.py diff --git a/src/pact/v3/__init__.py b/src/pact/v3/__init__.py index 20701bd7f..826a0d790 100644 --- a/src/pact/v3/__init__.py +++ b/src/pact/v3/__init__.py @@ -23,9 +23,11 @@ import warnings from pact.v3.pact import Pact +from pact.v3.verifier import Verifier __all__ = [ "Pact", + "Verifier", ] warnings.warn( diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index c6893c754..e809e173d 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -83,6 +83,7 @@ import gc import json +import logging import typing import warnings from enum import Enum @@ -91,11 +92,15 @@ from pact.v3._ffi import ffi, lib # type: ignore[import] if TYPE_CHECKING: + import datetime + from collections.abc import Collection from pathlib import Path import cffi from typing_extensions import Self +logger = logging.getLogger(__name__) + # The follow types are classes defined in the Rust code. Ultimately, a Python # alternative should be implemented, but for now, the follow lines only serve # to inform the type checker of the existence of these types. @@ -535,7 +540,7 @@ class VerifierHandle: [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/verifier/handle/struct.VerifierHandle.html) """ - def __init__(self, ref: int) -> None: + def __init__(self, ref: cffi.FFI.CData) -> None: """ Initialise a new Verifier Handle. @@ -543,7 +548,7 @@ def __init__(self, ref: int) -> None: ref: Rust library reference to the Verifier Handle. """ - self._ref: int = ref + self._ref = ref def __del__(self) -> None: """ @@ -555,13 +560,13 @@ def __str__(self) -> str: """ String representation of the Verifier Handle. """ - return f"PactHandle({self._ref})" + return f"VerifierHandle({hex(id(self._ref))})" def __repr__(self) -> str: """ String representation of the Verifier Handle. """ - return f"PactHandle({self._ref!r})" + return f"" class ExpressionValueType(Enum): @@ -6370,7 +6375,7 @@ def verifier_new_for_application() -> VerifierHandle: """ from pact import __version__ - result: int = lib.pactffi_verifier_new_for_application( + result: cffi.FFI.CData = lib.pactffi_verifier_new_for_application( b"pact-python", __version__.encode("utf-8"), ) @@ -6388,61 +6393,98 @@ def verifier_shutdown(handle: VerifierHandle) -> None: def verifier_set_provider_info( # noqa: PLR0913 handle: VerifierHandle, - name: str, - scheme: str, - host: str, - port: int, - path: str, + name: str | None, + scheme: str | None, + host: str | None, + port: int | None, + path: str | None, ) -> None: """ Set the provider details for the Pact verifier. - Passing a NULL for any field will use the default value for that field. - [Rust `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_provider_info) - # Safety + Args: + handle: + The verifier handle to update. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + name: + A user-friendly name to describe the provider. + + scheme: + Determine the scheme to use, typically one of `HTTP` or `HTTPS`. + + host: + The host of the provider. This may be either a hostname to resolve, + or an IP address. + + port: + The port of the provider. + + path: + The path of the provider. + + If any value is `None`, the default value as determined by the underlying + FFI library will be used. """ - raise NotImplementedError + lib.pactffi_verifier_set_provider_info( + handle._ref, + name.encode("utf-8") if name else ffi.NULL, + scheme.encode("utf-8") if scheme else ffi.NULL, + host.encode("utf-8") if host else ffi.NULL, + port, + path.encode("utf-8") if path else ffi.NULL, + ) def verifier_add_provider_transport( handle: VerifierHandle, - protocol: str, + protocol: str | None, port: int, - path: str, - scheme: str, + path: str | None, + scheme: str | None, ) -> None: """ Adds a new transport for the given provider. - Passing a NULL for any field will use the default value for that field. - [Rust `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_provider_transport) - For non-plugin based message interactions, set protocol to "message" and set - scheme to an empty string or "https" if secure HTTP is required. - Communication to the calling application will be over HTTP to the default - provider hostname. + Args: + handle: + The verifier handle to update. - # Safety + protocol: + In this context, the kind of - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + port: + The port of the provider. + + path: + The path of the provider. + + scheme: + The scheme to use, typically one of `HTTP` or `HTTPS`. + + If any value is `None`, the default value as determined by the underlying + FFI library will be used. """ - raise NotImplementedError + lib.pactffi_verifier_add_provider_transport( + handle._ref, + protocol.encode("utf-8") if protocol else ffi.NULL, + port, + path.encode("utf-8") if path else ffi.NULL, + scheme.encode("utf-8") if scheme else ffi.NULL, + ) def verifier_set_filter_info( handle: VerifierHandle, - filter_description: str, - filter_state: str, - filter_no_state: int, + filter_description: str | None, + filter_state: str | None, + *, + filter_no_state: bool, ) -> None: """ Set the filters for the Pact verifier. @@ -6450,26 +6492,35 @@ def verifier_set_filter_info( [Rust `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_filter_info) - If `filter_description` is not empty, it needs to be as a regular - expression. + Set filters to narrow down the interactions to verify. - `filter_no_state` is a boolean value. Set it to greater than zero to turn - the option on. + Args: + handle: + The verifier handle to update. - # Safety + filter_description: + A regular expression to filter the interactions by description. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + filter_state: + A regular expression to filter the interactions by state. + filter_no_state: + If `True`, the option to filter by state will be turned on. """ - raise NotImplementedError + lib.pactffi_verifier_set_filter_info( + handle._ref, + filter_description.encode("utf-8") if filter_description else ffi.NULL, + filter_state.encode("utf-8") if filter_state else ffi.NULL, + filter_no_state, + ) def verifier_set_provider_state( handle: VerifierHandle, url: str, - teardown: int, - body: int, + *, + teardown: bool, + body: bool, ) -> None: """ Set the provider state URL for the Pact verifier. @@ -6477,134 +6528,170 @@ def verifier_set_provider_state( [Rust `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_provider_state) - `teardown` is a boolean value. If teardown state change requests should be - made after an interaction is validated (default is false). Set it to greater - than zero to turn the option on. `body` is a boolean value. Sets if state - change request data should be sent in the body (> 0, true) or as query - parameters (== 0, false). Set it to greater than zero to turn the option on. + Args: + handle: + The verifier handle to update. - # Safety + url: + The URL to use for the provider state. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + teardown: + If teardown state change requests should be made after an + interaction is validated. - """ - raise NotImplementedError + body: + If state change request data should be sent in the body or the + query. + """ + lib.pactffi_verifier_set_provider_state( + handle._ref, + url.encode("utf-8"), + teardown, + body, + ) def verifier_set_verification_options( handle: VerifierHandle, - disable_ssl_verification: int, + *, + disable_ssl_verification: bool, request_timeout: int, -) -> int: +) -> None: """ Set the options used by the verifier when calling the provider. [Rust `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_verification_options) - `disable_ssl_verification` is a boolean value. Set it to greater than zero - to turn the option on. - - # Safety + Args: + handle: + The verifier handle to update. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + disable_ssl_verification: + If SSL verification should be disabled. + request_timeout: + The timeout for the request in milliseconds. """ - raise NotImplementedError + retval: int = lib.pactffi_verifier_set_verification_options( + handle._ref, + disable_ssl_verification, + request_timeout, + ) + if retval != 0: + msg = f"Failed to set verification options for {handle}." + raise RuntimeError(msg) -def verifier_set_coloured_output(handle: VerifierHandle, coloured_output: int) -> int: +def verifier_set_coloured_output( + handle: VerifierHandle, + *, + enabled: bool, +) -> None: """ Enables or disables coloured output using ANSI escape codes. - By default, coloured output is enabled. - [Rust `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_coloured_output) - `coloured_output` is a boolean value. Set it to greater than zero to turn - the option on. - - # Safety + By default, coloured output is enabled. - This function is safe as long as the handle pointer points to a valid - handle. + Args: + handle: + The verifier handle to update. + enabled: + A boolean value to enable or disable coloured output. """ - raise NotImplementedError + retval: int = lib.pactffi_verifier_set_coloured_output( + handle._ref, + enabled, + ) + if retval != 0: + msg = f"Failed to set coloured output for {handle}." + raise RuntimeError(msg) -def verifier_set_no_pacts_is_error(handle: VerifierHandle, is_error: int) -> int: +def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> None: """ Enables or disables if no pacts are found to verify results in an error. [Rust `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) - `is_error` is a boolean value. Set it to greater than zero to enable an - error when no pacts are found to verify, and set it to zero to disable this. - - # Safety - - This function is safe as long as the handle pointer points to a valid - handle. + Args: + handle: + The verifier handle to update. + enabled: + If `True`, an error will be raised when no pacts are found to verify. """ - raise NotImplementedError + retval: int = lib.pactffi_verifier_set_no_pacts_is_error( + handle._ref, + enabled, + ) + if retval != 0: + msg = f"Failed to set no pacts is error for {handle}." + raise RuntimeError(msg) -def verifier_set_publish_options( # noqa: PLR0913 +def verifier_set_publish_options( handle: VerifierHandle, provider_version: str, build_url: str, provider_tags: List[str], - provider_tags_len: int, provider_branch: str, -) -> int: +) -> None: """ Set the options used when publishing verification results to the Broker. [Rust `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_publish_options) - # Args + Args: + handle: + The verifier handle to update. - - `handle` - The pact verifier handle to update - - `provider_version` - Version of the provider to publish - - `build_url` - URL to the build which ran the verification - - `provider_tags` - Collection of tags for the provider - - `provider_tags_len` - Number of provider tags supplied - - `provider_branch` - Name of the branch used for verification + provider_version: + Version of the provider to publish. - # Safety + build_url: + URL to the build which ran the verification. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + provider_tags: + Collection of tags for the provider. + provider_branch: + Name of the branch used for verification. """ - raise NotImplementedError + retval: int = lib.pactffi_verifier_set_publish_options( + handle._ref, + provider_version.encode("utf-8"), + build_url.encode("utf-8"), + [ffi.new("char[]", t.encode("utf-8")) for t in provider_tags or []], + len(provider_tags), + provider_branch.encode("utf-8"), + ) + if retval != 0: + msg = f"Failed to set publish options for {handle}." + raise RuntimeError(msg) def verifier_set_consumer_filters( handle: VerifierHandle, - consumer_filters: List[str], - consumer_filters_len: int, + consumer_filters: Collection[str], ) -> None: """ Set the consumer filters for the Pact verifier. [Rust `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_consumer_filters) - - # Safety - - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. - """ - raise NotImplementedError + lib.pactffi_verifier_set_consumer_filters( + handle._ref, + [ffi.new("char[]", f.encode("utf-8")) for f in consumer_filters], + len(consumer_filters), + ) def verifier_add_custom_header( @@ -6617,13 +6704,12 @@ def verifier_add_custom_header( [Rust `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_custom_header) - - # Safety - - The header name and value must point to a valid NULL terminated string and - must contain valid UTF-8. """ - raise NotImplementedError + lib.pactffi_verifier_add_custom_header( + handle._ref, + header_name.encode("utf-8"), + header_value.encode("utf-8"), + ) def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: @@ -6632,14 +6718,8 @@ def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: [Rust `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_file_source) - - # Safety - - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. - """ - raise NotImplementedError + lib.pactffi_verifier_add_file_source(handle._ref, file.encode("utf-8")) def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> None: @@ -6657,124 +6737,179 @@ def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> Non with U+FFFD REPLACEMENT CHARACTER. """ - raise NotImplementedError + lib.pactffi_verifier_add_directory_source(handle._ref, directory.encode("utf-8")) def verifier_url_source( handle: VerifierHandle, url: str, - username: str, - password: str, - token: str, + username: str | None, + password: str | None, + token: str | None, ) -> None: """ Adds a URL as a source to verify. - The Pact file will be fetched from the URL. - [Rust `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_url_source) - If a username and password is given, then basic authentication will be used - when fetching the pact file. If a token is provided, then bearer token - authentication will be used. + Args: + handle: + The verifier handle to update. - # Safety + url: + The URL to use as a source for the verifier. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + username: + The username to use when fetching pacts from the URL. + password: + The password to use when fetching pacts from the URL. + + token: + The token to use when fetching pacts from the URL. This will be used + as a bearer token. It is mutually exclusive with the username and + password. """ - raise NotImplementedError + lib.pactffi_verifier_url_source( + handle._ref, + url.encode("utf-8"), + username.encode("utf-8") if username else ffi.NULL, + password.encode("utf-8") if password else ffi.NULL, + token.encode("utf-8") if token else ffi.NULL, + ) def verifier_broker_source( handle: VerifierHandle, url: str, - username: str, - password: str, - token: str, + username: str | None, + password: str | None, + token: str | None, ) -> None: """ Adds a Pact broker as a source to verify. + [Rust + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_broker_source) + This will fetch all the pact files from the broker that match the provider name. - [Rust - `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_broker_source) + Args: + handle: + The verifier handle to update. - If a username and password is given, then basic authentication will be used - when fetching the pact file. If a token is provided, then bearer token - authentication will be used. + url: + The URL to use as a source for the verifier. - # Safety + username: + The username to use when fetching pacts from the broker. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + password: + The password to use when fetching pacts from the broker. + token: + The token to use when fetching pacts from the broker. This will be + used as a bearer token. """ - raise NotImplementedError + lib.pactffi_verifier_broker_source( + handle._ref, + url.encode("utf-8"), + username.encode("utf-8") if username else ffi.NULL, + password.encode("utf-8") if password else ffi.NULL, + token.encode("utf-8") if token else ffi.NULL, + ) def verifier_broker_source_with_selectors( # noqa: PLR0913 handle: VerifierHandle, url: str, - username: str, - password: str, - token: str, + username: str | None, + password: str | None, + token: str | None, enable_pending: int, - include_wip_pacts_since: str, + include_wip_pacts_since: datetime.date | None, provider_tags: List[str], - provider_tags_len: int, - provider_branch: str, + provider_branch: str | None, consumer_version_selectors: List[str], - consumer_version_selectors_len: int, consumer_version_tags: List[str], - consumer_version_tags_len: int, ) -> None: """ Adds a Pact broker as a source to verify. - This will fetch all the pact files from the broker that match the provider - name and the consumer version selectors (See - `https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/`). - [Rust `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) - The consumer version selectors must be passed in in JSON format. - - `enable_pending` is a boolean value. Set it to greater than zero to turn the - option on. - - If the `include_wip_pacts_since` option is provided, it needs to be a date - formatted in ISO format (YYYY-MM-DD). - - If a username and password is given, then basic authentication will be used - when fetching the pact file. If a token is provided, then bearer token - authentication will be used. - - # Safety - - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + This will fetch all the pact files from the broker that match the provider + name and the consumer version selectors (See [Consumer Version + Selectors](https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/)). - """ - raise NotImplementedError + Args: + handle: + The verifier handle to update. + + url: + The URL to use as a source for the verifier. + + username: + The username to use when fetching pacts from the broker. + + password: + The password to use when fetching pacts from the broker. + + token: + The token to use when fetching pacts from the broker. This will be + used as a bearer token. + + enable_pending: + If pending pacts should be included in the verification process. + + include_wip_pacts_since: + The date to use to filter out WIP pacts. + + provider_tags: + The tags to use to filter the provider pacts. + + provider_branch: + The branch to use to filter the provider pacts. + + consumer_version_selectors: + The consumer version selectors to use to filter the consumer pacts. + + consumer_version_tags: + The tags to use to filter the consumer pacts. + """ + lib.pactffi_verifier_broker_source_with_selectors( + handle._ref, + url.encode("utf-8"), + username.encode("utf-8") if username else ffi.NULL, + password.encode("utf-8") if password else ffi.NULL, + token.encode("utf-8") if token else ffi.NULL, + enable_pending, + include_wip_pacts_since.isoformat().encode("utf-8") + if include_wip_pacts_since + else ffi.NULL, + [ffi.new("char[]", t.encode("utf-8")) for t in provider_tags], + len(provider_tags), + provider_branch.encode("utf-8") if provider_branch else ffi.NULL, + [ffi.new("char[]", s.encode("utf-8")) for s in consumer_version_selectors], + len(consumer_version_selectors), + [ffi.new("char[]", t.encode("utf-8")) for t in consumer_version_tags], + len(consumer_version_tags), + ) -def verifier_execute(handle: VerifierHandle) -> int: +def verifier_execute(handle: VerifierHandle) -> None: """ Runs the verification. - [Rust `pactffi_verifier_execute`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_execute) - - # Error Handling - - Errors will be reported with a non-zero return value. + (https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_execute) """ - raise NotImplementedError + success: int = lib.pactffi_verifier_execute(handle._ref) + if success != 0: + msg = f"Failed to execute verifier for {handle}." + raise RuntimeError(msg) def verifier_cli_args() -> str: @@ -6840,68 +6975,73 @@ def verifier_logs(handle: VerifierHandle) -> OwnedString: """ Extracts the logs for the verification run. - This needs the memory buffer log sink to be setup before the verification is - executed. The returned string will need to be freed with the `free_string` - function call to avoid leaking memory. - [Rust `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_logs) - Will return a NULL pointer if the logs for the verification can not be - retrieved. + This needs the memory buffer log sink to be setup before the verification is + executed. The returned string will need to be freed with the `free_string` + function call to avoid leaking memory. """ - raise NotImplementedError + ptr = lib.pactffi_verifier_logs(handle._ref) + if ptr == ffi.NULL: + msg = f"Failed to get logs for {handle}." + raise RuntimeError(msg) + return OwnedString(ptr) def verifier_logs_for_provider(provider_name: str) -> OwnedString: """ Extracts the logs for the verification run for the provider name. - This needs the memory buffer log sink to be setup before the verification is - executed. The returned string will need to be freed with the `free_string` - function call to avoid leaking memory. - [Rust `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_logs_for_provider) - Will return a NULL pointer if the logs for the verification can not be - retrieved. + This needs the memory buffer log sink to be setup before the verification is + executed. The returned string will need to be freed with the `free_string` + function call to avoid leaking memory. """ - raise NotImplementedError + ptr = lib.pactffi_verifier_logs_for_provider(provider_name.encode("utf-8")) + if ptr == ffi.NULL: + msg = f"Failed to get logs for {provider_name}." + raise RuntimeError(msg) + return OwnedString(ptr) def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: """ Extracts the standard output for the verification run. - The returned string will need to be freed with the `free_string` function - call to avoid leaking memory. - [Rust `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_output) - * `strip_ansi` - This parameter controls ANSI escape codes. Setting it to a - non-zero value - will cause the ANSI control codes to be stripped from the output. + Args: + handle: + The verifier handle to update. - Will return a NULL pointer if the handle is invalid. + strip_ansi: + This parameter controls ANSI escape codes. Setting it to a non-zero + value will cause the ANSI control codes to be stripped from the + output. """ - raise NotImplementedError + ptr = lib.pactffi_verifier_output(handle._ref, strip_ansi) + if ptr == ffi.NULL: + msg = f"Failed to get output for {handle}." + raise RuntimeError(msg) + return OwnedString(ptr) def verifier_json(handle: VerifierHandle) -> OwnedString: """ Extracts the verification result as a JSON document. - The returned string will need to be freed with the `free_string` function - call to avoid leaking memory. - [Rust `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_json) - - Will return a NULL pointer if the handle is invalid. """ - raise NotImplementedError + ptr = lib.pactffi_verifier_json(handle._ref) + if ptr == ffi.NULL: + msg = f"Failed to get JSON for {handle}." + raise RuntimeError(msg) + return OwnedString(ptr) def using_plugin( diff --git a/src/pact/v3/verifier.py b/src/pact/v3/verifier.py new file mode 100644 index 000000000..c6cf8f436 --- /dev/null +++ b/src/pact/v3/verifier.py @@ -0,0 +1,881 @@ +""" +Verifier for Pact. + +The Verifier is used to verify that a provider meets the expectations of a +consumer. This is done by replaying interactions from the consumer against the +provider, and ensuring that the provider's responses match the expectations set +by the consumer. +""" + +from __future__ import annotations + +import json +from datetime import date +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal, overload + +from typing_extensions import Self +from yarl import URL + +import pact.v3.ffi + +if TYPE_CHECKING: + from collections.abc import Iterable + + +class Verifier: + """ + A Verifier between a consumer and a provider. + + This class encapsulates the logic for verifying that a provider meets the + expectations of a consumer. This is done by replaying interactions from the + consumer against the provider, and ensuring that the provider's responses + match the expectations set by the consumer. + """ + + def __init__(self) -> None: + """ + Create a new Verifier. + """ + self._handle: pact.v3.ffi.VerifierHandle = ( + pact.v3.ffi.verifier_new_for_application() + ) + + # In order to provide a fluent interface, we remember some options which + # are set using the same FFI method. + self._disable_ssl_verification = False + self._request_timeout = 5000 + + def __str__(self) -> str: + """ + Informal string representation of the Verifier. + """ + return "Verifier" + + def __repr__(self) -> str: + """ + Information-rish string representation of the Verifier. + """ + return f"" + + def set_info( # noqa: PLR0913 + self, + name: str, + *, + url: str | URL | None = None, + scheme: str | None = None, + host: str | None = None, + port: int | None = None, + path: str | None = None, + ) -> Self: + """ + Set the provider information. + + This sets up information about the provider as well as the way it + communicates with the consumer. Note that for historical reasons, a + HTTP(S) transport method is always added. + + For a provider which uses other protocols (such as message queues), the + [`add_provider_transport`][pact.v3.verifier.Verifier.add_provider_transport] + must be used. This method can be called multiple times to add multiple + transport methods. + + Args: + name: + A user-friendly name for the provider. + + url: + The URL on which requests are made to the provider by Pact. + + It is recommended to use this parameter to set the provider URL. + If the port is not explicitly set, the default port for the + scheme will be used. + + This parameter is mutually exclusive with the individual + parameters. + + scheme: + The provider scheme. This must be one of `http` or `https`. + + host: + The provider hostname or IP address. If the provider is running + on the same machine as the verifier, `localhost` can be used. + + port: + The provider port. If not specified, the default port for the + schema will be used. + + path: + The provider context path. If not specified, the root path will + be used. + + If a non-root path is used, the path given here will be + prepended to the path in the interaction. For example, if the + path is `/api`, and the interaction path is `/users`, the + request will be made to `/api/users`. + """ + if url is not None: + if any(param is not None for param in (scheme, host, port, path)): + msg = "Cannot specify both `url` and individual parameters" + raise ValueError(msg) + + url = URL(url) + scheme = url.scheme + host = url.host + port = url.explicit_port + path = url.path + + if port is None: + msg = "Unable to determine default port for scheme {scheme}" + raise ValueError(msg) + + pact.v3.ffi.verifier_set_provider_info( + self._handle, + name, + scheme, + host, + port, + path, + ) + return self + + url = URL.build( + scheme=scheme or "http", + host=host or "localhost", + port=port, + path=path or "", + ) + return self.set_info(name, url=url) + + def add_transport( + self, + *, + protocol: str, + port: int | None = None, + path: str | None = None, + scheme: str | None = None, + ) -> Self: + """ + Add a provider transport method. + + If the provider supports multiple transport methods, or non-HTTP(S) + methods, this method allows these additional transport methods to be + added. It can be called multiple times to add multiple transport methods. + + As some transport methods may not use ports, paths or schemes, these + parameters are optional. + + Args: + protocol: + The protocol to use. This will typically be one of: + + - `http` for communications over HTTP(S). Note that when + setting up the provider information in + [`set_provider_info`][pact.v3.verifier.Verifier.set_provider_info], + a HTTP transport method is always added and it is unlikely + that an additional HTTP transport method will be needed + unless the provider is running on additional ports. + + - `message` for non-plugin synchronous message-based + communications. + + Any other protocol will be treated as a custom protocol and will + be handled by a plugin. + + port: + The provider port. + + If the protocol does not use ports, this parameter should be + `None`. If not specified, the default port for the scheme will + be used (provided the scheme is known). + + path: + The provider context path. + + For protocols which do not use paths, this parameter should be + `None`. + + For protocols which do use paths, this parameter should be + specified to avoid any ambiguity, though if left unspecified, + the root path will be used. + + If a non-root path is used, the path given here will be + prepended to the path in the interaction. For example, if the + path is `/api`, and the interaction path is `/users`, the + request will be made to `/api/users`. + + scheme: + The provider scheme, if applicable to the protocol. + + This is typically only used for the `http` protocol, where this + value can either be `http` (the default) or `https`. + """ + if port is None and scheme: + if scheme.lower() == "http": + port = 80 + elif scheme.lower() == "https": + port = 443 + + pact.v3.ffi.verifier_add_provider_transport( + self._handle, + protocol, + port or 0, + path, + scheme, + ) + return self + + def filter( + self, + description: str | None = None, + *, + state: str | None = None, + no_state: bool = False, + ) -> Self: + """ + Set the filter for the interactions. + + This method can be used to filter interactions based on their + description and state. Repeated calls to this method will replace the + previous filter. + + Args: + description: + The interaction description. This should be a regular + expression. If unspecified, no filtering will be done based on + the description. + + state: + The interaction state. This should be a regular expression. If + unspecified, no filtering will be done based on the state. + + no_state: + Whether to include interactions with no state. + """ + pact.v3.ffi.verifier_set_filter_info( + self._handle, + description, + state, + filter_no_state=no_state, + ) + return self + + def set_state( + self, + url: str | URL, + *, + teardown: bool = False, + body: bool = False, + ) -> Self: + """ + Set the provider state URL. + + The URL is used when the provider's internal state needs to be changed. + For example, a consumer might have an interaction that requires a + specific user to be present in the database. The provider state URL is + used to change the provider's internal state to include the required + user. + + Args: + url: + The URL to which a `POST` request will be made to change the + provider's internal state. + + teardown: + Whether to teardown the provider state after an interaction is + validated. + + body: + Whether to include the state change request in the body (`True`) + or in the query string (`False`). + """ + pact.v3.ffi.verifier_set_provider_state( + self._handle, + url if isinstance(url, str) else str(url), + teardown=teardown, + body=body, + ) + return self + + def disable_ssl_verification(self) -> Self: + """ + Disable SSL verification. + """ + self._disable_ssl_verification = True + pact.v3.ffi.verifier_set_verification_options( + self._handle, + disable_ssl_verification=self._disable_ssl_verification, + request_timeout=self._request_timeout, + ) + return self + + def set_request_timeout(self, timeout: int) -> Self: + """ + Set the request timeout. + + Args: + timeout: + The request timeout in milliseconds. + """ + if timeout < 0: + msg = "Request timeout must be a positive integer" + raise ValueError(msg) + + self._request_timeout = timeout + pact.v3.ffi.verifier_set_verification_options( + self._handle, + disable_ssl_verification=self._disable_ssl_verification, + request_timeout=self._request_timeout, + ) + return self + + def set_coloured_output(self, *, enabled: bool = True) -> Self: + """ + Toggle coloured output. + """ + pact.v3.ffi.verifier_set_coloured_output(self._handle, enabled=enabled) + return self + + def set_error_on_empty_pact(self, *, enabled: bool = True) -> Self: + """ + Toggle error on empty pact. + + If enabled, a Pact file with no interactions will cause the verifier to + return an error. If disabled, a Pact file with no interactions will be + ignored. + """ + pact.v3.ffi.verifier_set_no_pacts_is_error(self._handle, enabled=enabled) + return self + + def set_publish_options( + self, + version: str, + url: str, + branch: str, + tags: list[str] | None = None, + ) -> Self: + """ + Set options used when publishing results to the Broker. + + Args: + version: + The provider version. + + url: + URL to the build which ran the verification. + + tags: + Collection of tags for the provider. + + branch: + Name of the branch used for verification. + """ + pact.v3.ffi.verifier_set_publish_options( + self._handle, + version, + url, + tags or [], + branch, + ) + return self + + def filter_consumers(self, *filters: str) -> Self: + """ + Filter the consumers. + + Args: + filters: + Filters to apply to the consumers. + """ + pact.v3.ffi.verifier_set_consumer_filters(self._handle, filters) + return self + + def add_custom_header(self, name: str, value: str) -> Self: + """ + Add a customer header to the request. + + These headers are added to every request made to the provider. + + Args: + name: + The key of the header. + + value: + The value of the header. + """ + pact.v3.ffi.verifier_add_custom_header(self._handle, name, value) + return self + + def add_custom_headers( + self, + headers: dict[str, str] | Iterable[tuple[str, str]], + ) -> Self: + """ + Add multiple customer headers to the request. + + These headers are added to every request made to the provider. + + Args: + headers: + The headers to add. This can be a dictionary or an iterable of + key-value pairs. The iterable is preferred as it ensures that + repeated headers are not lost. + """ + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.add_custom_header(name, value) + return self + + @overload + def add_source( + self, + source: str | URL, + *, + username: str | None = None, + password: str | None = None, + ) -> Self: ... + + @overload + def add_source(self, source: str | URL, *, token: str | None = None) -> Self: ... + + @overload + def add_source(self, source: str | Path) -> Self: ... + + def add_source( + self, + source: str | Path | URL, + *, + username: str | None = None, + password: str | None = None, + token: str | None = None, + ) -> Self: + """ + Adds a source to the verifier. + + This will use one or more Pact files as the source of interactions to + verify. + + Args: + source: + The source of the interactions. This may be either of the + following: + + - A local file path to a Pact file. + - A local file path to a directory containing Pact files. + - A URL to a Pact file. + + If using a URL, the `username` and `password` parameters can be + used to provide basic HTTP authentication, or the `token` + parameter can be used to provide bearer token authentication. + The `username` and `password` parameters can also be passed as + part of the URL. + + username: + The username to use for basic HTTP authentication. This is only + used when the source is a URL. + + password: + The password to use for basic HTTP authentication. This is only + used when the source is a URL. + + token: + The token to use for bearer token authentication. This is only + used when the source is a URL. Note that this is mutually + exclusive with `username` and `password`. + """ + if isinstance(source, Path): + return self._add_source_local(source) + + if isinstance(source, URL): + if source.scheme == "file": + return self._add_source_local(source.path) + + if source.scheme in ("http", "https"): + return self._add_source_remote( + source, + username=username, + password=password, + token=token, + ) + + msg = f"Invalid source scheme: {source.scheme}" + raise ValueError(msg) + + # Strings are ambiguous, so we need identify them as either local or + # remote. + if "://" in source: + return self._add_source_remote( + URL(source), + username=username, + password=password, + token=token, + ) + return self._add_source_local(source) + + def _add_source_local(self, source: str | Path) -> Self: + """ + Adds a local source to the verifier. + + This will use one or more Pact files as the source of interactions to + verify. + + Args: + source: + The source of the interactions. This may be either of the + following: + + - A local file path to a Pact file. + - A local file path to a directory containing Pact files. + """ + source = Path(source) + if source.is_dir(): + pact.v3.ffi.verifier_add_directory_source(self._handle, str(source)) + return self + if source.is_file(): + pact.v3.ffi.verifier_add_file_source(self._handle, str(source)) + return self + msg = f"Invalid source: {source}" + raise ValueError(msg) + + def _add_source_remote( + self, + url: str | URL, + *, + username: str | None = None, + password: str | None = None, + token: str | None = None, + ) -> Self: + """ + Add a remote source to the verifier. + + This will use a Pact file accessible over HTTP or HTTPS as the source of + interactions to verify. + + Args: + url: + The source of the interactions. This must be a URL to a Pact + file. The URL may contain a username and password for basic HTTP + authentication. + + username: + The username to use for basic HTTP authentication. If the source + is a URL containing a username, this parameter must be `None`. + + password: + The password to use for basic HTTP authentication. If the source + is a URL containing a password, this parameter must be `None`. + + token: + The token to use for bearer token authentication. This is + mutually exclusive with `username` and `password` (whether they + be specified through arguments, or embedded in the URL). + """ + url = URL(url) + + if url.user and username: + msg = "Cannot specify both `username` and a username in the URL" + raise ValueError(msg) + username = url.user or username + + if url.password and password: + msg = "Cannot specify both `password` and a password in the URL" + raise ValueError(msg) + password = url.password or password + + if token and (username or password): + msg = "Cannot specify both `token` and `username`/`password`" + raise ValueError(msg) + + pact.v3.ffi.verifier_url_source( + self._handle, + str(url), + username, + password, + token, + ) + return self + + @overload + def broker_source( + self, + url: str | URL, + *, + username: str | None = None, + password: str | None = None, + selector: Literal[False] = False, + ) -> Self: ... + + @overload + def broker_source( + self, + url: str | URL, + *, + token: str | None = None, + selector: Literal[False] = False, + ) -> Self: ... + + @overload + def broker_source( + self, + url: str | URL, + *, + username: str | None = None, + password: str | None = None, + selector: Literal[True], + ) -> BrokerSelectorBuilder: ... + + @overload + def broker_source( + self, + url: str | URL, + *, + token: str | None = None, + selector: Literal[True], + ) -> BrokerSelectorBuilder: ... + + def broker_source( # noqa: PLR0913 + self, + url: str | URL, + *, + username: str | None = None, + password: str | None = None, + token: str | None = None, + selector: bool = False, + ) -> BrokerSelectorBuilder | Self: + """ + Adds a broker source to the verifier. + + Args: + url: + The broker URL. TThe URL may contain a username and password for + basic HTTP authentication. + + username: + The username to use for basic HTTP authentication. If the source + is a URL containing a username, this parameter must be `None`. + + password: + The password to use for basic HTTP authentication. If the source + is a URL containing a password, this parameter must be `None`. + + token: + The token to use for bearer token authentication. This is + mutually exclusive with `username` and `password` (whether they + be specified through arguments, or embedded in the URL). + + selector: + Whether to return a BrokerSelectorBuilder instance. + """ + url = URL(url) + + if url.user and username: + msg = "Cannot specify both `username` and a username in the URL" + raise ValueError(msg) + username = url.user or username + + if url.password and password: + msg = "Cannot specify both `password` and a password in the URL" + raise ValueError(msg) + password = url.password or password + + if token and (username or password): + msg = "Cannot specify both `token` and `username`/`password`" + raise ValueError(msg) + + if selector: + return BrokerSelectorBuilder( + self, + str(url), + username, + password, + token, + ) + pact.v3.ffi.verifier_broker_source( + self._handle, + str(url), + username, + password, + token, + ) + return self + + def verify(self) -> Self: + """ + Verify the interactions. + + Returns: + Whether the interactions were verified successfully. + """ + pact.v3.ffi.verifier_execute(self._handle) + return self + + @property + def logs(self) -> str: + """ + Get the logs. + """ + return pact.v3.ffi.verifier_logs(self._handle) + + @classmethod + def logs_for_provider(cls, provider: str) -> str: + """ + Get the logs for a provider. + """ + return pact.v3.ffi.verifier_logs_for_provider(provider) + + def output(self, *, strip_ansi: bool = False) -> str: + """ + Get the output. + """ + return pact.v3.ffi.verifier_output(self._handle, strip_ansi=strip_ansi) + + @property + def results(self) -> dict[str, Any]: + """ + Get the results. + """ + return json.loads(pact.v3.ffi.verifier_json(self._handle)) + + +class BrokerSelectorBuilder: + """ + A Broker selector. + + This class encapsulates the logic for selecting Pacts from a Pact broker. + """ + + def __init__( # noqa: PLR0913 + self, + verifier: Verifier, + url: str, + username: str | None, + password: str | None, + token: str | None, + ) -> None: + """ + Instantiate a new Broker Selector. + + This constructor should not be called directly. Instead, use the + `broker_source` method of the `Verifier` class with `selector=True`. + """ + self._verifier = verifier + self._url = url + self._username = username + self._password = password + self._token = token + + # If the instance is dropped without having the `build()` method called, + # raise a warning. + self._built = False + + self._include_pending: bool = False + "Whether to include pending Pacts." + + self._include_wip_since: date | None = None + "Whether to include work in progress Pacts since a given date." + + self._provider_tags: list[str] | None = None + "List of provider tags to match." + + self._provider_branch: str | None = None + "The provider branch." + + self._consumer_versions: list[str] | None = None + "List of consumer version regex patterns." + + self._consumer_tags: list[str] | None = None + "List of consumer tags to match." + + def include_pending(self) -> Self: + """ + Include pending Pacts. + """ + self._include_pending = True + return self + + def exclude_pending(self) -> Self: + """ + Exclude pending Pacts. + """ + self._include_pending = False + return self + + def include_wip_since(self, d: str | date) -> Self: + """ + Include work in progress Pacts since a given date. + """ + if isinstance(d, str): + d = date.fromisoformat(d) + self._include_wip_since = d + return self + + def exclude_wip(self) -> Self: + """ + Exclude work in progress Pacts. + """ + self._include_wip_since = None + return self + + def provider_tags(self, *tags: str) -> Self: + """ + Set the provider tags. + """ + self._provider_tags = list(tags) + return self + + def provider_branch(self, branch: str) -> Self: + """ + Set the provider branch. + """ + self._provider_branch = branch + return self + + def consumer_versions(self, *versions: str) -> Self: + """ + Set the consumer versions. + """ + self._consumer_versions = list(versions) + return self + + def consumer_tags(self, *tags: str) -> Self: + """ + Set the consumer tags. + """ + self._consumer_tags = list(tags) + return self + + def build(self) -> Verifier: + """ + Build the Broker Selector. + + Returns: + The Verifier instance with the broker source added. + """ + pact.v3.ffi.verifier_broker_source_with_selectors( + self._verifier._handle, # noqa: SLF001 + self._url, + self._username, + self._password, + self._token, + self._include_pending, + self._include_wip_since, + self._provider_tags or [], + self._provider_branch, + self._consumer_versions or [], + self._consumer_tags or [], + ) + self._built = True + return self._verifier + + def __del__(self) -> None: + """ + Destructor for the Broker Selector. + + This destructor will raise a warning if the instance is dropped without + having the [`build()`][pact.v3.verifier.BrokerSelectorBuilder.build] + method called. + """ + if not self._built: + msg = "BrokerSelectorBuilder was dropped before being built." + raise Warning(msg) From 97bbf76aa67313aa0f470710e4a399606c934284 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 8 Mar 2024 15:07:00 +1100 Subject: [PATCH 0259/1376] chore(v3): add basic verifier tests These tests are purely meant to detect any issues with the FFI and changes in any of the verifier methods. The compatibility suite will ensure that the verifier is functioning correctly. Signed-off-by: JP-Ellis --- tests/v3/assets/pacts/basic.json | 8 ++ tests/v3/test_verifier.py | 176 +++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 tests/v3/assets/pacts/basic.json create mode 100644 tests/v3/test_verifier.py diff --git a/tests/v3/assets/pacts/basic.json b/tests/v3/assets/pacts/basic.json new file mode 100644 index 000000000..5c8fb325b --- /dev/null +++ b/tests/v3/assets/pacts/basic.json @@ -0,0 +1,8 @@ +{ + "consumer": { "name": "Example Consumer" }, + "provider": { "name": "Example Producer" }, + "metadata": { + "pactSpecification": { "version": "2.0" } + }, + "interactions": [] +} diff --git a/tests/v3/test_verifier.py b/tests/v3/test_verifier.py new file mode 100644 index 000000000..cedb66511 --- /dev/null +++ b/tests/v3/test_verifier.py @@ -0,0 +1,176 @@ +""" +Unit tests for the pact.v3.verifier module. + +These tests perform only very basic checks to ensure that the FFI module is +working correctly. They are not intended to test the Verifier API much, as +that is handled by the compatibility suite. +""" + +import re +from pathlib import Path + +import pytest + +from pact.v3.verifier import Verifier + +ASSETS_DIR = Path(__file__).parent / "assets" + + +@pytest.fixture() +def verifier() -> Verifier: + return Verifier() + + +def test_str_repr(verifier: Verifier) -> None: + assert str(verifier) == "Verifier" + assert re.match(r"", repr(verifier)) + + +def test_set_provider_info(verifier: Verifier) -> None: + name = "test_provider" + url = "http://localhost:8888/api" + verifier.set_info(name, url=url) + + scheme = "http" + host = "localhost" + port = 8888 + path = "/api" + verifier.set_info( + name, + scheme=scheme, + host=host, + port=port, + path=path, + ) + + +def test_add_provider_transport(verifier: Verifier) -> None: + # HTTP + verifier.add_transport( + protocol="http", + port=1234, + path="/api", + scheme="http", + ) + + # HTTPS + verifier.add_transport( + protocol="http", + port=4321, + path="/api", + scheme="https", + ) + + # message + verifier.add_transport( + protocol="message", + ) + + # gRPC + verifier.add_transport( + protocol="grpc", + port=1234, + ) + + +def test_set_filter(verifier: Verifier) -> None: + verifier.filter("test_filter") + verifier.filter("test_filter", state="test_value") + verifier.filter("no_state", no_state=True) + + +def test_set_state(verifier: Verifier) -> None: + verifier.set_state("test_state") + verifier.set_state("test_state", teardown=True) + verifier.set_state("test_state", body=True) + + +def test_disable_ssl_verification(verifier: Verifier) -> None: + verifier.disable_ssl_verification() + + +def test_set_request_timeout(verifier: Verifier) -> None: + verifier.set_request_timeout(1000) + + +def test_set_coloured_output(verifier: Verifier) -> None: + verifier.set_coloured_output(enabled=True) + verifier.set_coloured_output(enabled=False) + + +def test_set_error_on_empty_pact(verifier: Verifier) -> None: + verifier.set_error_on_empty_pact(enabled=True) + verifier.set_error_on_empty_pact(enabled=False) + + +def test_set_publish_options(verifier: Verifier) -> None: + verifier.set_publish_options( + version="1.0.0", + url="http://localhost:8080/build/1234", + branch="main", + tags=["main", "test", "prod"], + ) + + +def test_filter_consumers(verifier: Verifier) -> None: + verifier.filter_consumers("consumer1") + verifier.filter_consumers("consumer1", "consumer2") + + +def test_add_custom_header(verifier: Verifier) -> None: + verifier.add_custom_header("Authorization", "Bearer: 1234") + + +def test_add_custom_headers(verifier: Verifier) -> None: + verifier.add_custom_headers({ + "Authorization": "Bearer: 1234", + "Content-Type": "application/json", + }) + + +def test_add_source(verifier: Verifier) -> None: + # URL + verifier.add_source("http://localhost:8080/pact.json") + + # File + verifier.add_source(ASSETS_DIR / "pacts" / "basic.json") + + # Directory + verifier.add_source(ASSETS_DIR / "pacts") + + +def test_broker_source(verifier: Verifier) -> None: + verifier.broker_source("http://localhost:8080") + verifier.broker_source( + "http://localhost:8080", + username="user", + password="password", # noqa: S106 + ) + verifier.broker_source( + "http://localhost:8080", + token="1234", # noqa: S106 + ) + + +def test_broker_source_selector(verifier: Verifier) -> None: + ( + verifier.broker_source("http://localhost:8080", selector=True) + .consumer_tags("main", "test") + .provider_tags("main", "test") + .consumer_versions("1.2.3") + .build() + ) + + +def test_verify(verifier: Verifier) -> None: + verifier.verify() + + +def test_logs(verifier: Verifier) -> None: + logs = verifier.logs + assert logs == "" + + +def test_output(verifier: Verifier) -> None: + output = verifier.output() + assert output == "" From 3392555fcff3b389b5482b72286029e5d790d78a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:15:35 +0000 Subject: [PATCH 0260/1376] chore(deps): update softprops/action-gh-release digest to 3198ee1 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48e08ae36..4ed71b20b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -229,7 +229,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@d99959edae48b5ffffd7b00da66dcdb0a33a52ee # v2 + uses: softprops/action-gh-release@3198ee18f814cdf787321b4a32a26ddbf37acc52 # v2 with: files: wheels/* body_path: ${{ runner.temp }}/changelog From d50158e796b8738b19aa2605c490e21809bb9c14 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 19:59:38 +0000 Subject: [PATCH 0261/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.18.3 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 272198893..5e42e93ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.18.1 + rev: v3.18.3 hooks: - id: commitizen stages: [commit-msg] From 4bcf8f4d9c1a19c0cd30a146f86451cb812240a0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 22:27:11 +0000 Subject: [PATCH 0262/1376] chore(deps): update pypa/cibuildwheel action to v2.17.0 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4ed71b20b..c570d97e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -103,7 +103,7 @@ jobs: echo "MACOSX_DEPLOYMENT_TARGET=10.12" >> "$GITHUB_ENV" - name: Create wheels - uses: pypa/cibuildwheel@ce3fb7832089eb3e723a0a99cab7f3eaccf074fd # v2.16.5 + uses: pypa/cibuildwheel@8d945475ac4b1aac4ae08b2fd27db9917158b6ce # v2.17.0 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} @@ -161,7 +161,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@ce3fb7832089eb3e723a0a99cab7f3eaccf074fd # v2.16.5 + uses: pypa/cibuildwheel@8d945475ac4b1aac4ae08b2fd27db9917158b6ce # v2.17.0 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} From a041823874053af6aba04bd38990cf1092a18bfc Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 12 Mar 2024 09:49:42 +1100 Subject: [PATCH 0263/1376] chore(deps): refactor dependencies The main change is to prefix the optional dependency groups used for development only with a `devel-`. For example, `pact-python[devel-types]` instead of `pact-python[types]`. This helps ensure that end-users don't get confused. There's also a little bit of minor reformatting. Signed-off-by: JP-Ellis --- pyproject.toml | 56 +++++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f00c782c6..3dfd96f32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,15 +38,15 @@ requires-python = ">=3.8" # - A specific feature is required in a new minor release # - A minor version address vulnerability which directly impacts Pact Python dependencies = [ - "cffi ~= 1.0", - "click ~= 8.0", - "fastapi ~= 0.0", - "psutil ~= 5.0", - "requests ~= 2.0", - "six ~= 1.0", - "typing-extensions ~= 4.0 ; python_version < '3.10'", - "uvicorn ~= 0.0", - "yarl ~= 1.0", + "cffi ~=1.0", + "click ~=8.0", + "fastapi ~=0.0", + "psutil ~=5.0", + "requests ~=2.0", + "six ~=1.0", + "typing-extensions ~=4.0 ; python_version < '3.10'", + "uvicorn ~=0.0", + "yarl ~=1.0", ] [project.urls] @@ -62,25 +62,29 @@ pact-verifier = "pact.cli.verify:main" [project.optional-dependencies] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -types = [ +devel-types = [ "mypy ==1.9.0", - "types-cffi ~= 1.0", - "types-requests ~= 2.0", + "types-cffi ~=1.0", + "types-requests ~=2.0", ] -test = [ - "aiohttp[speedups] ~= 3.0", - "coverage[toml] ~= 7.0", - "flask[async] ~= 3.0", - "httpx ~= 0.0", - "mock ~= 5.0", - # pytest 8.1.0 is not compatible with pytest-bdd yet - "pytest ~= 8.0.0", - "pytest-asyncio ~= 0.0", - "pytest-bdd ~= 7.0", - "pytest-cov ~= 4.0", - "testcontainers ~= 3.0", +devel-test = [ + "aiohttp[speedups] ~=3.0", + "coverage[toml] ~=7.0", + "flask[async] ~=3.0", + "httpx ~=0.0", + "mock ~=5.0", + # TODO: Upgrade to PyTest 8.1 + # Pending on https://github.com/pytest-dev/pytest-bdd/issues/673 + "pytest ~=8.0.0", + "pytest-asyncio ~=0.0", + "pytest-bdd ~=7.0", + "pytest-cov ~=4.0", + "testcontainers ~=3.0", +] +devel = [ + "pact-python[devel-types,devel-test]", + "ruff ==0.3.2" ] -dev = ["pact-python[types]", "pact-python[test]", "ruff==0.3.2"] ################################################################################ ## Hatch Build Configuration @@ -138,7 +142,7 @@ artifacts = [ # Install dev dependencies in the default environment to simplify the developer # workflow. [tool.hatch.envs.default] -features = ["dev"] +features = ["devel"] extra-dependencies = [ "hatchling", "packaging", From 87b89867513f76f4a048f052f9a57345e0a66f95 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 12 Mar 2024 11:56:08 +1100 Subject: [PATCH 0264/1376] chore: unskip tests Some tests were skipped due to an upstream issue. As the issue is now fixed, the tests can be re-enabled. Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/test_v1_consumer.py | 10 ---------- tests/v3/compatibility_suite/util/__init__.py | 3 +-- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/v3/compatibility_suite/test_v1_consumer.py b/tests/v3/compatibility_suite/test_v1_consumer.py index 084429630..75c7b07d2 100644 --- a/tests/v3/compatibility_suite/test_v1_consumer.py +++ b/tests/v3/compatibility_suite/test_v1_consumer.py @@ -4,7 +4,6 @@ import logging -import pytest from pytest_bdd import given, parsers, scenario from tests.v3.compatibility_suite.util import ( @@ -153,9 +152,6 @@ def test_request_with_invalid_body() -> None: """ -# TODO: Enable this test when the upstream issue is resolved: -# https://github.com/pact-foundation/pact-compatibility-suite/issues/3 -@pytest.mark.skip("Waiting on upstream fix") @scenario( "definition/features/V1/http_consumer.feature", "Request with the incorrect type of body contents", @@ -226,9 +222,6 @@ def test_request_with_xml_body_negative_case() -> None: """ -# TODO: Enable this test when the upstream issue is resolved: -# https://github.com/pact-foundation/pact-reference/issues/336 -@pytest.mark.skip("Waiting on upstream fix") @scenario( "definition/features/V1/http_consumer.feature", "Request with a binary body (positive case)", @@ -239,9 +232,6 @@ def test_request_with_a_binary_body_positive_case() -> None: """ -# TODO: Enable this test when the upstream issue is resolved: -# https://github.com/pact-foundation/pact-reference/issues/336 -@pytest.mark.skip("Waiting on upstream fix") @scenario( "definition/features/V1/http_consumer.feature", "Request with a binary body (negative case)", diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 947dd7cae..93734916a 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -199,7 +199,6 @@ def __init__(self, data: str) -> None: self.bytes = data.encode("utf-8") self.string = data - self.mime_type = "text/plain" def __repr__(self) -> str: """ @@ -311,7 +310,7 @@ def update(self, **kwargs: str) -> None: # noqa: C901, PLR0912 # the content type. orig_content_type = self.body.mime_type if self.body else None self.body = InteractionDefinition.Body(body) - self.body.mime_type = orig_content_type or self.body.mime_type + self.body.mime_type = self.body.mime_type or orig_content_type if content_type := ( kwargs.pop("content_type", None) or kwargs.pop("content type", None) From 5206dbd2833a77db32c45e0ce33ad8524829698a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 12 Mar 2024 11:54:32 +1100 Subject: [PATCH 0265/1376] feat(v3): add verbose mismatches If the Pact Server exits with mismatches, an error is raised. In addition, the mismatches are exported as a JSON to the logger. This new behaviour can be disabled by setting `verbose=False` when starting the server. Signed-off-by: JP-Ellis --- src/pact/v3/pact.py | 37 +++++++++++++++++++++++++------ tests/v3/test_http_interaction.py | 24 ++++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index 045145a4c..18bccac0b 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -11,6 +11,8 @@ from __future__ import annotations +import json +import logging from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, Set, overload @@ -31,6 +33,8 @@ except ImportError: from typing_extensions import Self +logger = logging.getLogger(__name__) + class Pact: """ @@ -236,6 +240,7 @@ def serve( # noqa: PLR0913 transport_config: str | None = None, *, raises: bool = True, + verbose: bool = True, ) -> PactServer: """ Return a mock server for the Pact. @@ -264,9 +269,14 @@ def serve( # noqa: PLR0913 Configuration for the transport. This is specific to the transport being used and should be a JSON string. - raises: Whether to raise an exception if there are mismatches - between the Pact and the server. If set to `False`, then the - mismatches must be handled manually. + raises: + Whether to raise an exception if there are mismatches between + the Pact and the server. If set to `False`, then the mismatches + must be handled manually. + + verbose: + Whether or not to print the mismatches to the logger. This works + independently of `raises`. Returns: A [`PactServer`][pact.v3.pact.PactServer] instance. @@ -278,6 +288,7 @@ def serve( # noqa: PLR0913 transport, transport_config, raises=raises, + verbose=verbose, ) def messages(self) -> pact.v3.ffi.PactMessageIterator: @@ -419,6 +430,7 @@ def __init__( # noqa: PLR0913 transport_config: str | None = None, *, raises: bool = True, + verbose: bool = True, ) -> None: """ Initialise a new Pact Server. @@ -452,8 +464,13 @@ def __init__( # noqa: PLR0913 Configuration for the transport. This is specific to the transport being used and should be a JSON string. - raises: Whether or not to raise an exception if the server - is not matched upon exit. + raises: + Whether or not to raise an exception if the server is not + matched upon exit. + + verbose: + Whether or not to print the mismatches to the logger. This works + independently of `raises`. """ self._host = host self._port = port @@ -462,6 +479,7 @@ def __init__( # noqa: PLR0913 self._pact_handle = pact_handle self._handle: None | pact.v3.ffi.PactServerHandle = None self._raises = raises + self._verbose = verbose @property def port(self) -> int: @@ -592,8 +610,13 @@ def __exit__( If the server has not been fully matched and the server is configured to raise an exception. """ - if self._handle: - if self._raises and not self.matched: + if self._handle and not self.matched: + if self._verbose: + logger.error( + "Mismatches:\n%s", + json.dumps(self.mismatches, indent=2), + ) + if self._raises: raise MismatchesError(self.mismatches) self._handle = None diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py index c328b18e3..1c76b44ce 100644 --- a/tests/v3/test_http_interaction.py +++ b/tests/v3/test_http_interaction.py @@ -5,6 +5,7 @@ from __future__ import annotations import json +import logging import re from typing import TYPE_CHECKING @@ -536,3 +537,26 @@ async def test_with_plugin(pact: Pact) -> None: async with session.get("/") as resp: assert resp.status == 200 assert await resp.read() == b"" + + +@pytest.mark.asyncio() +async def test_pact_server_verbose( + pact: Pact, + caplog: pytest.LogCaptureFixture, +) -> None: + ( + pact.upon_receiving("a basic request with a plugin") + .with_request("GET", "/foo") + .will_respond_with(200) + ) + with caplog.at_level(logging.WARNING, logger="pact.v3.pact"), pact.serve( + raises=False, verbose=True + ) as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.get("/bar") as resp: + assert resp.status == 500 + + assert len(caplog.records) == 1 + for record in caplog.records: + assert record.levelname == "ERROR" + assert record.message.startswith("Mismatches:\n") From a48ac2bbd8befb193472491238a72c686c836122 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 12 Mar 2024 12:24:04 +1100 Subject: [PATCH 0266/1376] chore: fix missed s/test/devel-test/ Signed-off-by: JP-Ellis --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3dfd96f32..01d5b452a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -164,7 +164,7 @@ docs-build = "mkdocs build {args}" # Test environment for running unit tests. This automatically tests against all # supported Python versions. [tool.hatch.envs.test] -features = ["test"] +features = ["devel-test"] [[tool.hatch.envs.test.matrix]] python = ["3.8", "3.9", "3.10", "3.11", "3.12"] From 65d637a0da53e2e0cfad31eef23167360ea9ad14 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 13 Mar 2024 09:14:00 +1100 Subject: [PATCH 0267/1376] chore(v3): improve body representation While debugging a flaky test, I realised that `truncate` was applied to the key-value pair, instead of just the value part. This made debugging more difficult than it ought have been. Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 93734916a..919543db5 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -205,7 +205,9 @@ def __repr__(self) -> str: Debugging representation. """ return "".format( - ", ".join(truncate(f"{k}={v!r}") for k, v in vars(self).items()), + ", ".join( + str(k) + "=" + truncate(repr(v)) for k, v in vars(self).items() + ), ) def parse_fixture(self, fixture: Path) -> None: From 116bc9a50dae802a06180a8bb55f3bf45e04c63d Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 13 Mar 2024 09:33:41 +1100 Subject: [PATCH 0268/1376] chore(test): improve test logging Signed-off-by: JP-Ellis --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 01d5b452a..820218d8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -188,6 +188,10 @@ filterwarnings = [ "ignore::PendingDeprecationWarning:tests", ] +log_level = "NOTSET" +log_format = "%(asctime)s [%(levelname)-8s] %(name)s: %(message)s" +log_date-format = "%H:%M:%S" + markers = [ # Markers for the compatibility suite "consumer", From a5fb8d8247ac438f43bc86f0b2fd972354adb76a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 19:28:29 +0000 Subject: [PATCH 0269/1376] chore(deps): update softprops/action-gh-release digest to 9d7c94c --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c570d97e1..c9028f534 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -229,7 +229,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@3198ee18f814cdf787321b4a32a26ddbf37acc52 # v2 + uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 # v2 with: files: wheels/* body_path: ${{ runner.temp }}/changelog From ee55bc0ace9af1671a569156baf030b74f85297f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:08:53 +0000 Subject: [PATCH 0270/1376] chore(deps): update peter-evans/create-pull-request digest to 70a41ab --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c9028f534..2846ebe83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -245,7 +245,7 @@ jobs: packages-dir: wheels - name: Create PR for changelog update - uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc # v6 + uses: peter-evans/create-pull-request@70a41aba780001da0a30141984ae2a0c95d8704e # v6 with: token: ${{ secrets.GH_TOKEN }} commit-message: "chore: update changelog ${{ github.ref_name }}" From 7ac729587336a81445fa3c3cf58fde936fe8e466 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 19:05:59 +0000 Subject: [PATCH 0271/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.18.4 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e42e93ec..5c041ad72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.18.3 + rev: v3.18.4 hooks: - id: commitizen stages: [commit-msg] From 3ee96857f45982a5ea502bd723eab26f0a040050 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 19:09:26 +0000 Subject: [PATCH 0272/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.3.3 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c041ad72..c3eaedd8a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.2 + rev: v0.3.3 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 375ec7ef169d183f5c50b0297e97900ccfba18b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 19:09:22 +0000 Subject: [PATCH 0273/1376] chore(deps): update dependency devel/ruff to v0.3.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 820218d8a..d46be5d82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ devel-test = [ ] devel = [ "pact-python[devel-types,devel-test]", - "ruff ==0.3.2" + "ruff ==0.3.3" ] ################################################################################ From 209772149231ac9267f0c8549f0ead1db58f9f1b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 18:14:32 +0000 Subject: [PATCH 0274/1376] chore(deps): update actions/cache digest to 0c45773 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2846ebe83..26cada02b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,7 +76,7 @@ jobs: fetch-depth: 0 - name: Cache pip packages - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4 with: path: ~/.cache/pip key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} @@ -145,7 +145,7 @@ jobs: fetch-depth: 0 - name: Cache pip packages - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4 with: path: ~/.cache/pip key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} From e063d11bb6c74a7fa3bef066dbbd8be87a2b180b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:20:56 +0000 Subject: [PATCH 0275/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.20.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c3eaedd8a..690de3b13 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.18.4 + rev: v3.20.0 hooks: - id: commitizen stages: [commit-msg] From dc22d87633d868220e834e0aea99b5c0b76bbeed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:13:43 +0000 Subject: [PATCH 0276/1376] chore(deps): update dependency devel/ruff to v0.3.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d46be5d82..915aaf174 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ devel-test = [ ] devel = [ "pact-python[devel-types,devel-test]", - "ruff ==0.3.3" + "ruff ==0.3.4" ] ################################################################################ From bc2e27a8dd05cd8ad7eefb6ea6ac2a3e2404884b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:13:47 +0000 Subject: [PATCH 0277/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.3.4 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 690de3b13..d6b8d69b7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.3 + rev: v0.3.4 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From ed7a0eba0ac84a70884d10372f29ce3a5760a0aa Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Mar 2024 11:26:25 +1100 Subject: [PATCH 0278/1376] fix(v3): allow optional publish options Most of the publish option arguments are optional, but the initial implementation had these as compulsory. Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 12 ++++++------ src/pact/v3/verifier.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index e809e173d..16fd47dfb 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -6638,9 +6638,9 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> def verifier_set_publish_options( handle: VerifierHandle, provider_version: str, - build_url: str, - provider_tags: List[str], - provider_branch: str, + build_url: str | None, + provider_tags: List[str] | None, + provider_branch: str | None, ) -> None: """ Set the options used when publishing verification results to the Broker. @@ -6667,10 +6667,10 @@ def verifier_set_publish_options( retval: int = lib.pactffi_verifier_set_publish_options( handle._ref, provider_version.encode("utf-8"), - build_url.encode("utf-8"), + build_url.encode("utf-8") if build_url else ffi.NULL, [ffi.new("char[]", t.encode("utf-8")) for t in provider_tags or []], - len(provider_tags), - provider_branch.encode("utf-8"), + len(provider_tags or []), + provider_branch.encode("utf-8") if provider_branch else ffi.NULL, ) if retval != 0: msg = f"Failed to set publish options for {handle}." diff --git a/src/pact/v3/verifier.py b/src/pact/v3/verifier.py index c6cf8f436..deef51907 100644 --- a/src/pact/v3/verifier.py +++ b/src/pact/v3/verifier.py @@ -350,8 +350,8 @@ def set_error_on_empty_pact(self, *, enabled: bool = True) -> Self: def set_publish_options( self, version: str, - url: str, - branch: str, + url: str | None = None, + branch: str | None = None, tags: list[str] | None = None, ) -> Self: """ From 6f5d8415ba06a693d8490c8356a72ff92514e4a9 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Mar 2024 11:29:33 +1100 Subject: [PATCH 0279/1376] fix(v3): strip embedded user/password from urls The username and password authentication are passed through other arguments. Signed-off-by: JP-Ellis --- src/pact/v3/verifier.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pact/v3/verifier.py b/src/pact/v3/verifier.py index deef51907..2e8ce85b5 100644 --- a/src/pact/v3/verifier.py +++ b/src/pact/v3/verifier.py @@ -589,7 +589,7 @@ def _add_source_remote( pact.v3.ffi.verifier_url_source( self._handle, - str(url), + str(url.with_user(None).with_password(None)), username, password, token, @@ -686,14 +686,14 @@ def broker_source( # noqa: PLR0913 if selector: return BrokerSelectorBuilder( self, - str(url), + str(url.with_user(None).with_password(None)), username, password, token, ) pact.v3.ffi.verifier_broker_source( self._handle, - str(url), + str(url.with_user(None).with_password(None)), username, password, token, From 0d2212cce54e5ad9b142b7434d618d98abecf30f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Mar 2024 11:33:04 +1100 Subject: [PATCH 0280/1376] chore(tests): update log formatting Signed-off-by: JP-Ellis --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 915aaf174..d4ade576b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -189,8 +189,8 @@ filterwarnings = [ ] log_level = "NOTSET" -log_format = "%(asctime)s [%(levelname)-8s] %(name)s: %(message)s" -log_date-format = "%H:%M:%S" +log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" +log_date_format = "%H:%M:%S" markers = [ # Markers for the compatibility suite From e43110f5e23cf58a89458dd047557d9a1e172d47 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Mar 2024 11:40:06 +1100 Subject: [PATCH 0281/1376] refactor(tests): move parse_headers/matching_rules out of class These functions do not really belong to the class, and there's now a need for them to be called directly. Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/__init__.py | 78 +++++++++---------- 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 919543db5..0f7fb8ca6 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -149,6 +149,41 @@ def parse_markdown_table(content: str) -> list[dict[str, str]]: return [dict(zip(rows[0], row)) for row in rows[1:]] +def parse_headers(headers: str) -> MultiDict[str]: + """ + Parse the headers. + + The headers are in the format: + + ```text + 'X-A: 1', 'X-B: 2', 'X-A: 3' + ``` + + As headers can be repeated, the result is a MultiDict. + """ + kvs: list[tuple[str, str]] = [] + for header in headers.split(", "): + k, _sep, v = header.strip("'").partition(": ") + kvs.append((k, v)) + return MultiDict(kvs) + + +def parse_matching_rules(matching_rules: str) -> str: + """ + Parse the matching rules. + + The matching rules are in one of two formats: + + - An explicit JSON object, prefixed by `JSON: `. + - A fixture file which contains the matching rules. + """ + if matching_rules.startswith("JSON: "): + return matching_rules[6:] + + with (FIXTURES_ROOT / matching_rules).open("r") as file: + return file.read() + + class InteractionDefinition: """ Interaction definition. @@ -300,12 +335,12 @@ def update(self, **kwargs: str) -> None: # noqa: C901, PLR0912 self.query = query if headers := kwargs.pop("headers", None): - self.headers = self.parse_headers(headers) + self.headers = parse_headers(headers) if headers := ( kwargs.pop("raw headers", None) or kwargs.pop("raw_headers", None) ): - self.headers = self.parse_headers(headers) + self.headers = parse_headers(headers) if body := kwargs.pop("body", None): # When updating the body, we _only_ update the body content, not @@ -337,9 +372,7 @@ def update(self, **kwargs: str) -> None: # noqa: C901, PLR0912 if matching_rules := ( kwargs.pop("matching_rules", None) or kwargs.pop("matching rules", None) ): - self.matching_rules = InteractionDefinition.parse_matching_rules( - matching_rules - ) + self.matching_rules = parse_matching_rules(matching_rules) if len(kwargs) > 0: msg = f"Unexpected arguments: {kwargs.keys()}" @@ -353,41 +386,6 @@ def __repr__(self) -> str: ", ".join(f"{k}={v!r}" for k, v in vars(self).items()), ) - @classmethod - def parse_headers(cls, headers: str) -> MultiDict[str]: - """ - Parse the headers. - - The headers are in the format: - - ```text - 'X-A: 1', 'X-B: 2', 'X-A: 3' - ``` - - As headers can be repeated, the result is a MultiDict. - """ - kvs: list[tuple[str, str]] = [] - for header in headers.split(", "): - k, _sep, v = header.strip("'").partition(": ") - kvs.append((k, v)) - return MultiDict(kvs) - - @classmethod - def parse_matching_rules(cls, matching_rules: str) -> str: - """ - Parse the matching rules. - - The matching rules are in one of two formats: - - - An explicit JSON object, prefixed by `JSON: `. - - A fixture file which contains the matching rules. - """ - if matching_rules.startswith("JSON: "): - return matching_rules[6:] - - with (FIXTURES_ROOT / matching_rules).open("r") as file: - return file.read() - def add_to_pact(self, pact: pact.v3.Pact, name: str) -> None: # noqa: PLR0912, C901 """ Add the interaction to the pact. From 7ed8269f8e99134c3cc5ecc08d77c01e7a8ae71a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Mar 2024 11:42:39 +1100 Subject: [PATCH 0282/1376] chore(test): add state to interaction definition Required for the provider state callbacks. Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 0f7fb8ca6..89a74c737 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -304,6 +304,7 @@ def parse_file(self, file: Path) -> None: def __init__(self, **kwargs: str) -> None: """Initialise the interaction definition.""" self.id: int | None = None + self.state: str | None = None self.method: str = kwargs.pop("method") self.path: str = kwargs.pop("path") self.response: int = int(kwargs.pop("response", 200)) From f2705f54d553ad0e7102ac351555a344e43e1567 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Mar 2024 11:50:07 +1100 Subject: [PATCH 0283/1376] chore(test): adapt InteractionDefinition for provider This introduces a few important changes to the interaction definition: - Addition of `response_headers` - Support for the response status code through either the `response` or `status` kwargs - Combining the response content type and body to reflect the same logic as with the request body and content type. - A new `add_to_flask` method (akin to `add_to_pact`) which defines the interaction for a Flask app. Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/__init__.py | 99 ++++++++++++++++--- 1 file changed, 84 insertions(+), 15 deletions(-) diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 89a74c737..19f779965 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -28,12 +28,13 @@ def _(): from pathlib import Path from xml.etree import ElementTree +import flask +from flask import request from multidict import MultiDict from yarl import URL if typing.TYPE_CHECKING: - import pact.v3 - import pact.v3.pact + from pact.v3.pact import Pact logger = logging.getLogger(__name__) SUITE_ROOT = Path(__file__).parent.parent / "definition" @@ -311,7 +312,7 @@ def __init__(self, **kwargs: str) -> None: self.query: str | None = None self.headers: MultiDict[str] = MultiDict() self.body: InteractionDefinition.Body | None = None - self.response_content: str | None = None + self.response_headers: MultiDict[str] = MultiDict() self.response_body: InteractionDefinition.Body | None = None self.matching_rules: str | None = None self.update(**kwargs) @@ -357,18 +358,31 @@ def update(self, **kwargs: str) -> None: # noqa: C901, PLR0912 self.body = InteractionDefinition.Body("") self.body.mime_type = content_type - if response := kwargs.pop("response", None): + if response := kwargs.pop("response", None) or kwargs.pop("status", None): self.response = int(response) + if response_headers := ( + kwargs.pop("response_headers", None) or kwargs.pop("response headers", None) + ): + self.response_headers = parse_headers(response_headers) + if response_content := ( kwargs.pop("response_content", None) or kwargs.pop("response content", None) ): - self.response_content = response_content + if self.response_body is None: + self.response_body = InteractionDefinition.Body("") + self.response_body.mime_type = response_content if response_body := ( kwargs.pop("response_body", None) or kwargs.pop("response body", None) ): + orig_content_type = ( + self.response_body.mime_type if self.response_body else None + ) self.response_body = InteractionDefinition.Body(response_body) + self.response_body.mime_type = ( + self.response_body.mime_type or orig_content_type + ) if matching_rules := ( kwargs.pop("matching_rules", None) or kwargs.pop("matching rules", None) @@ -387,7 +401,7 @@ def __repr__(self) -> str: ", ".join(f"{k}={v!r}" for k, v in vars(self).items()), ) - def add_to_pact(self, pact: pact.v3.Pact, name: str) -> None: # noqa: PLR0912, C901 + def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912 """ Add the interaction to the pact. @@ -406,6 +420,11 @@ def add_to_pact(self, pact: pact.v3.Pact, name: str) -> None: # noqa: PLR0912, logging.info("with_request(%s, %s)", self.method, self.path) interaction.with_request(self.method, self.path) + # We distinguish between "" and None here. + if self.state is not None: + logging.info("given(%s)", self.state) + interaction.given(self.state) + if self.query: query = URL.build(query_string=self.query).query logging.info("with_query_parameters(%s)", query.items()) @@ -448,31 +467,81 @@ def add_to_pact(self, pact: pact.v3.Pact, name: str) -> None: # noqa: PLR0912, logging.info("will_respond_with(%s)", self.response) interaction.will_respond_with(self.response) - if self.response_content: - if self.response_body is None: - msg = "Expected response body along with response content type" - raise ValueError(msg) - + if self.response_body: if self.response_body.string: logging.info( "with_body(%s, %s)", truncate(self.response_body.string), - self.response_content, + self.response_body.mime_type, ) interaction.with_body( self.response_body.string, - self.response_content, + self.response_body.mime_type, ) elif self.response_body.bytes: logging.info( "with_binary_file(%s, %s)", truncate(self.response_body.bytes), - self.response_content, + self.response_body.mime_type, ) interaction.with_binary_body( self.response_body.bytes, - self.response_content, + self.response_body.mime_type, ) else: msg = "Unexpected body definition" raise RuntimeError(msg) + + def add_to_flask(self, app: flask.Flask) -> None: + """ + Add an interaction to a Flask app. + + Args: + app: + The Flask app to add the interaction to. + """ + + async def route_fn() -> flask.Response: + logger.info("Received request: %s %s", self.method, self.path) + if self.query: + query = URL.build(query_string=self.query).query + # Perform a two-way check to ensure that the query parameters + # are present in the request, and that the request contains no + # unexpected query parameters. + for k, v in query.items(): + assert request.args[k] == v + for k, v in request.args.items(): + assert query[k] == v + + if self.headers: + # Perform a one-way check to ensure that the expected headers + # are present in the request, but don't check for any unexpected + # headers. + for k, v in self.headers.items(): + assert k in request.headers + assert request.headers[k] == v + + if self.body: + assert request.data == self.body.bytes + + return flask.Response( + response=self.response_body.bytes or self.response_body.string or None + if self.response_body + else None, + status=self.response, + headers=self.response_headers, + content_type=self.response_body.mime_type + if self.response_body + else None, + direct_passthrough=True, + ) + + # The route function needs to have a unique name + clean_name = self.path.replace("/", "_").replace("__", "_") + route_fn.__name__ = f"{self.method.lower()}_{clean_name}" + + app.add_url_rule( + self.path, + view_func=route_fn, + methods=[self.method], + ) From ce0ea526a2487e6fd06636fb07b8f93d6cf01daa Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Mar 2024 11:58:05 +1100 Subject: [PATCH 0284/1376] chore(test): add serialize function This is a utility function which is required in order to pass certain arguments between the test suite, and the Flask app running in a separate process. Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/__init__.py | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 19f779965..4c4240d90 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -21,11 +21,15 @@ def _(): from __future__ import annotations +import base64 import contextlib import hashlib import logging import typing +from collections.abc import Collection, Mapping +from datetime import date, datetime, time from pathlib import Path +from typing import Any from xml.etree import ElementTree import flask @@ -97,7 +101,7 @@ def truncate(data: str | bytes) -> str: """ if len(data) <= 32: if isinstance(data, str): - return f"{data!r}" + return f"{data}" return data.decode("utf-8", "backslashreplace") length = len(data) @@ -150,6 +154,57 @@ def parse_markdown_table(content: str) -> list[dict[str, str]]: return [dict(zip(rows[0], row)) for row in rows[1:]] +def serialize(obj: Any) -> Any: # noqa: ANN401, PLR0911 + """ + Convert an object to a dictionary. + + This function converts an object to a dictionary by calling `vars` on the + object. This is useful for classes which are not otherwise serializable + using `json.dumps`. + + A few special cases are handled: + + - If the object is a `datetime` object, it is converted to an ISO 8601 + string. + - All forms of [`Mapping`][collections.abc.Mapping] are converted to + dictionaries. + - All forms of [`Collection`][collections.abc.Collection] are converted to + lists. + + All other types are converted to strings using the `repr` function. + """ + if isinstance(obj, datetime | date | time): + return obj.isoformat() + + # Basic types which are already serializable + if isinstance(obj, str | int | float | bool | type(None)): + return obj + + # Bytes + if isinstance(obj, bytes): + return { + "__class__": obj.__class__.__name__, + "data": base64.b64encode(obj).decode("utf-8"), + } + + # Collections + if isinstance(obj, Mapping): + return {k: serialize(v) for k, v in obj.items()} + + if isinstance(obj, Collection): + return [serialize(v) for v in obj] + + # Objects + if hasattr(obj, "__dict__"): + return { + "__class__": obj.__class__.__name__, + "__module__": obj.__class__.__module__, + **{k: serialize(v) for k, v in obj.__dict__.items()}, + } + + return repr(obj) + + def parse_headers(headers: str) -> MultiDict[str]: """ Parse the headers. From 3c6ee183636e734cb1b5d865daa449d1281c1172 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Mar 2024 11:59:37 +1100 Subject: [PATCH 0285/1376] chore(test): add provider utilities Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/provider.py | 422 ++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 tests/v3/compatibility_suite/util/provider.py diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py new file mode 100644 index 000000000..6ad1d38b3 --- /dev/null +++ b/tests/v3/compatibility_suite/util/provider.py @@ -0,0 +1,422 @@ +""" +Provider utilities for compatibility suite tests. + +The main functionality provided by this module is the ability to start a +provider application with a set of interactions. Since this is done +in a subprocess, any configuration must be passed in through files. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent)) + + +import contextlib +import json +import logging +import os +import pickle +import shutil +import socket +import subprocess +from contextvars import ContextVar +from datetime import UTC, datetime +from pathlib import Path +from typing import TYPE_CHECKING + +import flask +import requests +from flask import request +from yarl import URL + +import pact.constants # type: ignore[import-untyped] +from tests.v3.compatibility_suite.util import serialize + +if TYPE_CHECKING: + from tests.v3.compatibility_suite.util import InteractionDefinition + + +logger = logging.getLogger(__name__) + +version_var = ContextVar("version_var", default="0") + + +def next_version() -> str: + """ + Get the next version for the consumer. + + This is used to generate a new version for the consumer application to use + when publishing the interactions to the Pact Broker. + + Returns: + The next version. + """ + version = version_var.get() + version_var.set(str(int(version) + 1)) + return version + + +def _find_free_port() -> int: + """ + Find a free port. + + This is used to find a free port to host the API on when running locally. It + is allocated, and then released immediately so that it can be used by the + API. + + Returns: + The port number. + """ + with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + s.bind(("", 0)) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return s.getsockname()[1] + + +class Provider: + """ + HTTP Provider. + """ + + def __init__(self, provider_dir: Path | str) -> None: + """ + Instantiate a new provider. + + Args: + provider_dir: + The directory containing various files used to configure the + provider. At a minimum, this directory must contain a file + called `interactions.pkl`. This file must contain a list of + [`InteractionDefinition`] objects. + """ + self.provider_dir = Path(provider_dir) + if not self.provider_dir.is_dir(): + msg = f"Directory {self.provider_dir} does not exist" + raise ValueError(msg) + + self.app: flask.Flask = flask.Flask("provider") + self._add_ping(self.app) + self._add_callback(self.app) + self._add_after_request(self.app) + self._add_interactions(self.app) + + def _add_ping(self, app: flask.Flask) -> None: + """ + Add a ping endpoint to the provider. + + This is used to check that the provider is running. + """ + + @app.get("/_test/ping") + def ping() -> str: + """Simple ping endpoint for testing.""" + return "pong" + + def _add_callback(self, app: flask.Flask) -> None: + """ + Add a callback endpoint to the provider. + + This is used to receive any callbacks from Pact to configure any + internal state (e.g., "given a user exists"). As far as the testing + is concerned, this is just a simple endpoint that records the request + and returns an empty response. + + If the provider directory contains a file called `fail_callback`, then + the callback will return a 404 response. + + If the provider directory contains a file called `provider_state`, then + the callback will check that the `state` query parameter matches the + contents of the file. + """ + + @app.route("/_test/callback", methods=["GET", "POST"]) + def callback() -> tuple[str, int] | str: + if (self.provider_dir / "fail_callback").exists(): + return "Provider state not found", 404 + + provider_state_path = self.provider_dir / "provider_state" + if provider_state_path.exists(): + state = provider_state_path.read_text() + assert request.args["state"] == state + + json_file = ( + self.provider_dir + / f"callback.{datetime.now(tz=UTC).strftime('%H:%M:%S.%f')}.json" + ) + with json_file.open("w") as f: + json.dump( + { + "method": request.method, + "path": request.path, + "query_string": request.query_string.decode("utf-8"), + "query_params": serialize(request.args), + "headers_list": serialize(request.headers), + "headers_dict": serialize(dict(request.headers)), + "body": request.data.decode("utf-8"), + "form": serialize(request.form), + }, + f, + ) + + return "" + + def _add_after_request(self, app: flask.Flask) -> None: + """ + Add a handler to log requests and responses. + + This is used to log the requests and responses to the provider + application (both to the logger as well as to files). + """ + + @app.after_request + def log_request(response: flask.Response) -> flask.Response: + logger.debug("Request: %s %s", request.method, request.path) + logger.debug( + "Request query string: %s", request.query_string.decode("utf-8") + ) + logger.debug("Request query params: %s", serialize(request.args)) + logger.debug("Request headers: %s", serialize(request.headers)) + logger.debug("Request body: %s", request.data.decode("utf-8")) + logger.debug("Request form: %s", serialize(request.form)) + + with ( + self.provider_dir + / f"request.{datetime.now(tz=UTC).strftime('%H:%M:%S.%f')}.json" + ).open("w") as f: + json.dump( + { + "method": request.method, + "path": request.path, + "query_string": request.query_string.decode("utf-8"), + "query_params": serialize(request.args), + "headers_list": serialize(request.headers), + "headers_dict": serialize(dict(request.headers)), + "body": request.data.decode("utf-8"), + "form": serialize(request.form), + }, + f, + ) + return response + + @app.after_request + def log_response(response: flask.Response) -> flask.Response: + try: + body = response.get_data(as_text=True) + except UnicodeDecodeError: + body = "" + logger.debug("Response: %s", response.status_code) + logger.debug("Response headers: %s", serialize(response.headers)) + logger.debug("Response body: %s", body) + with ( + self.provider_dir + / f"response.{datetime.now(tz=UTC).strftime('%H:%M:%S.%f')}.json" + ).open("w") as f: + json.dump( + { + "status_code": response.status_code, + "headers_list": serialize(response.headers), + "headers_dict": serialize(dict(response.headers)), + "body": body, + }, + f, + ) + return response + + def _add_interactions(self, app: flask.Flask) -> None: + """ + Add the interactions to the provider. + """ + with (self.provider_dir / "interactions.pkl").open("rb") as f: + interactions: list[InteractionDefinition] = pickle.load(f) # noqa: S301 + + for interaction in interactions: + interaction.add_to_flask(app) + + def run(self) -> None: + """ + Start the provider. + """ + url = URL(f"http://localhost:{_find_free_port()}") + self.app.run( + host=url.host, + port=url.port, + debug=True, + ) + + +class PactBroker: + """ + Interface to the Pact Broker. + """ + + def __init__( # noqa: PLR0913 + self, + broker_url: URL, + *, + username: str | None = None, + password: str | None = None, + provider: str = "provider", + consumer: str = "consumer", + ) -> None: + """ + Instantiate a new Pact Broker interface. + """ + self.url = broker_url + self.username = broker_url.user or username + self.password = broker_url.password or password + self.provider = provider + self.consumer = consumer + + self.broker_bin: str = ( + shutil.which("pact-broker") or pact.constants.BROKER_CLIENT_PATH + ) + if not self.broker_bin: + if "CI" in os.environ: + self._install() + bin_path = shutil.which("pact-broker") + assert bin_path, "pact-broker not found" + self.broker_bin = bin_path + else: + msg = "pact-broker not found" + raise RuntimeError(msg) + + def _install(self) -> None: + """ + Install the Pact Broker CLI tool. + + This function is intended to be run in CI environments, where the pact-broker + CLI tool may not be installed already. This will download and extract + the tool + """ + msg = "pact-broker not found" + raise NotImplementedError(msg) + + def publish(self, directory: Path | str, version: str | None = None) -> None: + """ + Publish the interactions to the Pact Broker. + + Args: + directory: + The directory containing the pact files. + + version: + The version of the consumer application. + """ + cmd = [ + self.broker_bin, + "publish", + str(directory), + "--broker-base-url", + str(self.url), + ] + if self.username: + cmd.extend(["--broker-username", self.username]) + if self.password: + cmd.extend(["--broker-password", self.password]) + + cmd.extend(["--consumer-app-version", version or next_version()]) + + subprocess.run( + cmd, # noqa: S603 + encoding="utf-8", + check=True, + ) + + def interaction_id(self, num: int) -> str: + """ + Find the interaction ID for the given interaction. + + This function is used to find the Pact Broker interaction ID for the given + interaction. It does this by looking for the interaction with the + description `f"interaction {num}"`. + + Args: + num: + The ID of the interaction. + """ + response = requests.get( + str( + self.url + / "pacts" + / "provider" + / self.provider + / "consumer" + / self.consumer + / "latest" + ), + timeout=2, + ) + response.raise_for_status() + for interaction in response.json()["interactions"]: + if interaction["description"] == f"interaction {num}": + return interaction["_id"] + msg = f"Interaction {num} not found" + raise ValueError(msg) + + def verification_results(self, num: int) -> requests.Response: + """ + Fetch the verification results for the given interaction. + + Args: + num: + The ID of the interaction. + """ + interaction_id = self.interaction_id(num) + response = requests.get( + str( + self.url + / "pacts" + / "provider" + / self.provider + / "consumer" + / self.consumer + / "latest" + / "verification-results" + / interaction_id + ), + timeout=2, + ) + response.raise_for_status() + return response + + def latest_verification_results(self) -> requests.Response | None: + """ + Fetch the latest verification results for the provider. + + If there are no verification results, then this function will return + `None`. + """ + response = requests.get( + str( + self.url + / "pacts" + / "provider" + / self.provider + / "consumer" + / self.consumer + / "latest" + ), + timeout=2, + ) + response.raise_for_status() + links = response.json()["_links"] + response = requests.get( + links["pb:latest-verification-results"]["href"], timeout=2 + ) + if response.status_code == 404: + return None + response.raise_for_status() + return response + + +if __name__ == "__main__": + import sys + + if len(sys.argv) != 2: + sys.stderr.write(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + Provider(sys.argv[1]).run() From cd4585e146eab5bde9e636c75d39444ac0c7fe65 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Mar 2024 12:27:11 +1100 Subject: [PATCH 0286/1376] chore(tests): add v1 provider compatibility suite Signed-off-by: JP-Ellis --- pyproject.toml | 1 + .../compatibility_suite/test_v1_provider.py | 904 ++++++++++++++++++ .../compatibility_suite/util/pact-broker.yml | 43 + 3 files changed, 948 insertions(+) create mode 100644 tests/v3/compatibility_suite/test_v1_provider.py create mode 100644 tests/v3/compatibility_suite/util/pact-broker.yml diff --git a/pyproject.toml b/pyproject.toml index d4ade576b..9559c6ee7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -195,6 +195,7 @@ log_date_format = "%H:%M:%S" markers = [ # Markers for the compatibility suite "consumer", + "provider", ] ################################################################################ diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py new file mode 100644 index 000000000..f795f3ce0 --- /dev/null +++ b/tests/v3/compatibility_suite/test_v1_provider.py @@ -0,0 +1,904 @@ +""" +Basic HTTP provider feature test. +""" + +from __future__ import annotations + +import copy +import json +import logging +import pickle +import re +import signal +import subprocess +import sys +import time +from pathlib import Path +from threading import Thread +from typing import Any, Generator, NoReturn + +import pytest +import requests +from pytest_bdd import given, parsers, scenario, then, when +from testcontainers.compose import DockerCompose # type: ignore[import-untyped] +from yarl import URL + +from pact.v3.pact import Pact +from pact.v3.verifier import Verifier +from tests.v3.compatibility_suite.util import ( + InteractionDefinition, + parse_headers, + parse_markdown_table, +) +from tests.v3.compatibility_suite.util.provider import PactBroker + +logger = logging.getLogger(__name__) + + +@pytest.fixture() +def verifier() -> Verifier: + """Return a new Verifier.""" + return Verifier() + + +################################################################################ +## Scenario +################################################################################ + + +@scenario( + "definition/features/V1/http_provider.feature", + "Verifying a simple HTTP request", +) +def test_verifying_a_simple_http_request() -> None: + """Verifying a simple HTTP request.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Verifying multiple Pact files", +) +def test_verifying_multiple_pact_files() -> None: + """Verifying multiple Pact files.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Incorrect request is made to provider", +) +def test_incorrect_request_is_made_to_provider() -> None: + """Incorrect request is made to provider.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Verifying a simple HTTP request via a Pact broker", +) +def test_verifying_a_simple_http_request_via_a_pact_broker() -> None: + """Verifying a simple HTTP request via a Pact broker.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Verifying a simple HTTP request via a Pact broker with publishing results enabled", +) +def test_verifying_a_simple_http_request_via_a_pact_broker_with_publishing() -> None: + """Verifying a simple HTTP request via a Pact broker with publishing.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Verifying multiple Pact files via a Pact broker", +) +def test_verifying_multiple_pact_files_via_a_pact_broker() -> None: + """Verifying multiple Pact files via a Pact broker.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Incorrect request is made to provider via a Pact broker", +) +def test_incorrect_request_is_made_to_provider_via_a_pact_broker() -> None: + """Incorrect request is made to provider via a Pact broker.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Verifying an interaction with a defined provider state", +) +def test_verifying_an_interaction_with_a_defined_provider_state() -> None: + """Verifying an interaction with a defined provider state.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Verifying an interaction with no defined provider state", +) +def test_verifying_an_interaction_with_no_defined_provider_state() -> None: + """Verifying an interaction with no defined provider state.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Verifying an interaction where the provider state callback fails", +) +def test_verifying_an_interaction_where_the_provider_state_callback_fails() -> None: + """Verifying an interaction where the provider state callback fails.""" + + +# TODO: Enable this test once we can capture warnings +# https://github.com/pact-foundation/pact-reference/issues/404 +@pytest.mark.skip("Unable to get warnings to be captured") +@scenario( + "definition/features/V1/http_provider.feature", + "Verifying an interaction where a provider state callback is not configured", +) +def test_verifying_an_interaction_where_no_provider_state_callback_configured() -> None: + """Verifying an interaction where a provider state callback is not configured.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Verifying a HTTP request with a request filter configured", +) +def test_verifying_a_http_request_with_a_request_filter_configured() -> None: + """Verifying a HTTP request with a request filter configured.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Verifies the response status code", +) +def test_verifies_the_response_status_code() -> None: + """Verifies the response status code.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Verifies the response headers", +) +def test_verifies_the_response_headers() -> None: + """Verifies the response headers.""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Response with plain text body (positive case)", +) +def test_response_with_plain_text_body_positive_case() -> None: + """Response with plain text body (positive case).""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Response with plain text body (negative case)", +) +def test_response_with_plain_text_body_negative_case() -> None: + """Response with plain text body (negative case).""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Response with JSON body (positive case)", +) +def test_response_with_json_body_positive_case() -> None: + """Response with JSON body (positive case).""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Response with JSON body (negative case)", +) +def test_response_with_json_body_negative_case() -> None: + """Response with JSON body (negative case).""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Response with XML body (positive case)", +) +def test_response_with_xml_body_positive_case() -> None: + """Response with XML body (positive case).""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Response with XML body (negative case)", +) +def test_response_with_xml_body_negative_case() -> None: + """Response with XML body (negative case).""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Response with binary body (positive case)", +) +def test_response_with_binary_body_positive_case() -> None: + """Response with binary body (positive case).""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Response with binary body (negative case)", +) +def test_response_with_binary_body_negative_case() -> None: + """Response with binary body (negative case).""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Response with form post body (positive case)", +) +def test_response_with_form_post_body_positive_case() -> None: + """Response with form post body (positive case).""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Response with form post body (negative case)", +) +def test_response_with_form_post_body_negative_case() -> None: + """Response with form post body (negative case).""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Response with multipart body (positive case)", +) +def test_response_with_multipart_body_positive_case() -> None: + """Response with multipart body (positive case).""" + + +@scenario( + "definition/features/V1/http_provider.feature", + "Response with multipart body (negative case)", +) +def test_response_with_multipart_body_negative_case() -> None: + """Response with multipart body (negative case).""" + + +################################################################################ +## Given +################################################################################ + + +@given( + parsers.parse("the following HTTP interactions have been defined:\n{content}"), + target_fixture="interaction_definitions", + converters={"content": parse_markdown_table}, +) +def the_following_http_interactions_have_been_defined( + content: list[dict[str, str]], +) -> dict[int, InteractionDefinition]: + """ + Parse the HTTP interactions table into a dictionary. + + The table columns are expected to be: + + - No + - method + - path + - query + - headers + - body + - response + - response headers + - response content + - response body + + The first row is ignored, as it is assumed to be the column headers. The + order of the columns is similarly ignored. + """ + logger.debug("Parsing interaction definitions") + + # Check that the table is well-formed + assert len(content[0]) == 10, f"Expected 10 columns, got {len(content[0])}" + assert "No" in content[0], "'No' column not found" + + # Parse the table into a more useful format + interactions: dict[int, InteractionDefinition] = {} + for row in content: + interactions[int(row["No"])] = InteractionDefinition(**row) + return interactions + + +@given( + parsers.re( + r"a provider is started that returns the responses? " + r'from interactions? "?(?P[0-9, ]+)"?', + ), + converters={"interactions": lambda x: [int(i) for i in x.split(",") if i]}, + target_fixture="provider_url", +) +def a_provider_is_started_that_returns_the_responses_from_interactions( + interaction_definitions: dict[int, InteractionDefinition], + interactions: list[int], + temp_dir: Path, +) -> Generator[URL, None, None]: + """ + Start a provider that returns the responses from the given interactions. + """ + logger.debug("Starting provider for interactions %s", interactions) + + for i in interactions: + logger.debug("Interaction %d: %s", i, interaction_definitions[i]) + + with (temp_dir / "interactions.pkl").open("wb") as pkl_file: + pickle.dump([interaction_definitions[i] for i in interactions], pkl_file) + + yield from start_provider(temp_dir) + + +@given( + parsers.re( + r"a provider is started that returns the responses?" + r' from interactions? "?(?P[0-9, ]+)"?' + r" with the following changes:\n(?P.+)", + re.DOTALL, + ), + converters={ + "interactions": lambda x: [int(i) for i in x.split(",") if i], + "changes": parse_markdown_table, + }, + target_fixture="provider_url", +) +def a_provider_is_started_that_returns_the_responses_from_interactions_with_changes( + interaction_definitions: dict[int, InteractionDefinition], + interactions: list[int], + changes: list[dict[str, str]], + temp_dir: Path, +) -> Generator[URL, None, None]: + """ + Start a provider that returns the responses from the given interactions. + """ + logger.debug("Starting provider for interactions %s", interactions) + + assert len(changes) == 1, "Only one set of changes is supported" + defns: list[InteractionDefinition] = [] + for interaction in interactions: + defn = copy.deepcopy(interaction_definitions[interaction]) + defn.update(**changes[0]) + defns.append(defn) + logger.debug( + "Update interaction %d: %s", + interaction, + defn, + ) + + with (temp_dir / "interactions.pkl").open("wb") as pkl_file: + pickle.dump(defns, pkl_file) + + yield from start_provider(temp_dir) + + +def start_provider(provider_dir: str | Path) -> Generator[URL, None, None]: # noqa: C901 + """Start the provider app with the given interactions.""" + process = subprocess.Popen( + [ # noqa: S603 + sys.executable, + Path(__file__).parent / "util" / "provider.py", + str(provider_dir), + ], + cwd=Path.cwd(), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + + pattern = re.compile(r" \* Running on (?P[^ ]+)") + while True: + if process.poll() is not None: + logger.error("Provider process exited with code %d", process.returncode) + logger.error( + "Provider stdout: %s", process.stdout.read() if process.stdout else "" + ) + logger.error( + "Provider stderr: %s", process.stderr.read() if process.stderr else "" + ) + msg = f"Provider process exited with code {process.returncode}" + raise RuntimeError(msg) + if ( + process.stderr + and (line := process.stderr.readline()) + and (match := pattern.match(line)) + ): + break + time.sleep(0.1) + + url = URL(match.group("url")) + logger.debug("Provider started on %s", url) + for _ in range(50): + try: + response = requests.get(str(url / "_test" / "ping"), timeout=1) + assert response.text == "pong" + break + except (requests.RequestException, AssertionError): + time.sleep(0.1) + continue + else: + msg = "Failed to ping provider" + raise RuntimeError(msg) + + def redirect() -> NoReturn: + while True: + if process.stdout: + while line := process.stdout.readline(): + logger.debug("Provider stdout: %s", line) + if process.stderr: + while line := process.stderr.readline(): + logger.debug("Provider stderr: %s", line) + + thread = Thread(target=redirect, daemon=True) + thread.start() + + yield url + + process.send_signal(signal.SIGINT) + + +@given( + parsers.re( + r"a Pact file for interaction (?P\d+) is to be verified", + ), + converters={"interaction": int}, +) +def a_pact_file_for_interaction_is_to_be_verified( + interaction_definitions: dict[int, InteractionDefinition], + verifier: Verifier, + interaction: int, + temp_dir: Path, +) -> None: + """ + Verify the Pact file for the given interaction. + """ + logger.debug( + "Adding interaction %d to be verified: %s", + interaction, + interaction_definitions[interaction], + ) + + defn = interaction_definitions[interaction] + + pact = Pact("consumer", "provider") + pact.with_specification("V1") + defn.add_to_pact(pact, f"interaction {interaction}") + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + + verifier.add_source(temp_dir / "pacts") + + +@given( + parsers.re( + r"a Pact file for interaction (?P\d+)" + r" is to be verified from a Pact broker", + ), + converters={"interaction": int}, + target_fixture="pact_broker", +) +def a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker( + interaction_definitions: dict[int, InteractionDefinition], + verifier: Verifier, + interaction: int, + temp_dir: Path, +) -> Generator[PactBroker, None, None]: + """ + Verify the Pact file for the given interaction from a Pact broker. + """ + logger.debug("Adding interaction %d to be verified from a Pact broker", interaction) + + defn = interaction_definitions[interaction] + + pact = Pact("consumer", "provider") + pact.with_specification("V1") + defn.add_to_pact(pact, f"interaction {interaction}") + + pacts_dir = temp_dir / "pacts" + pacts_dir.mkdir(exist_ok=True, parents=True) + pact.write_file(pacts_dir) + + with DockerCompose( + Path(__file__).parent / "util", + compose_file_name="pact-broker.yml", + pull=True, + ) as _: + pact_broker = PactBroker(URL("http://pactbroker:pactbroker@localhost:9292")) + pact_broker.publish(pacts_dir) + verifier.broker_source(pact_broker.url) + yield pact_broker + + +@given("publishing of verification results is enabled") +def publishing_of_verification_results_is_enabled(verifier: Verifier) -> None: + """ + Enable publishing of verification results. + """ + logger.debug("Publishing verification results") + + verifier.set_publish_options( + "0.0.0", + ) + + +@given( + parsers.re( + r"a provider state callback is configured" + r"(?P(, but will return a failure)?)", + ), + converters={"failure": lambda x: x != ""}, +) +def a_provider_state_callback_is_configured( + verifier: Verifier, + provider_url: URL, + temp_dir: Path, + failure: bool, # noqa: FBT001 +) -> None: + """ + Configure a provider state callback. + """ + logger.debug("Configuring provider state callback") + + if failure: + with (temp_dir / "fail_callback").open("w") as f: + f.write("true") + + verifier.set_state( + provider_url / "_test" / "callback", + teardown=True, + ) + + +@given( + parsers.re( + r"a Pact file for interaction (?P\d+) is to be verified" + r' with a provider state "(?P[^"]+)" defined', + ), + converters={"interaction": int}, +) +def a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state_define( + interaction_definitions: dict[int, InteractionDefinition], + verifier: Verifier, + interaction: int, + state: str, + temp_dir: Path, +) -> None: + """ + Verify the Pact file for the given interaction with a provider state defined. + """ + logger.debug( + "Adding interaction %d to be verified with provider state %s", + interaction, + state, + ) + + defn = interaction_definitions[interaction] + defn.state = state + + pact = Pact("consumer", "provider") + pact.with_specification("V1") + defn.add_to_pact(pact, f"interaction {interaction}") + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + + verifier.add_source(temp_dir / "pacts") + + with (temp_dir / "provider_state").open("w") as f: + logger.debug("Writing provider state to %s", temp_dir / "provider_state") + f.write(state) + + +@given( + parsers.parse( + "a request filter is configured to make the following changes:\n{content}" + ), + converters={"content": parse_markdown_table}, +) +def a_request_filter_is_configured_to_make_the_following_changes( + content: list[dict[str, str]], + verifier: Verifier, +) -> None: + """ + Configure a request filter to make the given changes. + """ + logger.debug("Configuring request filter") + + if "headers" in content[0]: + verifier.add_custom_headers(parse_headers(content[0]["headers"]).items()) + else: + msg = "Unsupported filter type" + raise RuntimeError(msg) + + +################################################################################ +## When +################################################################################ + + +@when("the verification is run", target_fixture="verifier_result") +def the_verification_is_run( + verifier: Verifier, + provider_url: URL, +) -> tuple[Verifier, Exception | None]: + """ + Run the verification. + """ + logger.debug("Running verification on %r", verifier) + + verifier.set_info("provider", url=provider_url) + try: + verifier.verify() + except Exception as e: # noqa: BLE001 + return verifier, e + return verifier, None + + +################################################################################ +## Then +################################################################################ + + +@then( + parsers.re(r"the verification will(?P( NOT)?) be successful"), + converters={"negated": lambda x: x == " NOT"}, +) +def the_verification_will_be_successful( + verifier_result: tuple[Verifier, Exception | None], + negated: bool, # noqa: FBT001 +) -> None: + """ + Check that the verification was successful. + """ + logger.debug("Checking verification result") + logger.debug("Verifier result: %s", verifier_result) + + if negated: + assert verifier_result[1] is not None + else: + assert verifier_result[1] is None + + +@then( + parsers.re(r'the verification results will contain a "(?P[^"]+)" error'), +) +def the_verification_results_will_contain_a_error( + verifier_result: tuple[Verifier, Exception | None], error: str +) -> None: + """ + Check that the verification results contain the given error. + """ + logger.debug("Checking that verification results contain error %s", error) + + verifier = verifier_result[0] + logger.debug("Verification results: %s", json.dumps(verifier.results, indent=2)) + + if error == "Response status did not match": + mismatch_type = "StatusMismatch" + elif error == "Headers had differences": + mismatch_type = "HeaderMismatch" + elif error == "Body had differences": + mismatch_type = "BodyMismatch" + elif error == "State change request failed": + assert "One or more of the setup state change handlers has failed" in [ + error["mismatch"]["message"] for error in verifier.results["errors"] + ] + return + else: + msg = f"Unknown error type: {error}" + raise ValueError(msg) + + assert mismatch_type in [ + mismatch["type"] + for error in verifier.results["errors"] + for mismatch in error["mismatch"]["mismatches"] + ] + + +@then( + parsers.re(r"a verification result will NOT be published back"), +) +def a_verification_result_will_not_be_published_back(pact_broker: PactBroker) -> None: + """ + Check that the verification result was published back to the Pact broker. + """ + logger.debug("Checking that verification result was not published back") + + response = pact_broker.latest_verification_results() + if response: + with pytest.raises(requests.HTTPError, match="404 Client Error"): + response.raise_for_status() + + +@then( + parsers.re( + "a successful verification result " + "will be published back " + r"for interaction \{(?P\d+)\}", + ), + converters={"interaction": int}, +) +def a_successful_verification_result_will_be_published_back( + pact_broker: PactBroker, + interaction: int, +) -> None: + """ + Check that the verification result was published back to the Pact broker. + """ + logger.debug( + "Checking that verification result was published back for interaction %d", + interaction, + ) + + interaction_id = pact_broker.interaction_id(interaction) + response = pact_broker.latest_verification_results() + assert response is not None + assert response.ok + data: dict[str, Any] = response.json() + assert data["success"] + + for test_result in data["testResults"]: + if test_result["interactionId"] == interaction_id: + assert test_result["success"] + break + else: + msg = f"Interaction {interaction} not found in verification results" + raise ValueError(msg) + + +@then( + parsers.re( + "a failed verification result " + "will be published back " + r"for the interaction \{(?P\d+)\}", + ), + converters={"interaction": int}, +) +def a_failed_verification_result_will_be_published_back( + pact_broker: PactBroker, + interaction: int, +) -> None: + """ + Check that the verification result was published back to the Pact broker. + """ + logger.debug( + "Checking that failed verification result" + " was published back for interaction %d", + interaction, + ) + + interaction_id = pact_broker.interaction_id(interaction) + response = pact_broker.latest_verification_results() + assert response is not None + assert response.ok + data: dict[str, Any] = response.json() + assert not data["success"] + + for test_result in data["testResults"]: + if test_result["interactionId"] == interaction_id: + assert not test_result["success"] + break + else: + msg = f"Interaction {interaction} not found in verification results" + raise ValueError(msg) + + +@then("the provider state callback will be called before the verification is run") +def the_provider_state_callback_will_be_called_before_the_verification_is_run() -> None: + """ + Check that the provider state callback was called before the verification was run. + """ + logger.debug("Checking provider state callback was called before verification") + + +@then( + parsers.re( + r"the provider state callback will receive a (?Psetup|teardown) call" + r' (with )?"(?P[^"]*)" as the provider state parameter', + ), +) +def the_provider_state_callback_will_receive_a_setup_call( + temp_dir: Path, + action: str, + state: str, +) -> None: + """ + Check that the provider state callback received a setup call. + """ + logger.info("Checking provider state callback received a %s call", action) + logger.info("Callback files: %s", list(temp_dir.glob("callback.*.json"))) + for file in temp_dir.glob("callback.*.json"): + with file.open("r") as f: + data: dict[str, Any] = json.load(f) + logger.debug("Checking callback data: %s", data) + if ( + "action" in data["query_params"] + and data["query_params"]["action"] == action + and data["query_params"]["state"] == state + ): + break + else: + msg = f"No {action} call found" + raise AssertionError(msg) + + +@then( + parsers.re( + r"the provider state callback will " + r"NOT receive a (?Psetup|teardown) call" + ) +) +def the_provider_state_callback_will_not_receive_a_setup_call( + temp_dir: Path, + action: str, +) -> None: + """ + Check that the provider state callback did not receive a setup call. + """ + for file in temp_dir.glob("callback.*.json"): + with file.open("r") as f: + data: dict[str, Any] = json.load(f) + logger.debug("Checking callback data: %s", data) + if ( + "action" in data["query_params"] + and data["query_params"]["action"] == action + ): + msg = f"Unexpected {action} call found" + raise AssertionError(msg) + + +@then("the provider state callback will be called after the verification is run") +def the_provider_state_callback_will_be_called_after_the_verification_is_run() -> None: + """ + Check that the provider state callback was called after the verification was run. + """ + + +@then( + parsers.re( + r"a warning will be displayed " + r"that there was no provider state callback configured " + r'for provider state "(?P[^"]*)"', + ) +) +def a_warning_will_be_displayed_that_there_was_no_callback_configured( + verifier_result: tuple[Verifier, Exception | None], + state: str, # noqa: ARG001 +) -> None: + """ + Check that a warning was displayed that there was no callback configured. + """ + logger.debug("Checking for warning about missing provider state callback") + verifier = verifier_result[0] + logger.debug("verifier output: %s", verifier.output(strip_ansi=True)) + logger.debug("verifier results: %s", json.dumps(verifier.results, indent=2)) + msg = "Not implemented" + raise NotImplementedError(msg) + + +@then( + parsers.re( + r'the request to the provider will contain the header "(?P
[^"]+)"', + ), + converters={"header": lambda x: parse_headers(f"'{x}'")}, +) +def the_request_to_the_provider_will_contain_the_header( + verifier_result: tuple[Verifier, Exception | None], + header: dict[str, str], + temp_dir: Path, +) -> None: + """ + Check that the request to the provider contained the given header. + """ + verifier = verifier_result[0] + logger.debug("verifier output: %s", verifier.output(strip_ansi=True)) + logger.debug("verifier results: %s", json.dumps(verifier.results, indent=2)) + for request in temp_dir.glob("request.*.json"): + with request.open("r") as f: + data: dict[str, Any] = json.load(f) + if data["path"].startswith("/_test"): + continue + logger.debug("Checking request data: %s", data) + assert all([k, v] in data["headers_list"] for k, v in header.items()) + break + else: + msg = "No request found" + raise AssertionError(msg) diff --git a/tests/v3/compatibility_suite/util/pact-broker.yml b/tests/v3/compatibility_suite/util/pact-broker.yml new file mode 100644 index 000000000..53b3f6d72 --- /dev/null +++ b/tests/v3/compatibility_suite/util/pact-broker.yml @@ -0,0 +1,43 @@ +version: "3.9" + +services: + postgres: + image: postgres + ports: + - "5432:5432" + healthcheck: + test: psql postgres -U postgres --command 'SELECT 1' + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + + broker: + image: pactfoundation/pact-broker:latest + depends_on: + - postgres + ports: + - "9292:9292" + restart: always + environment: + # Basic auth credentials for the Broker + PACT_BROKER_ALLOW_PUBLIC_READ: "true" + PACT_BROKER_BASIC_AUTH_USERNAME: pactbroker + PACT_BROKER_BASIC_AUTH_PASSWORD: pactbroker + # Database + PACT_BROKER_DATABASE_URL: "postgres://postgres:postgres@postgres/postgres" + # PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite # Pending pact-foundation/pact-broker-docker#148 + + healthcheck: + test: + [ + "CMD", + "curl", + "--silent", + "--show-error", + "--fail", + "http://pactbroker:pactbroker@localhost:9292/diagnostic/status/heartbeat", + ] + interval: 1s + timeout: 2s + retries: 5 From f6f8074cca1feb950076ab31b85e49fb019e369a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Mar 2024 13:57:37 +1100 Subject: [PATCH 0287/1376] chore(tests): fixes for lower python versions Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/__init__.py | 4 ++-- tests/v3/compatibility_suite/util/provider.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 4c4240d90..50e149c73 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -173,11 +173,11 @@ def serialize(obj: Any) -> Any: # noqa: ANN401, PLR0911 All other types are converted to strings using the `repr` function. """ - if isinstance(obj, datetime | date | time): + if isinstance(obj, (datetime, date, time)): return obj.isoformat() # Basic types which are already serializable - if isinstance(obj, str | int | float | bool | type(None)): + if isinstance(obj, (str, int, float, bool, type(None))): return obj # Bytes diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 6ad1d38b3..1f7b86c09 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -23,7 +23,7 @@ import socket import subprocess from contextvars import ContextVar -from datetime import UTC, datetime +from datetime import datetime from pathlib import Path from typing import TYPE_CHECKING @@ -38,6 +38,13 @@ if TYPE_CHECKING: from tests.v3.compatibility_suite.util import InteractionDefinition +if sys.version_info < (3, 11): + from datetime import timezone + + UTC = timezone.utc +else: + from datetime import UTC + logger = logging.getLogger(__name__) From 4332f055253f95788ecdea78b0be85083fb14c99 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 20 Mar 2024 14:29:16 +1100 Subject: [PATCH 0288/1376] chore(tests): re-enable warning check Following recommendations from Ron, making the check a no-op. Ref: pact-foundation/pact-reference#404 Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/test_v1_provider.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py index f795f3ce0..359d135f9 100644 --- a/tests/v3/compatibility_suite/test_v1_provider.py +++ b/tests/v3/compatibility_suite/test_v1_provider.py @@ -126,9 +126,6 @@ def test_verifying_an_interaction_where_the_provider_state_callback_fails() -> N """Verifying an interaction where the provider state callback fails.""" -# TODO: Enable this test once we can capture warnings -# https://github.com/pact-foundation/pact-reference/issues/404 -@pytest.mark.skip("Unable to get warnings to be captured") @scenario( "definition/features/V1/http_provider.feature", "Verifying an interaction where a provider state callback is not configured", @@ -860,18 +857,13 @@ def the_provider_state_callback_will_be_called_after_the_verification_is_run() - ) ) def a_warning_will_be_displayed_that_there_was_no_callback_configured( - verifier_result: tuple[Verifier, Exception | None], - state: str, # noqa: ARG001 + state: str, ) -> None: """ Check that a warning was displayed that there was no callback configured. """ logger.debug("Checking for warning about missing provider state callback") - verifier = verifier_result[0] - logger.debug("verifier output: %s", verifier.output(strip_ansi=True)) - logger.debug("verifier results: %s", json.dumps(verifier.results, indent=2)) - msg = "Not implemented" - raise NotImplementedError(msg) + assert state @then( From 7263f95082a7395c1744f1f35dde442add40cb91 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 21 Mar 2024 15:07:07 +1100 Subject: [PATCH 0289/1376] chore(tests): improve logging from provider As the provider is launched in its own Python process, logging is not configured. So instead of using various `logging` methods, directly write to `stderr`. Signed-off-by: JP-Ellis --- .../compatibility_suite/test_v1_provider.py | 4 +-- tests/v3/compatibility_suite/util/__init__.py | 16 ++++++++-- tests/v3/compatibility_suite/util/provider.py | 30 +++++++++---------- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py index 359d135f9..9b216962d 100644 --- a/tests/v3/compatibility_suite/test_v1_provider.py +++ b/tests/v3/compatibility_suite/test_v1_provider.py @@ -420,10 +420,10 @@ def redirect() -> NoReturn: while True: if process.stdout: while line := process.stdout.readline(): - logger.debug("Provider stdout: %s", line) + logger.debug("Provider stdout: %s", line.strip()) if process.stderr: while line := process.stderr.readline(): - logger.debug("Provider stderr: %s", line) + logger.debug("Provider stderr: %s", line.strip()) thread = Thread(target=redirect, daemon=True) thread.start() diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 50e149c73..1f47080b2 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -25,6 +25,7 @@ def _(): import contextlib import hashlib import logging +import sys import typing from collections.abc import Collection, Mapping from datetime import date, datetime, time @@ -555,9 +556,18 @@ def add_to_flask(self, app: flask.Flask) -> None: app: The Flask app to add the interaction to. """ - - async def route_fn() -> flask.Response: - logger.info("Received request: %s %s", self.method, self.path) + sys.stderr.write( + f"Adding interaction to Flask app: {self.method} {self.path}\n" + ) + sys.stderr.write(f" Query: {self.query}\n") + sys.stderr.write(f" Headers: {self.headers}\n") + sys.stderr.write(f" Body: {self.body}\n") + sys.stderr.write(f" Response: {self.response}\n") + sys.stderr.write(f" Response headers: {self.response_headers}\n") + sys.stderr.write(f" Response body: {self.response_body}\n") + + def route_fn() -> flask.Response: + sys.stderr.write(f"Received request: {self.method} {self.path}\n") if self.query: query = URL.build(query_string=self.query).query # Perform a two-way check to ensure that the query parameters diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 1f7b86c09..f90c35902 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -180,14 +180,12 @@ def _add_after_request(self, app: flask.Flask) -> None: @app.after_request def log_request(response: flask.Response) -> flask.Response: - logger.debug("Request: %s %s", request.method, request.path) - logger.debug( - "Request query string: %s", request.query_string.decode("utf-8") - ) - logger.debug("Request query params: %s", serialize(request.args)) - logger.debug("Request headers: %s", serialize(request.headers)) - logger.debug("Request body: %s", request.data.decode("utf-8")) - logger.debug("Request form: %s", serialize(request.form)) + sys.stderr.write(f"START REQUEST: {request.method} {request.path}\n") + sys.stderr.write(f"Query string: {request.query_string.decode('utf-8')}\n") + sys.stderr.write(f"Header: {serialize(request.headers)}\n") + sys.stderr.write(f"Body: {request.data.decode('utf-8')}\n") + sys.stderr.write(f"Form: {serialize(request.form)}\n") + sys.stderr.write("END REQUEST\n") with ( self.provider_dir @@ -210,13 +208,13 @@ def log_request(response: flask.Response) -> flask.Response: @app.after_request def log_response(response: flask.Response) -> flask.Response: - try: - body = response.get_data(as_text=True) - except UnicodeDecodeError: - body = "" - logger.debug("Response: %s", response.status_code) - logger.debug("Response headers: %s", serialize(response.headers)) - logger.debug("Response body: %s", body) + sys.stderr.write(f"START RESPONSE: {response.status_code}\n") + sys.stderr.write(f"Headers: {serialize(response.headers)}\n") + sys.stderr.write( + f"Body: {response.get_data().decode('utf-8', errors='replace')}\n" + ) + sys.stderr.write("END RESPONSE\n") + with ( self.provider_dir / f"response.{datetime.now(tz=UTC).strftime('%H:%M:%S.%f')}.json" @@ -226,7 +224,7 @@ def log_response(response: flask.Response) -> flask.Response: "status_code": response.status_code, "headers_list": serialize(response.headers), "headers_dict": serialize(dict(response.headers)), - "body": body, + "body": response.get_data().decode("utf-8", errors="replace"), }, f, ) From 66ee152711b220caf85b5194149f139c2c52989d Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 21 Mar 2024 17:17:58 +1100 Subject: [PATCH 0290/1376] chore(test): strip authentication from url Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index f90c35902..0f4d4be62 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -315,7 +315,7 @@ def publish(self, directory: Path | str, version: str | None = None) -> None: "publish", str(directory), "--broker-base-url", - str(self.url), + str(self.url.with_user(None).with_password(None)), ] if self.username: cmd.extend(["--broker-username", self.username]) From 7ad29bd3937dd729365ea08edd593858587e272d Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 22 Mar 2024 11:05:19 +1100 Subject: [PATCH 0291/1376] chore(tests): use long-lived pact broker The initial implementation of the compatibility suite spun up and down the Pact Broker for each scenario, which also resulting in flaky tests in CI. This refactor uses a session pytest fixture which will spin up the broker once, and keep re-using it. Functionality to 'reset' the broker between tests has also been added. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 65 ++++++++++++++++-- conftest.py | 13 ++++ pyproject.toml | 3 + .../compatibility_suite/test_v1_provider.py | 68 ++++++++++++++++--- tests/v3/compatibility_suite/util/provider.py | 19 ++++++ 5 files changed, 154 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0695b7ff2..16213fb08 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,17 +18,30 @@ env: HATCH_VERBOSE: 1 jobs: - test: + test-container: name: >- Tests py${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} + services: + broker: + image: pactfoundation/pact-broker:latest@sha256:8f10947f230f661ef21f270a4abcf53214ba27cd68063db81de555fcd93e07dd + ports: + - "9292:9292" + env: + # Basic auth credentials for the Broker + PACT_BROKER_ALLOW_PUBLIC_READ: "true" + PACT_BROKER_BASIC_AUTH_USERNAME: pactbroker + PACT_BROKER_BASIC_AUTH_PASSWORD: pactbroker + # Database + PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite + strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] experimental: [false] include: @@ -51,8 +64,20 @@ jobs: - name: Install Hatch run: pip install --upgrade hatch + - name: Ensure broker is live + run: | + i=0 + until curl -sSf http://localhost:9292/diagnostic/status/heartbeat; do + i=$((i+1)) + if [ $i -gt 120 ]; then + echo "Broker failed to start" + exit 1 + fi + sleep 1 + done + - name: Run tests - run: hatch run test + run: hatch run test --broker-url=http://pactbroker:pactbroker@localhost:9292 --container - name: Upload coverage # TODO: Configure code coverage monitoring @@ -61,12 +86,44 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} + test-no-container: + name: >- + Tests py${{ matrix.python-version }} on ${{ matrix.os }} + + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental }} + + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + experimental: [false] + + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + with: + submodules: true + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install Hatch + run: pip install --upgrade hatch + + - name: Run tests + run: hatch run test + test-conlusion: name: Test matrix complete runs-on: ubuntu-latest needs: - - test + - test-container + - test-no-container steps: - run: echo "Test matrix completed successfully." diff --git a/conftest.py b/conftest.py index f6b59a455..359b5e916 100644 --- a/conftest.py +++ b/conftest.py @@ -19,3 +19,16 @@ def pytest_addoption(parser: pytest.Parser) -> None: ), type=str, ) + parser.addoption( + "--container", + action="store_true", + help="Run tests using a container", + ) + + +def pytest_runtest_setup(item: pytest.Item) -> None: + """ + Hook into the setup phase of tests. + """ + if "container" in item.keywords and not item.config.getoption("--container"): + pytest.skip("need --container to run this test") diff --git a/pyproject.toml b/pyproject.toml index 9559c6ee7..2839b5d54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -193,6 +193,9 @@ log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" log_date_format = "%H:%M:%S" markers = [ + # Marker for tests that require a container + "container", + # Markers for the compatibility suite "consumer", "provider", diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py index 9b216962d..880847d1a 100644 --- a/tests/v3/compatibility_suite/test_v1_provider.py +++ b/tests/v3/compatibility_suite/test_v1_provider.py @@ -13,9 +13,10 @@ import subprocess import sys import time +from contextvars import ContextVar from pathlib import Path from threading import Thread -from typing import Any, Generator, NoReturn +from typing import Any, Generator, NoReturn, Union import pytest import requests @@ -34,6 +35,16 @@ logger = logging.getLogger(__name__) +reset_broker_var = ContextVar("reset_broker", default=True) +""" +This context variable is used to determine whether the Pact broker should be +cleaned up. It is used to ensure that the broker is only cleaned up once, even +if a step is run multiple times. + +All scenarios which make use of the Pact broker should set this to `True` at the +start of the scenario. +""" + @pytest.fixture() def verifier() -> Verifier: @@ -41,6 +52,35 @@ def verifier() -> Verifier: return Verifier() +@pytest.fixture(scope="session") +def broker_url(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: + """ + Fixture to run the Pact broker. + + This inspects whether the `--broker-url` option has been given. If it has, + it is assumed that the broker is already running and simply returns the + given URL. + + Otherwise, the Pact broker is started in a container. The URL of the + containerised broker is then returned. + """ + broker_url: Union[str, None] = request.config.getoption("--broker-url") + + # If we have been given a broker URL, there's nothing more to do here and we + # can return early. + if broker_url: + yield URL(broker_url) + return + + with DockerCompose( + Path(__file__).parent / "util", + compose_file_name="pact-broker.yml", + pull=True, + ) as _: + yield URL("http://pactbroker:pactbroker@localhost:9292") + return + + ################################################################################ ## Scenario ################################################################################ @@ -70,36 +110,44 @@ def test_incorrect_request_is_made_to_provider() -> None: """Incorrect request is made to provider.""" +@pytest.mark.container() @scenario( "definition/features/V1/http_provider.feature", "Verifying a simple HTTP request via a Pact broker", ) def test_verifying_a_simple_http_request_via_a_pact_broker() -> None: """Verifying a simple HTTP request via a Pact broker.""" + reset_broker_var.set(True) # noqa: FBT003 +@pytest.mark.container() @scenario( "definition/features/V1/http_provider.feature", "Verifying a simple HTTP request via a Pact broker with publishing results enabled", ) def test_verifying_a_simple_http_request_via_a_pact_broker_with_publishing() -> None: """Verifying a simple HTTP request via a Pact broker with publishing.""" + reset_broker_var.set(True) # noqa: FBT003 +@pytest.mark.container() @scenario( "definition/features/V1/http_provider.feature", "Verifying multiple Pact files via a Pact broker", ) def test_verifying_multiple_pact_files_via_a_pact_broker() -> None: """Verifying multiple Pact files via a Pact broker.""" + reset_broker_var.set(True) # noqa: FBT003 +@pytest.mark.container() @scenario( "definition/features/V1/http_provider.feature", "Incorrect request is made to provider via a Pact broker", ) def test_incorrect_request_is_made_to_provider_via_a_pact_broker() -> None: """Incorrect request is made to provider via a Pact broker.""" + reset_broker_var.set(True) # noqa: FBT003 @scenario( @@ -475,6 +523,7 @@ def a_pact_file_for_interaction_is_to_be_verified( ) def a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker( interaction_definitions: dict[int, InteractionDefinition], + broker_url: URL, verifier: Verifier, interaction: int, temp_dir: Path, @@ -494,15 +543,14 @@ def a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker( pacts_dir.mkdir(exist_ok=True, parents=True) pact.write_file(pacts_dir) - with DockerCompose( - Path(__file__).parent / "util", - compose_file_name="pact-broker.yml", - pull=True, - ) as _: - pact_broker = PactBroker(URL("http://pactbroker:pactbroker@localhost:9292")) - pact_broker.publish(pacts_dir) - verifier.broker_source(pact_broker.url) - yield pact_broker + pact_broker = PactBroker(broker_url) + if reset_broker_var.get(): + logger.debug("Resetting Pact broker") + pact_broker.reset() + reset_broker_var.set(False) # noqa: FBT003 + pact_broker.publish(pacts_dir) + verifier.broker_source(pact_broker.url) + yield pact_broker @given("publishing of verification results is enabled") diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 0f4d4be62..2968152b0 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -299,6 +299,25 @@ def _install(self) -> None: msg = "pact-broker not found" raise NotImplementedError(msg) + def reset(self) -> None: + """ + Reset the Pact Broker. + + This function will reset the Pact Broker by deleting all pacts and + verification results. + """ + requests.delete( + str( + self.url + / "integrations" + / "provider" + / self.provider + / "consumer" + / self.consumer + ), + timeout=2, + ) + def publish(self, directory: Path | str, version: str | None = None) -> None: """ Publish the interactions to the Pact Broker. From 01307c650b502bd82ecbf21c881c7979a63fe61c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 22 Mar 2024 12:12:37 +1100 Subject: [PATCH 0292/1376] chore(test): apply a temporary diff to compatibility suite Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 12 +++ .../definition-update.diff | 79 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 tests/v3/compatibility_suite/definition-update.diff diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 16213fb08..7e56120ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,6 +55,12 @@ jobs: with: submodules: true + - name: Apply temporary definitions update + shell: bash + run: | + cd tests/v3/compatibility_suite + patch -p1 -d definition < definition-update.diff + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: @@ -105,6 +111,12 @@ jobs: with: submodules: true + - name: Apply temporary definitions update + shell: bash + run: | + cd tests/v3/compatibility_suite + patch -p1 -d definition < definition-update.diff + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: diff --git a/tests/v3/compatibility_suite/definition-update.diff b/tests/v3/compatibility_suite/definition-update.diff new file mode 100644 index 000000000..23538b1ab --- /dev/null +++ b/tests/v3/compatibility_suite/definition-update.diff @@ -0,0 +1,79 @@ +diff --git a/features/V1/http_provider.feature b/features/V1/http_provider.feature +index 94fda44..2838116 100644 +--- a/features/V1/http_provider.feature ++++ b/features/V1/http_provider.feature +@@ -118,16 +118,16 @@ Feature: Basic HTTP provider + + Scenario: Verifies the response status code + Given a provider is started that returns the response from interaction 1, with the following changes: +- | status | +- | 400 | ++ | response | ++ | 400 | + And a Pact file for interaction 1 is to be verified + When the verification is run + Then the verification will NOT be successful + And the verification results will contain a "Response status did not match" error + + Scenario: Verifies the response headers +- Given a provider is started that returns the response from interaction 1, with the following changes: +- | headers | ++ Given a provider is started that returns the response from interaction 5, with the following changes: ++ | response headers | + | 'X-TEST: Compatibility' | + And a Pact file for interaction 5 is to be verified + When the verification is run +@@ -142,7 +142,7 @@ Feature: Basic HTTP provider + + Scenario: Response with plain text body (negative case) + Given a provider is started that returns the response from interaction 6, with the following changes: +- | body | ++ | response body | + | Hello Compatibility Suite! | + And a Pact file for interaction 6 is to be verified + When the verification is run +@@ -157,7 +157,7 @@ Feature: Basic HTTP provider + + Scenario: Response with JSON body (negative case) + Given a provider is started that returns the response from interaction 1, with the following changes: +- | body | ++ | response body | + | JSON: { "one": 100, "two": "b" } | + And a Pact file for interaction 1 is to be verified + When the verification is run +@@ -172,7 +172,7 @@ Feature: Basic HTTP provider + + Scenario: Response with XML body (negative case) + Given a provider is started that returns the response from interaction 7, with the following changes: +- | body | ++ | response body | + | XML: A | + And a Pact file for interaction 7 is to be verified + When the verification is run +@@ -187,7 +187,7 @@ Feature: Basic HTTP provider + + Scenario: Response with binary body (negative case) + Given a provider is started that returns the response from interaction 8, with the following changes: +- | body | ++ | response body | + | file: spider.jpg | + And a Pact file for interaction 8 is to be verified + When the verification is run +@@ -202,7 +202,7 @@ Feature: Basic HTTP provider + + Scenario: Response with form post body (negative case) + Given a provider is started that returns the response from interaction 9, with the following changes: +- | body | ++ | response body | + | a=1&b=2&c=33&d=4 | + And a Pact file for interaction 9 is to be verified + When the verification is run +@@ -217,7 +217,7 @@ Feature: Basic HTTP provider + + Scenario: Response with multipart body (negative case) + Given a provider is started that returns the response from interaction 10, with the following changes: +- | body | ++ | response body | + | file: multipart2-body.xml | + And a Pact file for interaction 10 is to be verified + When the verification is run From 7edfb29258b17112130be2c80f244d6b22ba9802 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:37:50 +0000 Subject: [PATCH 0293/1376] chore(deps): update dependency devel-test/pytest-cov to v5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2839b5d54..a5ae8c3de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ devel-test = [ "pytest ~=8.0.0", "pytest-asyncio ~=0.0", "pytest-bdd ~=7.0", - "pytest-cov ~=4.0", + "pytest-cov ~=5.0", "testcontainers ~=3.0", ] devel = [ From a34c30d3e8570fc58eb333a0f4d26636e6a8e656 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 20:26:02 +0000 Subject: [PATCH 0294/1376] chore(deps): update codecov/codecov-action digest to c16abc2 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e56120ef..8dca091c1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -88,7 +88,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@54bcd8715eee62d40e33596ef5e8f0f48dbbccab # v4 + uses: codecov/codecov-action@c16abc29c95fcf9174b58eb7e1abf4c866893bc8 # v4 with: token: ${{ secrets.CODECOV_TOKEN }} From 074fd010af9c9a52b84362b9a1d54a623b48fe67 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 16:10:20 +0000 Subject: [PATCH 0295/1376] chore(deps): update actions/setup-python digest to 82c7e63 --- .github/workflows/build.yml | 4 ++-- .github/workflows/test.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26cada02b..f87f70c52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} cache: pip @@ -206,7 +206,7 @@ jobs: merge-multiple: true - name: Setup Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8dca091c1..b42b26127 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,7 +62,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 with: python-version: ${{ matrix.python-version }} cache: pip @@ -118,7 +118,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 with: python-version: ${{ matrix.python-version }} cache: pip @@ -163,7 +163,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Set up Python 3 - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} cache: pip @@ -196,7 +196,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Set up Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} cache: pip From dc7d90f028ff3c7df3c481db204d007b517627a7 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 25 Mar 2024 11:16:16 +1100 Subject: [PATCH 0296/1376] chore(test): refactor v1 bdd steps In preparation for the implementatino of the V2+ specs, refactor the V1 compatibility suite to share steps Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/conftest.py | 41 + .../compatibility_suite/test_v1_provider.py | 690 ++-------------- tests/v3/compatibility_suite/util/provider.py | 738 +++++++++++++++++- 3 files changed, 817 insertions(+), 652 deletions(-) diff --git a/tests/v3/compatibility_suite/conftest.py b/tests/v3/compatibility_suite/conftest.py index 46d3d33cb..f7f249f1e 100644 --- a/tests/v3/compatibility_suite/conftest.py +++ b/tests/v3/compatibility_suite/conftest.py @@ -7,9 +7,15 @@ import shutil import subprocess +from collections.abc import Generator from pathlib import Path +from typing import Any, Union import pytest +from testcontainers.compose import DockerCompose # type: ignore[import-untyped] +from yarl import URL + +from pact.v3.verifier import Verifier @pytest.fixture(scope="session", autouse=True) @@ -28,3 +34,38 @@ def _submodule_init() -> None: ) raise RuntimeError(msg) subprocess.check_call([git_exec, "submodule", "init"]) # noqa: S603 + + +@pytest.fixture() +def verifier() -> Verifier: + """Return a new Verifier.""" + return Verifier() + + +@pytest.fixture(scope="session") +def broker_url(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: + """ + Fixture to run the Pact broker. + + This inspects whether the `--broker-url` option has been given. If it has, + it is assumed that the broker is already running and simply returns the + given URL. + + Otherwise, the Pact broker is started in a container. The URL of the + containerised broker is then returned. + """ + broker_url: Union[str, None] = request.config.getoption("--broker-url") + + # If we have been given a broker URL, there's nothing more to do here and we + # can return early. + if broker_url: + yield URL(broker_url) + return + + with DockerCompose( + Path(__file__).parent / "util", + compose_file_name="pact-broker.yml", + pull=True, + ) as _: + yield URL("http://pactbroker:pactbroker@localhost:9292") + return diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py index 880847d1a..a317a3eba 100644 --- a/tests/v3/compatibility_suite/test_v1_provider.py +++ b/tests/v3/compatibility_suite/test_v1_provider.py @@ -4,82 +4,41 @@ from __future__ import annotations -import copy -import json import logging -import pickle -import re -import signal -import subprocess -import sys -import time -from contextvars import ContextVar -from pathlib import Path -from threading import Thread -from typing import Any, Generator, NoReturn, Union import pytest -import requests -from pytest_bdd import given, parsers, scenario, then, when -from testcontainers.compose import DockerCompose # type: ignore[import-untyped] -from yarl import URL +from pytest_bdd import given, parsers, scenario -from pact.v3.pact import Pact -from pact.v3.verifier import Verifier from tests.v3.compatibility_suite.util import ( InteractionDefinition, - parse_headers, parse_markdown_table, ) -from tests.v3.compatibility_suite.util.provider import PactBroker +from tests.v3.compatibility_suite.util.provider import ( + a_failed_verification_result_will_be_published_back, + a_pact_file_for_interaction_is_to_be_verified, + a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker, + a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state_define, + a_provider_is_started_that_returns_the_responses_from_interactions, + a_provider_is_started_that_returns_the_responses_from_interactions_with_changes, + a_provider_state_callback_is_configured, + a_request_filter_is_configured_to_make_the_following_changes, + a_successful_verification_result_will_be_published_back, + a_verification_result_will_not_be_published_back, + a_warning_will_be_displayed_that_there_was_no_callback_configured, + publishing_of_verification_results_is_enabled, + reset_broker_var, + the_provider_state_callback_will_be_called_after_the_verification_is_run, + the_provider_state_callback_will_be_called_before_the_verification_is_run, + the_provider_state_callback_will_not_receive_a_setup_call, + the_provider_state_callback_will_receive_a_setup_call, + the_request_to_the_provider_will_contain_the_header, + the_verification_is_run, + the_verification_results_will_contain_a_error, + the_verification_will_be_successful, +) logger = logging.getLogger(__name__) -reset_broker_var = ContextVar("reset_broker", default=True) -""" -This context variable is used to determine whether the Pact broker should be -cleaned up. It is used to ensure that the broker is only cleaned up once, even -if a step is run multiple times. - -All scenarios which make use of the Pact broker should set this to `True` at the -start of the scenario. -""" - - -@pytest.fixture() -def verifier() -> Verifier: - """Return a new Verifier.""" - return Verifier() - - -@pytest.fixture(scope="session") -def broker_url(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: - """ - Fixture to run the Pact broker. - - This inspects whether the `--broker-url` option has been given. If it has, - it is assumed that the broker is already running and simply returns the - given URL. - - Otherwise, the Pact broker is started in a container. The URL of the - containerised broker is then returned. - """ - broker_url: Union[str, None] = request.config.getoption("--broker-url") - - # If we have been given a broker URL, there's nothing more to do here and we - # can return early. - if broker_url: - yield URL(broker_url) - return - - with DockerCompose( - Path(__file__).parent / "util", - compose_file_name="pact-broker.yml", - pull=True, - ) as _: - yield URL("http://pactbroker:pactbroker@localhost:9292") - return - ################################################################################ ## Scenario @@ -347,311 +306,14 @@ def the_following_http_interactions_have_been_defined( return interactions -@given( - parsers.re( - r"a provider is started that returns the responses? " - r'from interactions? "?(?P[0-9, ]+)"?', - ), - converters={"interactions": lambda x: [int(i) for i in x.split(",") if i]}, - target_fixture="provider_url", -) -def a_provider_is_started_that_returns_the_responses_from_interactions( - interaction_definitions: dict[int, InteractionDefinition], - interactions: list[int], - temp_dir: Path, -) -> Generator[URL, None, None]: - """ - Start a provider that returns the responses from the given interactions. - """ - logger.debug("Starting provider for interactions %s", interactions) - - for i in interactions: - logger.debug("Interaction %d: %s", i, interaction_definitions[i]) - - with (temp_dir / "interactions.pkl").open("wb") as pkl_file: - pickle.dump([interaction_definitions[i] for i in interactions], pkl_file) - - yield from start_provider(temp_dir) - - -@given( - parsers.re( - r"a provider is started that returns the responses?" - r' from interactions? "?(?P[0-9, ]+)"?' - r" with the following changes:\n(?P.+)", - re.DOTALL, - ), - converters={ - "interactions": lambda x: [int(i) for i in x.split(",") if i], - "changes": parse_markdown_table, - }, - target_fixture="provider_url", -) -def a_provider_is_started_that_returns_the_responses_from_interactions_with_changes( - interaction_definitions: dict[int, InteractionDefinition], - interactions: list[int], - changes: list[dict[str, str]], - temp_dir: Path, -) -> Generator[URL, None, None]: - """ - Start a provider that returns the responses from the given interactions. - """ - logger.debug("Starting provider for interactions %s", interactions) - - assert len(changes) == 1, "Only one set of changes is supported" - defns: list[InteractionDefinition] = [] - for interaction in interactions: - defn = copy.deepcopy(interaction_definitions[interaction]) - defn.update(**changes[0]) - defns.append(defn) - logger.debug( - "Update interaction %d: %s", - interaction, - defn, - ) - - with (temp_dir / "interactions.pkl").open("wb") as pkl_file: - pickle.dump(defns, pkl_file) - - yield from start_provider(temp_dir) - - -def start_provider(provider_dir: str | Path) -> Generator[URL, None, None]: # noqa: C901 - """Start the provider app with the given interactions.""" - process = subprocess.Popen( - [ # noqa: S603 - sys.executable, - Path(__file__).parent / "util" / "provider.py", - str(provider_dir), - ], - cwd=Path.cwd(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="utf-8", - ) - - pattern = re.compile(r" \* Running on (?P[^ ]+)") - while True: - if process.poll() is not None: - logger.error("Provider process exited with code %d", process.returncode) - logger.error( - "Provider stdout: %s", process.stdout.read() if process.stdout else "" - ) - logger.error( - "Provider stderr: %s", process.stderr.read() if process.stderr else "" - ) - msg = f"Provider process exited with code {process.returncode}" - raise RuntimeError(msg) - if ( - process.stderr - and (line := process.stderr.readline()) - and (match := pattern.match(line)) - ): - break - time.sleep(0.1) - - url = URL(match.group("url")) - logger.debug("Provider started on %s", url) - for _ in range(50): - try: - response = requests.get(str(url / "_test" / "ping"), timeout=1) - assert response.text == "pong" - break - except (requests.RequestException, AssertionError): - time.sleep(0.1) - continue - else: - msg = "Failed to ping provider" - raise RuntimeError(msg) - - def redirect() -> NoReturn: - while True: - if process.stdout: - while line := process.stdout.readline(): - logger.debug("Provider stdout: %s", line.strip()) - if process.stderr: - while line := process.stderr.readline(): - logger.debug("Provider stderr: %s", line.strip()) - - thread = Thread(target=redirect, daemon=True) - thread.start() - - yield url - - process.send_signal(signal.SIGINT) - - -@given( - parsers.re( - r"a Pact file for interaction (?P\d+) is to be verified", - ), - converters={"interaction": int}, -) -def a_pact_file_for_interaction_is_to_be_verified( - interaction_definitions: dict[int, InteractionDefinition], - verifier: Verifier, - interaction: int, - temp_dir: Path, -) -> None: - """ - Verify the Pact file for the given interaction. - """ - logger.debug( - "Adding interaction %d to be verified: %s", - interaction, - interaction_definitions[interaction], - ) - - defn = interaction_definitions[interaction] - - pact = Pact("consumer", "provider") - pact.with_specification("V1") - defn.add_to_pact(pact, f"interaction {interaction}") - (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) - pact.write_file(temp_dir / "pacts") - - verifier.add_source(temp_dir / "pacts") - - -@given( - parsers.re( - r"a Pact file for interaction (?P\d+)" - r" is to be verified from a Pact broker", - ), - converters={"interaction": int}, - target_fixture="pact_broker", -) -def a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker( - interaction_definitions: dict[int, InteractionDefinition], - broker_url: URL, - verifier: Verifier, - interaction: int, - temp_dir: Path, -) -> Generator[PactBroker, None, None]: - """ - Verify the Pact file for the given interaction from a Pact broker. - """ - logger.debug("Adding interaction %d to be verified from a Pact broker", interaction) - - defn = interaction_definitions[interaction] - - pact = Pact("consumer", "provider") - pact.with_specification("V1") - defn.add_to_pact(pact, f"interaction {interaction}") - - pacts_dir = temp_dir / "pacts" - pacts_dir.mkdir(exist_ok=True, parents=True) - pact.write_file(pacts_dir) - - pact_broker = PactBroker(broker_url) - if reset_broker_var.get(): - logger.debug("Resetting Pact broker") - pact_broker.reset() - reset_broker_var.set(False) # noqa: FBT003 - pact_broker.publish(pacts_dir) - verifier.broker_source(pact_broker.url) - yield pact_broker - - -@given("publishing of verification results is enabled") -def publishing_of_verification_results_is_enabled(verifier: Verifier) -> None: - """ - Enable publishing of verification results. - """ - logger.debug("Publishing verification results") - - verifier.set_publish_options( - "0.0.0", - ) - - -@given( - parsers.re( - r"a provider state callback is configured" - r"(?P(, but will return a failure)?)", - ), - converters={"failure": lambda x: x != ""}, -) -def a_provider_state_callback_is_configured( - verifier: Verifier, - provider_url: URL, - temp_dir: Path, - failure: bool, # noqa: FBT001 -) -> None: - """ - Configure a provider state callback. - """ - logger.debug("Configuring provider state callback") - - if failure: - with (temp_dir / "fail_callback").open("w") as f: - f.write("true") - - verifier.set_state( - provider_url / "_test" / "callback", - teardown=True, - ) - - -@given( - parsers.re( - r"a Pact file for interaction (?P\d+) is to be verified" - r' with a provider state "(?P[^"]+)" defined', - ), - converters={"interaction": int}, -) -def a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state_define( - interaction_definitions: dict[int, InteractionDefinition], - verifier: Verifier, - interaction: int, - state: str, - temp_dir: Path, -) -> None: - """ - Verify the Pact file for the given interaction with a provider state defined. - """ - logger.debug( - "Adding interaction %d to be verified with provider state %s", - interaction, - state, - ) - - defn = interaction_definitions[interaction] - defn.state = state - - pact = Pact("consumer", "provider") - pact.with_specification("V1") - defn.add_to_pact(pact, f"interaction {interaction}") - (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) - pact.write_file(temp_dir / "pacts") - - verifier.add_source(temp_dir / "pacts") - - with (temp_dir / "provider_state").open("w") as f: - logger.debug("Writing provider state to %s", temp_dir / "provider_state") - f.write(state) - - -@given( - parsers.parse( - "a request filter is configured to make the following changes:\n{content}" - ), - converters={"content": parse_markdown_table}, -) -def a_request_filter_is_configured_to_make_the_following_changes( - content: list[dict[str, str]], - verifier: Verifier, -) -> None: - """ - Configure a request filter to make the given changes. - """ - logger.debug("Configuring request filter") - - if "headers" in content[0]: - verifier.add_custom_headers(parse_headers(content[0]["headers"]).items()) - else: - msg = "Unsupported filter type" - raise RuntimeError(msg) +a_pact_file_for_interaction_is_to_be_verified("V1") +a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker("V1") +a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state_define("V1") +a_provider_is_started_that_returns_the_responses_from_interactions() +a_provider_is_started_that_returns_the_responses_from_interactions_with_changes() +a_provider_state_callback_is_configured() +a_request_filter_is_configured_to_make_the_following_changes() +publishing_of_verification_results_is_enabled() ################################################################################ @@ -659,22 +321,7 @@ def a_request_filter_is_configured_to_make_the_following_changes( ################################################################################ -@when("the verification is run", target_fixture="verifier_result") -def the_verification_is_run( - verifier: Verifier, - provider_url: URL, -) -> tuple[Verifier, Exception | None]: - """ - Run the verification. - """ - logger.debug("Running verification on %r", verifier) - - verifier.set_info("provider", url=provider_url) - try: - verifier.verify() - except Exception as e: # noqa: BLE001 - return verifier, e - return verifier, None +the_verification_is_run() ################################################################################ @@ -682,263 +329,14 @@ def the_verification_is_run( ################################################################################ -@then( - parsers.re(r"the verification will(?P( NOT)?) be successful"), - converters={"negated": lambda x: x == " NOT"}, -) -def the_verification_will_be_successful( - verifier_result: tuple[Verifier, Exception | None], - negated: bool, # noqa: FBT001 -) -> None: - """ - Check that the verification was successful. - """ - logger.debug("Checking verification result") - logger.debug("Verifier result: %s", verifier_result) - - if negated: - assert verifier_result[1] is not None - else: - assert verifier_result[1] is None - - -@then( - parsers.re(r'the verification results will contain a "(?P[^"]+)" error'), -) -def the_verification_results_will_contain_a_error( - verifier_result: tuple[Verifier, Exception | None], error: str -) -> None: - """ - Check that the verification results contain the given error. - """ - logger.debug("Checking that verification results contain error %s", error) - - verifier = verifier_result[0] - logger.debug("Verification results: %s", json.dumps(verifier.results, indent=2)) - - if error == "Response status did not match": - mismatch_type = "StatusMismatch" - elif error == "Headers had differences": - mismatch_type = "HeaderMismatch" - elif error == "Body had differences": - mismatch_type = "BodyMismatch" - elif error == "State change request failed": - assert "One or more of the setup state change handlers has failed" in [ - error["mismatch"]["message"] for error in verifier.results["errors"] - ] - return - else: - msg = f"Unknown error type: {error}" - raise ValueError(msg) - - assert mismatch_type in [ - mismatch["type"] - for error in verifier.results["errors"] - for mismatch in error["mismatch"]["mismatches"] - ] - - -@then( - parsers.re(r"a verification result will NOT be published back"), -) -def a_verification_result_will_not_be_published_back(pact_broker: PactBroker) -> None: - """ - Check that the verification result was published back to the Pact broker. - """ - logger.debug("Checking that verification result was not published back") - - response = pact_broker.latest_verification_results() - if response: - with pytest.raises(requests.HTTPError, match="404 Client Error"): - response.raise_for_status() - - -@then( - parsers.re( - "a successful verification result " - "will be published back " - r"for interaction \{(?P\d+)\}", - ), - converters={"interaction": int}, -) -def a_successful_verification_result_will_be_published_back( - pact_broker: PactBroker, - interaction: int, -) -> None: - """ - Check that the verification result was published back to the Pact broker. - """ - logger.debug( - "Checking that verification result was published back for interaction %d", - interaction, - ) - - interaction_id = pact_broker.interaction_id(interaction) - response = pact_broker.latest_verification_results() - assert response is not None - assert response.ok - data: dict[str, Any] = response.json() - assert data["success"] - - for test_result in data["testResults"]: - if test_result["interactionId"] == interaction_id: - assert test_result["success"] - break - else: - msg = f"Interaction {interaction} not found in verification results" - raise ValueError(msg) - - -@then( - parsers.re( - "a failed verification result " - "will be published back " - r"for the interaction \{(?P\d+)\}", - ), - converters={"interaction": int}, -) -def a_failed_verification_result_will_be_published_back( - pact_broker: PactBroker, - interaction: int, -) -> None: - """ - Check that the verification result was published back to the Pact broker. - """ - logger.debug( - "Checking that failed verification result" - " was published back for interaction %d", - interaction, - ) - - interaction_id = pact_broker.interaction_id(interaction) - response = pact_broker.latest_verification_results() - assert response is not None - assert response.ok - data: dict[str, Any] = response.json() - assert not data["success"] - - for test_result in data["testResults"]: - if test_result["interactionId"] == interaction_id: - assert not test_result["success"] - break - else: - msg = f"Interaction {interaction} not found in verification results" - raise ValueError(msg) - - -@then("the provider state callback will be called before the verification is run") -def the_provider_state_callback_will_be_called_before_the_verification_is_run() -> None: - """ - Check that the provider state callback was called before the verification was run. - """ - logger.debug("Checking provider state callback was called before verification") - - -@then( - parsers.re( - r"the provider state callback will receive a (?Psetup|teardown) call" - r' (with )?"(?P[^"]*)" as the provider state parameter', - ), -) -def the_provider_state_callback_will_receive_a_setup_call( - temp_dir: Path, - action: str, - state: str, -) -> None: - """ - Check that the provider state callback received a setup call. - """ - logger.info("Checking provider state callback received a %s call", action) - logger.info("Callback files: %s", list(temp_dir.glob("callback.*.json"))) - for file in temp_dir.glob("callback.*.json"): - with file.open("r") as f: - data: dict[str, Any] = json.load(f) - logger.debug("Checking callback data: %s", data) - if ( - "action" in data["query_params"] - and data["query_params"]["action"] == action - and data["query_params"]["state"] == state - ): - break - else: - msg = f"No {action} call found" - raise AssertionError(msg) - - -@then( - parsers.re( - r"the provider state callback will " - r"NOT receive a (?Psetup|teardown) call" - ) -) -def the_provider_state_callback_will_not_receive_a_setup_call( - temp_dir: Path, - action: str, -) -> None: - """ - Check that the provider state callback did not receive a setup call. - """ - for file in temp_dir.glob("callback.*.json"): - with file.open("r") as f: - data: dict[str, Any] = json.load(f) - logger.debug("Checking callback data: %s", data) - if ( - "action" in data["query_params"] - and data["query_params"]["action"] == action - ): - msg = f"Unexpected {action} call found" - raise AssertionError(msg) - - -@then("the provider state callback will be called after the verification is run") -def the_provider_state_callback_will_be_called_after_the_verification_is_run() -> None: - """ - Check that the provider state callback was called after the verification was run. - """ - - -@then( - parsers.re( - r"a warning will be displayed " - r"that there was no provider state callback configured " - r'for provider state "(?P[^"]*)"', - ) -) -def a_warning_will_be_displayed_that_there_was_no_callback_configured( - state: str, -) -> None: - """ - Check that a warning was displayed that there was no callback configured. - """ - logger.debug("Checking for warning about missing provider state callback") - assert state - - -@then( - parsers.re( - r'the request to the provider will contain the header "(?P
[^"]+)"', - ), - converters={"header": lambda x: parse_headers(f"'{x}'")}, -) -def the_request_to_the_provider_will_contain_the_header( - verifier_result: tuple[Verifier, Exception | None], - header: dict[str, str], - temp_dir: Path, -) -> None: - """ - Check that the request to the provider contained the given header. - """ - verifier = verifier_result[0] - logger.debug("verifier output: %s", verifier.output(strip_ansi=True)) - logger.debug("verifier results: %s", json.dumps(verifier.results, indent=2)) - for request in temp_dir.glob("request.*.json"): - with request.open("r") as f: - data: dict[str, Any] = json.load(f) - if data["path"].startswith("/_test"): - continue - logger.debug("Checking request data: %s", data) - assert all([k, v] in data["headers_list"] for k, v in header.items()) - break - else: - msg = "No request found" - raise AssertionError(msg) +a_failed_verification_result_will_be_published_back() +a_successful_verification_result_will_be_published_back() +a_verification_result_will_not_be_published_back() +a_warning_will_be_displayed_that_there_was_no_callback_configured() +the_provider_state_callback_will_be_called_after_the_verification_is_run() +the_provider_state_callback_will_be_called_before_the_verification_is_run() +the_provider_state_callback_will_not_receive_a_setup_call() +the_provider_state_callback_will_receive_a_setup_call() +the_request_to_the_provider_will_contain_the_header() +the_verification_results_will_contain_a_error() +the_verification_will_be_successful() diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 2968152b0..e3b284e9d 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -1,9 +1,15 @@ """ Provider utilities for compatibility suite tests. -The main functionality provided by this module is the ability to start a -provider application with a set of interactions. Since this is done -in a subprocess, any configuration must be passed in through files. +This file has two main purposes. + +The first functionality provided by this module is the ability to start a +provider application with a set of interactions. Since this is done in a +subprocess, any configuration must be passed in through files. The process is +started with + +The second functionality provided by this module is to define some of the shared +steps for the compatibility suite tests. """ from __future__ import annotations @@ -11,31 +17,46 @@ import sys from pathlib import Path +import pytest + sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent)) import contextlib +import copy import json import logging import os import pickle +import re import shutil +import signal import socket import subprocess +import time from contextvars import ContextVar from datetime import datetime -from pathlib import Path -from typing import TYPE_CHECKING +from threading import Thread +from typing import TYPE_CHECKING, Any, NoReturn import flask import requests from flask import request +from pytest_bdd import given, parsers, then, when from yarl import URL import pact.constants # type: ignore[import-untyped] -from tests.v3.compatibility_suite.util import serialize +from pact.v3.pact import Pact +from tests.v3.compatibility_suite.util import ( + parse_headers, + parse_markdown_table, + serialize, +) if TYPE_CHECKING: + from collections.abc import Generator + + from pact.v3.verifier import Verifier from tests.v3.compatibility_suite.util import InteractionDefinition if sys.version_info < (3, 11): @@ -49,6 +70,21 @@ logger = logging.getLogger(__name__) version_var = ContextVar("version_var", default="0") +""" +Shared context variable to store the version of the consumer application. + +This is used to generate a new version for the consumer application to use when +publishing the interactions to the Pact Broker. +""" +reset_broker_var = ContextVar("reset_broker", default=True) +""" +This context variable is used to determine whether the Pact broker should be +cleaned up. It is used to ensure that the broker is only cleaned up once, even +if a step is run multiple times. + +All scenarios which make use of the Pact broker should set this to `True` at the +start of the scenario. +""" def next_version() -> str: @@ -444,3 +480,693 @@ def latest_verification_results(self) -> requests.Response | None: sys.exit(1) Provider(sys.argv[1]).run() + + +################################################################################ +## Given +################################################################################ + + +def a_provider_is_started_that_returns_the_responses_from_interactions( + stacklevel: int = 1, +) -> None: + @given( + parsers.re( + r"a provider is started that returns the responses? " + r'from interactions? "?(?P[0-9, ]+)"?', + ), + converters={"interactions": lambda x: [int(i) for i in x.split(",") if i]}, + target_fixture="provider_url", + stacklevel=stacklevel + 1, + ) + def _( + interaction_definitions: dict[int, InteractionDefinition], + interactions: list[int], + temp_dir: Path, + ) -> Generator[URL, None, None]: + """ + Start a provider that returns the responses from the given interactions. + """ + logger.debug("Starting provider for interactions %s", interactions) + + for i in interactions: + logger.debug("Interaction %d: %s", i, interaction_definitions[i]) + + with (temp_dir / "interactions.pkl").open("wb") as pkl_file: + pickle.dump([interaction_definitions[i] for i in interactions], pkl_file) + + yield from start_provider(temp_dir) + + +def a_provider_is_started_that_returns_the_responses_from_interactions_with_changes( + stacklevel: int = 1, +) -> None: + @given( + parsers.re( + r"a provider is started that returns the responses?" + r' from interactions? "?(?P[0-9, ]+)"?' + r" with the following changes:\n(?P.+)", + re.DOTALL, + ), + converters={ + "interactions": lambda x: [int(i) for i in x.split(",") if i], + "changes": parse_markdown_table, + }, + target_fixture="provider_url", + stacklevel=stacklevel + 1, + ) + def _( + interaction_definitions: dict[int, InteractionDefinition], + interactions: list[int], + changes: list[dict[str, str]], + temp_dir: Path, + ) -> Generator[URL, None, None]: + """ + Start a provider that returns the responses from the given interactions. + """ + logger.debug("Starting provider for modified interactions %s", interactions) + + assert len(changes) == 1, "Only one set of changes is supported" + defns: list[InteractionDefinition] = [] + for interaction in interactions: + defn = copy.deepcopy(interaction_definitions[interaction]) + defn.update(**changes[0]) + defns.append(defn) + logger.debug( + "Updated interaction %d: %s", + interaction, + defn, + ) + + with (temp_dir / "interactions.pkl").open("wb") as pkl_file: + pickle.dump(defns, pkl_file) + + yield from start_provider(temp_dir) + + +def start_provider(provider_dir: str | Path) -> Generator[URL, None, None]: # noqa: C901 + """Start the provider app with the given interactions.""" + process = subprocess.Popen( + [ # noqa: S603 + sys.executable, + Path(__file__), + str(provider_dir), + ], + cwd=Path.cwd(), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + + pattern = re.compile(r" \* Running on (?P[^ ]+)") + while True: + if process.poll() is not None: + logger.error("Provider process exited with code %d", process.returncode) + logger.error( + "Provider stdout: %s", process.stdout.read() if process.stdout else "" + ) + logger.error( + "Provider stderr: %s", process.stderr.read() if process.stderr else "" + ) + msg = f"Provider process exited with code {process.returncode}" + raise RuntimeError(msg) + if ( + process.stderr + and (line := process.stderr.readline()) + and (match := pattern.match(line)) + ): + break + time.sleep(0.1) + + url = URL(match.group("url")) + logger.debug("Provider started on %s", url) + for _ in range(50): + try: + response = requests.get(str(url / "_test" / "ping"), timeout=1) + assert response.text == "pong" + break + except (requests.RequestException, AssertionError): + time.sleep(0.1) + continue + else: + msg = "Failed to ping provider" + raise RuntimeError(msg) + + def redirect() -> NoReturn: + while True: + if process.stdout: + while line := process.stdout.readline(): + logger.debug("Provider stdout: %s", line.strip()) + if process.stderr: + while line := process.stderr.readline(): + logger.debug("Provider stderr: %s", line.strip()) + + thread = Thread(target=redirect, daemon=True) + thread.start() + + yield url + + process.send_signal(signal.SIGINT) + + +def a_pact_file_for_interaction_is_to_be_verified( + version: str, + stacklevel: int = 1, +) -> None: + @given( + parsers.re( + r"a Pact file for interaction (?P\d+) is to be verified", + ), + converters={"interaction": int}, + stacklevel=stacklevel + 1, + ) + def _( + interaction_definitions: dict[int, InteractionDefinition], + verifier: Verifier, + interaction: int, + temp_dir: Path, + ) -> None: + """ + Verify the Pact file for the given interaction. + """ + logger.debug( + "Adding interaction %d to be verified: %s", + interaction, + interaction_definitions[interaction], + ) + + defn = interaction_definitions[interaction] + + pact = Pact("consumer", "provider") + pact.with_specification(version) + defn.add_to_pact(pact, f"interaction {interaction}") + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + + verifier.add_source(temp_dir / "pacts") + + +def a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker( + version: str, + stacklevel: int = 1, +) -> None: + @given( + parsers.re( + r"a Pact file for interaction (?P\d+)" + r" is to be verified from a Pact broker", + ), + converters={"interaction": int}, + target_fixture="pact_broker", + stacklevel=stacklevel + 1, + ) + def _( + interaction_definitions: dict[int, InteractionDefinition], + broker_url: URL, + verifier: Verifier, + interaction: int, + temp_dir: Path, + ) -> Generator[PactBroker, None, None]: + """ + Verify the Pact file for the given interaction from a Pact broker. + """ + logger.debug( + "Adding interaction %d to be verified from a Pact broker", interaction + ) + + defn = interaction_definitions[interaction] + + pact = Pact("consumer", "provider") + pact.with_specification(version) + defn.add_to_pact(pact, f"interaction {interaction}") + + pacts_dir = temp_dir / "pacts" + pacts_dir.mkdir(exist_ok=True, parents=True) + pact.write_file(pacts_dir) + + pact_broker = PactBroker(broker_url) + if reset_broker_var.get(): + logger.debug("Resetting Pact broker") + pact_broker.reset() + reset_broker_var.set(False) # noqa: FBT003 + pact_broker.publish(pacts_dir) + verifier.broker_source(pact_broker.url) + yield pact_broker + + +def publishing_of_verification_results_is_enabled(stacklevel: int = 1) -> None: + @given("publishing of verification results is enabled", stacklevel=stacklevel + 1) + def _(verifier: Verifier) -> None: + """ + Enable publishing of verification results. + """ + logger.debug("Publishing verification results") + + verifier.set_publish_options( + "0.0.0", + ) + + +def a_provider_state_callback_is_configured( + stacklevel: int = 1, +) -> None: + @given( + parsers.re( + r"a provider state callback is configured" + r"(?P(, but will return a failure)?)", + ), + converters={"failure": lambda x: x != ""}, + stacklevel=stacklevel + 1, + ) + def _( + verifier: Verifier, + provider_url: URL, + temp_dir: Path, + failure: bool, # noqa: FBT001 + ) -> None: + """ + Configure a provider state callback. + """ + logger.debug("Configuring provider state callback") + + if failure: + with (temp_dir / "fail_callback").open("w") as f: + f.write("true") + + verifier.set_state( + provider_url / "_test" / "callback", + teardown=True, + ) + + +def a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state_define( + version: str, + stacklevel: int = 1, +) -> None: + @given( + parsers.re( + r"a Pact file for interaction (?P\d+) is to be verified" + r' with a provider state "(?P[^"]+)" defined', + ), + converters={"interaction": int}, + stacklevel=stacklevel + 1, + ) + def _( + interaction_definitions: dict[int, InteractionDefinition], + verifier: Verifier, + interaction: int, + state: str, + temp_dir: Path, + ) -> None: + """ + Verify the Pact file for the given interaction with a provider state defined. + """ + logger.debug( + "Adding interaction %d to be verified with provider state %s", + interaction, + state, + ) + + defn = interaction_definitions[interaction] + defn.state = state + + pact = Pact("consumer", "provider") + pact.with_specification(version) + defn.add_to_pact(pact, f"interaction {interaction}") + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + + verifier.add_source(temp_dir / "pacts") + + with (temp_dir / "provider_state").open("w") as f: + logger.debug("Writing provider state to %s", temp_dir / "provider_state") + f.write(state) + + +def a_request_filter_is_configured_to_make_the_following_changes( + stacklevel: int = 1, +) -> None: + @given( + parsers.parse( + "a request filter is configured to make the following changes:\n{content}" + ), + converters={"content": parse_markdown_table}, + stacklevel=stacklevel + 1, + ) + def _( + content: list[dict[str, str]], + verifier: Verifier, + ) -> None: + """ + Configure a request filter to make the given changes. + """ + logger.debug("Configuring request filter") + + if "headers" in content[0]: + verifier.add_custom_headers(parse_headers(content[0]["headers"]).items()) + else: + msg = "Unsupported filter type" + raise RuntimeError(msg) + + +################################################################################ +## When +################################################################################ + + +def the_verification_is_run( + stacklevel: int = 1, +) -> None: + @when( + "the verification is run", + target_fixture="verifier_result", + stacklevel=stacklevel + 1, + ) + def _( + verifier: Verifier, + provider_url: URL, + ) -> tuple[Verifier, Exception | None]: + """ + Run the verification. + """ + logger.debug("Running verification on %r", verifier) + + verifier.set_info("provider", url=provider_url) + try: + verifier.verify() + except Exception as e: # noqa: BLE001 + return verifier, e + return verifier, None + + +################################################################################ +## Then +################################################################################ + + +def the_verification_will_be_successful( + stacklevel: int = 1, +) -> None: + @then( + parsers.re(r"the verification will(?P( NOT)?) be successful"), + converters={"negated": lambda x: x == " NOT"}, + stacklevel=stacklevel + 1, + ) + def _( + verifier_result: tuple[Verifier, Exception | None], + negated: bool, # noqa: FBT001 + ) -> None: + """ + Check that the verification was successful. + """ + logger.debug("Checking verification result") + logger.debug("Verifier result: %s", verifier_result) + + if negated: + assert verifier_result[1] is not None + else: + assert verifier_result[1] is None + + +def the_verification_results_will_contain_a_error( + stacklevel: int = 1, +) -> None: + @then( + parsers.re(r'the verification results will contain a "(?P[^"]+)" error'), + stacklevel=stacklevel + 1, + ) + def _(verifier_result: tuple[Verifier, Exception | None], error: str) -> None: + """ + Check that the verification results contain the given error. + """ + logger.debug("Checking that verification results contain error %s", error) + + verifier = verifier_result[0] + logger.debug("Verification results: %s", json.dumps(verifier.results, indent=2)) + + if error == "Response status did not match": + mismatch_type = "StatusMismatch" + elif error == "Headers had differences": + mismatch_type = "HeaderMismatch" + elif error == "Body had differences": + mismatch_type = "BodyMismatch" + elif error == "State change request failed": + assert "One or more of the setup state change handlers has failed" in [ + error["mismatch"]["message"] for error in verifier.results["errors"] + ] + return + else: + msg = f"Unknown error type: {error}" + raise ValueError(msg) + + assert mismatch_type in [ + mismatch["type"] + for error in verifier.results["errors"] + for mismatch in error["mismatch"]["mismatches"] + ] + + +def a_verification_result_will_not_be_published_back( + stacklevel: int = 1, +) -> None: + @then( + parsers.re(r"a verification result will NOT be published back"), + stacklevel=stacklevel + 1, + ) + def _(pact_broker: PactBroker) -> None: + """ + Check that the verification result was published back to the Pact broker. + """ + logger.debug("Checking that verification result was not published back") + + response = pact_broker.latest_verification_results() + if response: + with pytest.raises(requests.HTTPError, match="404 Client Error"): + response.raise_for_status() + + +def a_successful_verification_result_will_be_published_back( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + "a successful verification result " + "will be published back " + r"for interaction \{(?P\d+)\}", + ), + converters={"interaction": int}, + stacklevel=stacklevel + 1, + ) + def _( + pact_broker: PactBroker, + interaction: int, + ) -> None: + """ + Check that the verification result was published back to the Pact broker. + """ + logger.debug( + "Checking that verification result was published back for interaction %d", + interaction, + ) + + interaction_id = pact_broker.interaction_id(interaction) + response = pact_broker.latest_verification_results() + assert response is not None + assert response.ok + data: dict[str, Any] = response.json() + assert data["success"] + + for test_result in data["testResults"]: + if test_result["interactionId"] == interaction_id: + assert test_result["success"] + break + else: + msg = f"Interaction {interaction} not found in verification results" + raise ValueError(msg) + + +def a_failed_verification_result_will_be_published_back( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + "a failed verification result " + "will be published back " + r"for the interaction \{(?P\d+)\}", + ), + converters={"interaction": int}, + stacklevel=stacklevel + 1, + ) + def _( + pact_broker: PactBroker, + interaction: int, + ) -> None: + """ + Check that the verification result was published back to the Pact broker. + """ + logger.debug( + "Checking that failed verification result" + " was published back for interaction %d", + interaction, + ) + + interaction_id = pact_broker.interaction_id(interaction) + response = pact_broker.latest_verification_results() + assert response is not None + assert response.ok + data: dict[str, Any] = response.json() + assert not data["success"] + + for test_result in data["testResults"]: + if test_result["interactionId"] == interaction_id: + assert not test_result["success"] + break + else: + msg = f"Interaction {interaction} not found in verification results" + raise ValueError(msg) + + +def the_provider_state_callback_will_be_called_before_the_verification_is_run( + stacklevel: int = 1, +) -> None: + @then( + "the provider state callback will be called before the verification is run", + stacklevel=stacklevel + 1, + ) + def _() -> None: + """ + Check that the provider state callback was called before the verification. + """ + logger.debug("Checking provider state callback was called before verification") + + +def the_provider_state_callback_will_receive_a_setup_call( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + r"the provider state callback" + r" will receive a (?Psetup|teardown) call" + r' (with )?"(?P[^"]*)" as the provider state parameter', + ), + stacklevel=stacklevel + 1, + ) + def _( + temp_dir: Path, + action: str, + state: str, + ) -> None: + """ + Check that the provider state callback received a setup call. + """ + logger.info("Checking provider state callback received a %s call", action) + logger.info("Callback files: %s", list(temp_dir.glob("callback.*.json"))) + for file in temp_dir.glob("callback.*.json"): + with file.open("r") as f: + data: dict[str, Any] = json.load(f) + logger.debug("Checking callback data: %s", data) + if ( + "action" in data["query_params"] + and data["query_params"]["action"] == action + and data["query_params"]["state"] == state + ): + break + else: + msg = f"No {action} call found" + raise AssertionError(msg) + + +def the_provider_state_callback_will_not_receive_a_setup_call( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + r"the provider state callback will " + r"NOT receive a (?Psetup|teardown) call" + ), + stacklevel=stacklevel + 1, + ) + def _( + temp_dir: Path, + action: str, + ) -> None: + """ + Check that the provider state callback did not receive a setup call. + """ + for file in temp_dir.glob("callback.*.json"): + with file.open("r") as f: + data: dict[str, Any] = json.load(f) + logger.debug("Checking callback data: %s", data) + if ( + "action" in data["query_params"] + and data["query_params"]["action"] == action + ): + msg = f"Unexpected {action} call found" + raise AssertionError(msg) + + +def the_provider_state_callback_will_be_called_after_the_verification_is_run( + stacklevel: int = 1, +) -> None: + @then( + "the provider state callback will be called after the verification is run", + stacklevel=stacklevel + 1, + ) + def _() -> None: + """ + Check that the provider state callback was called after the verification. + """ + + +def a_warning_will_be_displayed_that_there_was_no_callback_configured( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + r"a warning will be displayed" + r" that there was no provider state callback configured" + r' for provider state "(?P[^"]*)"', + ), + stacklevel=stacklevel + 1, + ) + def _( + state: str, + ) -> None: + """ + Check that a warning was displayed that there was no callback configured. + """ + logger.debug("Checking for warning about missing provider state callback") + assert state + + +def the_request_to_the_provider_will_contain_the_header( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + r'the request to the provider will contain the header "(?P
[^"]+)"', + ), + converters={"header": lambda x: parse_headers(f"'{x}'")}, + stacklevel=stacklevel + 1, + ) + def _( + verifier_result: tuple[Verifier, Exception | None], + header: dict[str, str], + temp_dir: Path, + ) -> None: + """ + Check that the request to the provider contained the given header. + """ + verifier = verifier_result[0] + logger.debug("verifier output: %s", verifier.output(strip_ansi=True)) + logger.debug("verifier results: %s", json.dumps(verifier.results, indent=2)) + for request_path in temp_dir.glob("request.*.json"): + with request_path.open("r") as f: + data: dict[str, Any] = json.load(f) + if data["path"].startswith("/_test"): + continue + logger.debug("Checking request data: %s", data) + assert all([k, v] in data["headers_list"] for k, v in header.items()) + break + else: + msg = "No request found" + raise AssertionError(msg) From 8b1857e933fe0a77e81450d1af6833c50bf202f8 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Mar 2024 12:31:40 +1100 Subject: [PATCH 0297/1376] chore(test): fix misspelling in step name Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/test_v1_provider.py | 4 ++-- tests/v3/compatibility_suite/util/provider.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py index a317a3eba..bdcca16fc 100644 --- a/tests/v3/compatibility_suite/test_v1_provider.py +++ b/tests/v3/compatibility_suite/test_v1_provider.py @@ -17,7 +17,7 @@ a_failed_verification_result_will_be_published_back, a_pact_file_for_interaction_is_to_be_verified, a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker, - a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state_define, + a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state_defined, a_provider_is_started_that_returns_the_responses_from_interactions, a_provider_is_started_that_returns_the_responses_from_interactions_with_changes, a_provider_state_callback_is_configured, @@ -308,7 +308,7 @@ def the_following_http_interactions_have_been_defined( a_pact_file_for_interaction_is_to_be_verified("V1") a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker("V1") -a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state_define("V1") +a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state_defined("V1") a_provider_is_started_that_returns_the_responses_from_interactions() a_provider_is_started_that_returns_the_responses_from_interactions_with_changes() a_provider_state_callback_is_configured() diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index e3b284e9d..66f7e06a0 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -758,7 +758,7 @@ def _( ) -def a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state_define( +def a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state_defined( version: str, stacklevel: int = 1, ) -> None: From 356d552355a8681ed18797d243993f80ac8ac048 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Mar 2024 13:55:57 +1100 Subject: [PATCH 0298/1376] chore(tests): improve logging Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/__init__.py | 18 +++++++++--------- tests/v3/compatibility_suite/util/provider.py | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 1f47080b2..10a1efebc 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -473,7 +473,7 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912 Name for this interaction. Must be unique for the pact. """ interaction = pact.upon_receiving(name) - logging.info("with_request(%s, %s)", self.method, self.path) + logger.info("with_request(%s, %s)", self.method, self.path) interaction.with_request(self.method, self.path) # We distinguish between "" and None here. @@ -483,16 +483,16 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912 if self.query: query = URL.build(query_string=self.query).query - logging.info("with_query_parameters(%s)", query.items()) + logger.info("with_query_parameters(%s)", query.items()) interaction.with_query_parameters(query.items()) if self.headers: - logging.info("with_headers(%s)", self.headers.items()) + logger.info("with_headers(%s)", self.headers.items()) interaction.with_headers(self.headers.items()) if self.body: if self.body.string: - logging.info( + logger.info( "with_body(%s, %s)", truncate(self.body.string), self.body.mime_type, @@ -502,7 +502,7 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912 self.body.mime_type, ) elif self.body.bytes: - logging.info( + logger.info( "with_binary_file(%s, %s)", truncate(self.body.bytes), self.body.mime_type, @@ -516,16 +516,16 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912 raise RuntimeError(msg) if self.matching_rules: - logging.info("with_matching_rules(%s)", self.matching_rules) + logger.info("with_matching_rules(%s)", self.matching_rules) interaction.with_matching_rules(self.matching_rules) if self.response: - logging.info("will_respond_with(%s)", self.response) + logger.info("will_respond_with(%s)", self.response) interaction.will_respond_with(self.response) if self.response_body: if self.response_body.string: - logging.info( + logger.info( "with_body(%s, %s)", truncate(self.response_body.string), self.response_body.mime_type, @@ -535,7 +535,7 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912 self.response_body.mime_type, ) elif self.response_body.bytes: - logging.info( + logger.info( "with_binary_file(%s, %s)", truncate(self.response_body.bytes), self.response_body.mime_type, diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 66f7e06a0..571e3ed7e 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -34,6 +34,7 @@ import socket import subprocess import time +import warnings from contextvars import ContextVar from datetime import datetime from threading import Thread @@ -281,6 +282,10 @@ def run(self) -> None: Start the provider. """ url = URL(f"http://localhost:{_find_free_port()}") + sys.stderr.write("Starting provider on %s\n" % url) + for endpoint in self.app.url_map.iter_rules(): + sys.stderr.write(f" * {endpoint}\n") + self.app.run( host=url.host, port=url.port, @@ -918,11 +923,19 @@ def _(verifier_result: tuple[Verifier, Exception | None], error: str) -> None: msg = f"Unknown error type: {error}" raise ValueError(msg) - assert mismatch_type in [ + mismatch_types = [ mismatch["type"] for error in verifier.results["errors"] for mismatch in error["mismatch"]["mismatches"] ] + assert mismatch_type in mismatch_types + if len(mismatch_types) > 1: + warnings.warn( + f"Multiple mismatch types found: {mismatch_types}", stacklevel=1 + ) + for error in verifier.results["errors"]: + for mismatch in error["mismatch"]["mismatches"]: + warnings.warn(f"Mismatch: {mismatch}", stacklevel=1) def a_verification_result_will_not_be_published_back( From 2b55ef486ab320133b311b3dd2e4745f6758ef4e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Mar 2024 13:59:54 +1100 Subject: [PATCH 0299/1376] chore(tests): allow multiple states with parameters Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/__init__.py | 60 +++++++++++++++++-- tests/v3/compatibility_suite/util/provider.py | 27 ++++++--- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 10a1efebc..db9101110 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -24,13 +24,14 @@ def _(): import base64 import contextlib import hashlib +import json import logging import sys import typing from collections.abc import Collection, Mapping from datetime import date, datetime, time from pathlib import Path -from typing import Any +from typing import Any, Self from xml.etree import ElementTree import flask @@ -358,10 +359,54 @@ def parse_file(self, file: Path) -> None: msg = "Unknown file type" raise ValueError(msg) + class State: + """ + Provider state. + """ + + def __init__( + self, + name: str, + parameters: str | dict[str, Any] | None = None, + ) -> None: + """ + Instantiate the provider state. + """ + self.name = name + self.parameters: dict[str, Any] + if isinstance(parameters, str): + self.parameters = json.loads(parameters) + else: + self.parameters = parameters or {} + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return "".format( + ", ".join( + str(k) + "=" + truncate(repr(v)) for k, v in vars(self).items() + ), + ) + + def as_dict(self) -> dict[str, str | dict[str, Any]]: + """ + Convert the provider state to a dictionary. + """ + return {"name": self.name, "parameters": self.parameters} + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Self: + """ + Convert a dictionary to a provider state. + """ + return cls(**data) + def __init__(self, **kwargs: str) -> None: """Initialise the interaction definition.""" self.id: int | None = None - self.state: str | None = None + + self.states: list[InteractionDefinition.State] = [] self.method: str = kwargs.pop("method") self.path: str = kwargs.pop("path") self.response: int = int(kwargs.pop("response", 200)) @@ -476,10 +521,13 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912 logger.info("with_request(%s, %s)", self.method, self.path) interaction.with_request(self.method, self.path) - # We distinguish between "" and None here. - if self.state is not None: - logging.info("given(%s)", self.state) - interaction.given(self.state) + for state in self.states or []: + if state.parameters: + logger.info("given(%s, parameters=%s)", state.name, state.parameters) + interaction.given(state.name, parameters=state.parameters) + else: + logger.info("given(%s)", state.name) + interaction.given(state.name) if self.query: query = URL.build(query_string=self.query).query diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 571e3ed7e..952c8b0da 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -49,6 +49,7 @@ import pact.constants # type: ignore[import-untyped] from pact.v3.pact import Pact from tests.v3.compatibility_suite.util import ( + InteractionDefinition, parse_headers, parse_markdown_table, serialize, @@ -58,7 +59,6 @@ from collections.abc import Generator from pact.v3.verifier import Verifier - from tests.v3.compatibility_suite.util import InteractionDefinition if sys.version_info < (3, 11): from datetime import timezone @@ -181,10 +181,19 @@ def callback() -> tuple[str, int] | str: if (self.provider_dir / "fail_callback").exists(): return "Provider state not found", 404 - provider_state_path = self.provider_dir / "provider_state" - if provider_state_path.exists(): - state = provider_state_path.read_text() - assert request.args["state"] == state + provider_states_path = self.provider_dir / "provider_states" + if provider_states_path.exists(): + with provider_states_path.open() as f: + states = [InteractionDefinition.State(**s) for s in json.load(f)] + for state in states: + if request.args["state"] == state.name: + for k, v in state.parameters.items(): + assert k in request.args + assert str(request.args[k]) == str(v) + break + else: + msg = "State not found" + raise ValueError(msg) json_file = ( self.provider_dir @@ -792,7 +801,7 @@ def _( ) defn = interaction_definitions[interaction] - defn.state = state + defn.states = [InteractionDefinition.State(state)] pact = Pact("consumer", "provider") pact.with_specification(version) @@ -802,9 +811,9 @@ def _( verifier.add_source(temp_dir / "pacts") - with (temp_dir / "provider_state").open("w") as f: - logger.debug("Writing provider state to %s", temp_dir / "provider_state") - f.write(state) + with (temp_dir / "provider_states").open("w") as f: + logger.debug("Writing provider state to %s", temp_dir / "provider_states") + json.dump([s.as_dict() for s in defn.states], f) def a_request_filter_is_configured_to_make_the_following_changes( From 7c10f659ae3c25cc8f059df3d6553d7559848ce3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Mar 2024 14:48:01 +1100 Subject: [PATCH 0300/1376] chore(tests): implement http provider compatibility suite Signed-off-by: JP-Ellis --- .../definition-update.diff | 54 ++++ .../compatibility_suite/test_v2_provider.py | 131 +++++++++ .../compatibility_suite/test_v3_provider.py | 117 ++++++++ .../compatibility_suite/test_v4_provider.py | 123 +++++++++ tests/v3/compatibility_suite/util/__init__.py | 42 ++- tests/v3/compatibility_suite/util/provider.py | 253 +++++++++++++++++- 6 files changed, 714 insertions(+), 6 deletions(-) create mode 100644 tests/v3/compatibility_suite/test_v2_provider.py create mode 100644 tests/v3/compatibility_suite/test_v3_provider.py create mode 100644 tests/v3/compatibility_suite/test_v4_provider.py diff --git a/tests/v3/compatibility_suite/definition-update.diff b/tests/v3/compatibility_suite/definition-update.diff index 23538b1ab..9b4854304 100644 --- a/tests/v3/compatibility_suite/definition-update.diff +++ b/tests/v3/compatibility_suite/definition-update.diff @@ -77,3 +77,57 @@ index 94fda44..2838116 100644 | file: multipart2-body.xml | And a Pact file for interaction 10 is to be verified When the verification is run +diff --git a/features/V2/http_provider.feature b/features/V2/http_provider.feature +index d51df8b..57c58e7 100644 +--- a/features/V2/http_provider.feature ++++ b/features/V2/http_provider.feature +@@ -10,15 +10,15 @@ Feature: Basic HTTP provider + + Scenario: Supports matching rules for the response headers (positive case) + Given a provider is started that returns the response from interaction 1, with the following changes: +- | headers | +- | 'X-TEST: 1000' | ++ | response headers | ++ | 'X-TEST: 1000' | + And a Pact file for interaction 1 is to be verified + When the verification is run + Then the verification will be successful + + Scenario: Supports matching rules for the response headers (negative case) + Given a provider is started that returns the response from interaction 1, with the following changes: +- | headers | ++ | response headers | + | 'X-TEST: 123ABC' | + And a Pact file for interaction 1 is to be verified + When the verification is run +@@ -27,7 +27,7 @@ Feature: Basic HTTP provider + + Scenario: Verifies the response body (positive case) + Given a provider is started that returns the response from interaction 2, with the following changes: +- | body | ++ | response body | + | JSON: { "one": "100", "two": "b" } | + And a Pact file for interaction 2 is to be verified + When the verification is run +@@ -35,7 +35,7 @@ Feature: Basic HTTP provider + + Scenario: Verifies the response body (negative case) + Given a provider is started that returns the response from interaction 2, with the following changes: +- | body | ++ | response body | + | JSON: { "one": 100, "two": "b" } | + And a Pact file for interaction 2 is to be verified + When the verification is run +diff --git a/features/V4/http_provider.feature b/features/V4/http_provider.feature +index be3d1ff..8e15a13 100644 +--- a/features/V4/http_provider.feature ++++ b/features/V4/http_provider.feature +@@ -9,7 +9,7 @@ Feature: HTTP provider + + Scenario: Verifying a pending HTTP interaction + Given a provider is started that returns the response from interaction 1, with the following changes: +- | body | ++ | response body | + | file: basic2.json | + And a Pact file for interaction 1 is to be verified, but is marked pending + When the verification is run diff --git a/tests/v3/compatibility_suite/test_v2_provider.py b/tests/v3/compatibility_suite/test_v2_provider.py new file mode 100644 index 000000000..303425aa5 --- /dev/null +++ b/tests/v3/compatibility_suite/test_v2_provider.py @@ -0,0 +1,131 @@ +""" +Basic HTTP provider feature test. +""" + +from __future__ import annotations + +import logging + +from pytest_bdd import given, parsers, scenario + +from tests.v3.compatibility_suite.util import ( + InteractionDefinition, + parse_markdown_table, +) +from tests.v3.compatibility_suite.util.provider import ( + a_pact_file_for_interaction_is_to_be_verified, + a_provider_is_started_that_returns_the_responses_from_interactions_with_changes, + the_verification_is_run, + the_verification_results_will_contain_a_error, + the_verification_will_be_successful, +) + +logger = logging.getLogger(__name__) + + +################################################################################ +## Scenario +################################################################################ + + +@scenario( + "definition/features/V2/http_provider.feature", + "Supports matching rules for the response headers (positive case)", +) +def test_supports_matching_rules_for_the_response_headers_positive_case() -> None: + """ + Supports matching rules for the response headers (positive case). + """ + + +@scenario( + "definition/features/V2/http_provider.feature", + "Supports matching rules for the response headers (negative case)", +) +def test_supports_matching_rules_for_the_response_headers_negative_case() -> None: + """ + Supports matching rules for the response headers (negative case). + """ + + +@scenario( + "definition/features/V2/http_provider.feature", + "Verifies the response body (positive case)", +) +def test_verifies_the_response_body_positive_case() -> None: + """ + Verifies the response body (positive case). + """ + + +@scenario( + "definition/features/V2/http_provider.feature", + "Verifies the response body (negative case)", +) +def test_verifies_the_response_body_negative_case() -> None: + """ + Verifies the response body (negative case). + """ + + +################################################################################ +## Given +################################################################################ + + +@given( + parsers.parse("the following HTTP interactions have been defined:\n{content}"), + target_fixture="interaction_definitions", + converters={"content": parse_markdown_table}, +) +def the_following_http_interactions_have_been_defined( + content: list[dict[str, str]], +) -> dict[int, InteractionDefinition]: + """ + Parse the HTTP interactions table into a dictionary. + + The table columns are expected to be: + + - No + - method + - path + - response + - response headers + - response content + - response body + - response matching rules + + The first row is ignored, as it is assumed to be the column headers. The + order of the columns is similarly ignored. + """ + logger.debug("Parsing interaction definitions") + + # Check that the table is well-formed + assert len(content[0]) == 8, f"Expected 8 columns, got {len(content[0])}" + assert "No" in content[0], "'No' column not found" + + # Parse the table into a more useful format + interactions: dict[int, InteractionDefinition] = {} + for row in content: + interactions[int(row["No"])] = InteractionDefinition(**row) + return interactions + + +a_pact_file_for_interaction_is_to_be_verified("V2") +a_provider_is_started_that_returns_the_responses_from_interactions_with_changes() + +################################################################################ +## When +################################################################################ + + +the_verification_is_run() + + +################################################################################ +## Then +################################################################################ + + +the_verification_results_will_contain_a_error() +the_verification_will_be_successful() diff --git a/tests/v3/compatibility_suite/test_v3_provider.py b/tests/v3/compatibility_suite/test_v3_provider.py new file mode 100644 index 000000000..c702d3e9c --- /dev/null +++ b/tests/v3/compatibility_suite/test_v3_provider.py @@ -0,0 +1,117 @@ +""" +Basic HTTP provider feature test. +""" + +from __future__ import annotations + +import logging + +from pytest_bdd import given, parsers, scenario + +from tests.v3.compatibility_suite.util import ( + InteractionDefinition, + parse_markdown_table, +) +from tests.v3.compatibility_suite.util.provider import ( + a_pact_file_for_interaction_is_to_be_verified_with_a_provider_states_defined, + a_provider_is_started_that_returns_the_responses_from_interactions, + a_provider_state_callback_is_configured, + the_provider_state_callback_will_be_called_after_the_verification_is_run, + the_provider_state_callback_will_be_called_before_the_verification_is_run, + the_provider_state_callback_will_receive_a_setup_call, + the_provider_state_callback_will_receive_a_setup_call_with_parameters, + the_verification_is_run, +) + +logger = logging.getLogger(__name__) + + +################################################################################ +## Scenario +################################################################################ + + +@scenario( + "definition/features/V3/http_provider.feature", + "Verifying an interaction with multiple defined provider states", +) +def test_verifying_an_interaction_with_multiple_defined_provider_states() -> None: + """ + Verifying an interaction with multiple defined provider states. + """ + + +@scenario( + "definition/features/V3/http_provider.feature", + "Verifying an interaction with a provider state with parameters", +) +def test_verifying_an_interaction_with_a_provider_state_with_parameters() -> None: + """ + Verifying an interaction with a provider state with parameters. + """ + + +################################################################################ +## Given +################################################################################ + + +@given( + parsers.parse("the following HTTP interactions have been defined:\n{content}"), + target_fixture="interaction_definitions", + converters={"content": parse_markdown_table}, +) +def the_following_http_interactions_have_been_defined( + content: list[dict[str, str]], +) -> dict[int, InteractionDefinition]: + """ + Parse the HTTP interactions table into a dictionary. + + The table columns are expected to be: + + - No + - method + - path + - response + - response headers + - response content + - response body + - response matching rules + + The first row is ignored, as it is assumed to be the column headers. The + order of the columns is similarly ignored. + """ + logger.debug("Parsing interaction definitions") + + # Check that the table is well-formed + assert len(content[0]) == 8, f"Expected 8 columns, got {len(content[0])}" + assert "No" in content[0], "'No' column not found" + + # Parse the table into a more useful format + interactions: dict[int, InteractionDefinition] = {} + for row in content: + interactions[int(row["No"])] = InteractionDefinition(**row) + return interactions + + +a_pact_file_for_interaction_is_to_be_verified_with_a_provider_states_defined("V3") +a_provider_is_started_that_returns_the_responses_from_interactions() +a_provider_state_callback_is_configured() + +################################################################################ +## When +################################################################################ + + +the_verification_is_run() + + +################################################################################ +## Then +################################################################################ + + +the_provider_state_callback_will_be_called_after_the_verification_is_run() +the_provider_state_callback_will_be_called_before_the_verification_is_run() +the_provider_state_callback_will_receive_a_setup_call() +the_provider_state_callback_will_receive_a_setup_call_with_parameters() diff --git a/tests/v3/compatibility_suite/test_v4_provider.py b/tests/v3/compatibility_suite/test_v4_provider.py new file mode 100644 index 000000000..8d4463d81 --- /dev/null +++ b/tests/v3/compatibility_suite/test_v4_provider.py @@ -0,0 +1,123 @@ +""" +Basic HTTP provider feature test. +""" + +from __future__ import annotations + +import logging + +from pytest_bdd import given, parsers, scenario + +from tests.v3.compatibility_suite.util import ( + InteractionDefinition, + parse_markdown_table, +) +from tests.v3.compatibility_suite.util.provider import ( + a_pact_file_for_interaction_is_to_be_verified, + a_pact_file_for_interaction_is_to_be_verified_with_comments, + a_provider_is_started_that_returns_the_responses_from_interactions, + a_provider_is_started_that_returns_the_responses_from_interactions_with_changes, + the_comment_will_have_been_printed_to_the_console, + the_name_of_the_test_will_be_displayed_as_the_original_test_name, + the_verification_is_run, + the_verification_results_will_contain_a_error, + the_verification_will_be_successful, + there_will_be_a_pending_error, +) + +logger = logging.getLogger(__name__) + + +################################################################################ +## Scenario +################################################################################ + + +@scenario( + "definition/features/V4/http_provider.feature", + "Verifying a pending HTTP interaction", +) +def test_verifying_a_pending_http_interaction() -> None: + """ + Verifying a pending HTTP interaction. + """ + + +@scenario( + "definition/features/V4/http_provider.feature", + "Verifying a HTTP interaction with comments", +) +def test_verifying_a_http_interaction_with_comments() -> None: + """ + Verifying a HTTP interaction with comments. + """ + + +################################################################################ +## Given +################################################################################ + + +@given( + parsers.parse("the following HTTP interactions have been defined:\n{content}"), + target_fixture="interaction_definitions", + converters={"content": parse_markdown_table}, +) +def the_following_http_interactions_have_been_defined( + content: list[dict[str, str]], +) -> dict[int, InteractionDefinition]: + """ + Parse the HTTP interactions table into a dictionary. + + The table columns are expected to be: + + - No + - method + - path + - query + - headers + - body + - response + - response headers + - response content + - response body + + The first row is ignored, as it is assumed to be the column headers. The + order of the columns is similarly ignored. + """ + logger.debug("Parsing interaction definitions") + + # Check that the table is well-formed + assert len(content[0]) == 10, f"Expected 10 columns, got {len(content[0])}" + assert "No" in content[0], "'No' column not found" + + # Parse the table into a more useful format + interactions: dict[int, InteractionDefinition] = {} + for row in content: + interactions[int(row["No"])] = InteractionDefinition(**row) + return interactions + + +a_pact_file_for_interaction_is_to_be_verified("V4") +a_pact_file_for_interaction_is_to_be_verified_with_comments("V4") +a_provider_is_started_that_returns_the_responses_from_interactions() +a_provider_is_started_that_returns_the_responses_from_interactions_with_changes() + +################################################################################ +## When +################################################################################ + + +the_verification_is_run() + + +################################################################################ +## Then +################################################################################ + + +the_comment_will_have_been_printed_to_the_console() +the_name_of_the_test_will_be_displayed_as_the_original_test_name() +the_verification_results_will_contain_a_error() +the_verification_will_be_successful() +there_will_be_a_pending_error() diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index db9101110..57d1df6f1 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -407,15 +407,23 @@ def __init__(self, **kwargs: str) -> None: self.id: int | None = None self.states: list[InteractionDefinition.State] = [] + self.pending: bool = False + self.text_comments: list[str] = [] + self.comments: dict[str, str] = {} + self.test_name: str | None = None + self.method: str = kwargs.pop("method") self.path: str = kwargs.pop("path") self.response: int = int(kwargs.pop("response", 200)) self.query: str | None = None self.headers: MultiDict[str] = MultiDict() self.body: InteractionDefinition.Body | None = None + self.response_headers: MultiDict[str] = MultiDict() self.response_body: InteractionDefinition.Body | None = None self.matching_rules: str | None = None + self.response_matching_rules: str | None = None + self.update(**kwargs) def update(self, **kwargs: str) -> None: # noqa: C901, PLR0912 @@ -490,6 +498,12 @@ def update(self, **kwargs: str) -> None: # noqa: C901, PLR0912 ): self.matching_rules = parse_matching_rules(matching_rules) + if matching_rules := ( + kwargs.pop("response_matching_rules", None) + or kwargs.pop("response matching rules", None) + ): + self.response_matching_rules = parse_matching_rules(matching_rules) + if len(kwargs) > 0: msg = f"Unexpected arguments: {kwargs.keys()}" raise TypeError(msg) @@ -502,7 +516,7 @@ def __repr__(self) -> str: ", ".join(f"{k}={v!r}" for k, v in vars(self).items()), ) - def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912 + def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912, PLR0915 """ Add the interaction to the pact. @@ -529,6 +543,22 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912 logger.info("given(%s)", state.name) interaction.given(state.name) + if self.pending: + logger.info("set_pending(True)") + interaction.set_pending(pending=True) + + if self.text_comments: + logger.info("set_comment(text, %s)", self.text_comments) + interaction.set_comment("text", self.text_comments) + + for key, value in self.comments.items(): + logger.info("set_comment(%s, %s)", key, value) + interaction.set_comment(key, value) + + if self.test_name: + logger.info("test_name(%s)", self.test_name) + interaction.test_name(self.test_name) + if self.query: query = URL.build(query_string=self.query).query logger.info("with_query_parameters(%s)", query.items()) @@ -571,6 +601,10 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912 logger.info("will_respond_with(%s)", self.response) interaction.will_respond_with(self.response) + if self.response_headers: + logger.info("with_headers(%s)", self.response_headers) + interaction.with_headers(self.response_headers.items()) + if self.response_body: if self.response_body.string: logger.info( @@ -596,6 +630,10 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912 msg = "Unexpected body definition" raise RuntimeError(msg) + if self.response_matching_rules: + logger.info("with_matching_rules(%s)", self.response_matching_rules) + interaction.with_matching_rules(self.response_matching_rules) + def add_to_flask(self, app: flask.Flask) -> None: """ Add an interaction to a Flask app. @@ -642,7 +680,7 @@ def route_fn() -> flask.Response: if self.response_body else None, status=self.response, - headers=self.response_headers, + headers=dict(**self.response_headers), content_type=self.response_body.mime_type if self.response_body else None, diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 952c8b0da..d060b72f7 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -649,15 +649,17 @@ def a_pact_file_for_interaction_is_to_be_verified( ) -> None: @given( parsers.re( - r"a Pact file for interaction (?P\d+) is to be verified", + r"a Pact file for interaction (?P\d+) is to be verified" + r"(?P(, but is marked pending)?)", ), - converters={"interaction": int}, + converters={"interaction": int, "pending": lambda x: x != ""}, stacklevel=stacklevel + 1, ) def _( interaction_definitions: dict[int, InteractionDefinition], verifier: Verifier, interaction: int, + pending: bool, # noqa: FBT001 temp_dir: Path, ) -> None: """ @@ -670,6 +672,62 @@ def _( ) defn = interaction_definitions[interaction] + defn.pending = pending + + pact = Pact("consumer", "provider") + pact.with_specification(version) + defn.add_to_pact(pact, f"interaction {interaction}") + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + + with (temp_dir / "pacts" / "consumer-provider.json").open( + "r", + encoding="utf-8", + ) as f: + for line in f: + logger.debug("Pact file: %s", line.rstrip()) + + verifier.add_source(temp_dir / "pacts") + + +def a_pact_file_for_interaction_is_to_be_verified_with_comments( + version: str, + stacklevel: int = 1, +) -> None: + @given( + parsers.re( + r"a Pact file for interaction (?P\d+) is to be verified" + r" with the following comments:\n(?P.+)", + re.DOTALL, + ), + converters={"interaction": int, "comments": parse_markdown_table}, + stacklevel=stacklevel + 1, + ) + def _( + interaction_definitions: dict[int, InteractionDefinition], + verifier: Verifier, + interaction: int, + comments: list[dict[str, str]], + temp_dir: Path, + ) -> None: + """ + Verify the Pact file for the given interaction. + """ + logger.debug( + "Adding interaction %d to be verified: %s", + interaction, + interaction_definitions[interaction], + ) + + defn = interaction_definitions[interaction] + for comment in comments: + if comment["type"] == "text": + defn.text_comments.append(comment["comment"]) + elif comment["type"] == "testname": + defn.test_name = comment["comment"] + else: + defn.comments[comment["type"]] = comment["comment"] + logger.info("Updated interaction %d: %s", interaction, defn) pact = Pact("consumer", "provider") pact.with_specification(version) @@ -677,6 +735,13 @@ def _( (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) pact.write_file(temp_dir / "pacts") + with (temp_dir / "pacts" / "consumer-provider.json").open( + "r", + encoding="utf-8", + ) as f: + for line in f: + logger.debug("Pact file: %s", line.rstrip()) + verifier.add_source(temp_dir / "pacts") @@ -816,6 +881,54 @@ def _( json.dump([s.as_dict() for s in defn.states], f) +def a_pact_file_for_interaction_is_to_be_verified_with_a_provider_states_defined( + version: str, + stacklevel: int = 1, +) -> None: + @given( + parsers.re( + r"a Pact file for interaction (?P\d+) is to be verified" + r" with the following provider states defined:\n(?P.+)", + re.DOTALL, + ), + converters={"interaction": int, "states": parse_markdown_table}, + stacklevel=stacklevel + 1, + ) + def _( + interaction_definitions: dict[int, InteractionDefinition], + verifier: Verifier, + interaction: int, + states: list[dict[str, Any]], + temp_dir: Path, + ) -> None: + """ + Verify the Pact file for the given interaction with provider states defined. + """ + logger.debug( + "Adding interaction %d to be verified with provider states %s", + interaction, + states, + ) + + defn = interaction_definitions[interaction] + defn.states = [ + InteractionDefinition.State(s["State Name"], s.get("Parameters", None)) + for s in states + ] + + pact = Pact("consumer", "provider") + pact.with_specification(version) + defn.add_to_pact(pact, f"interaction {interaction}") + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + + verifier.add_source(temp_dir / "pacts") + + with (temp_dir / "provider_states").open("w") as f: + logger.debug("Writing provider state to %s", temp_dir / "provider_states") + json.dump([s.as_dict() for s in defn.states], f) + + def a_request_filter_is_configured_to_make_the_following_changes( stacklevel: int = 1, ) -> None: @@ -942,8 +1055,8 @@ def _(verifier_result: tuple[Verifier, Exception | None], error: str) -> None: warnings.warn( f"Multiple mismatch types found: {mismatch_types}", stacklevel=1 ) - for error in verifier.results["errors"]: - for mismatch in error["mismatch"]["mismatches"]: + for verifier_error in verifier.results["errors"]: + for mismatch in verifier_error["mismatch"]["mismatches"]: warnings.warn(f"Mismatch: {mismatch}", stacklevel=1) @@ -1097,6 +1210,55 @@ def _( raise AssertionError(msg) +def the_provider_state_callback_will_receive_a_setup_call_with_parameters( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + r"the provider state callback" + r" will receive a (?Psetup|teardown) call" + r' (with )?"(?P[^"]*)"' + r" and the following parameters:\n(?P.+)", + re.DOTALL, + ), + converters={"parameters": parse_markdown_table}, + stacklevel=stacklevel + 1, + ) + def _( + temp_dir: Path, + action: str, + state: str, + parameters: list[dict[str, str]], + ) -> None: + """ + Check that the provider state callback received a setup call. + """ + logger.info("Checking provider state callback received a %s call", action) + logger.info("Callback files: %s", list(temp_dir.glob("callback.*.json"))) + params: dict[str, str] = parameters[0] + # If we have a string that looks quoted, unquote it + for key, value in params.items(): + if value.startswith('"') and value.endswith('"'): + params[key] = value[1:-1] + + for file in temp_dir.glob("callback.*.json"): + with file.open("r") as f: + data: dict[str, Any] = json.load(f) + logger.debug("Checking callback data: %s", data) + if ( + "action" in data["query_params"] + and data["query_params"]["action"] == action + and data["query_params"]["state"] == state + ): + for key, value in params.items(): + assert key in data["query_params"], f"Parameter {key} not found" + assert data["query_params"][key] == value + break + else: + msg = f"No {action} call found" + raise AssertionError(msg) + + def the_provider_state_callback_will_not_receive_a_setup_call( stacklevel: int = 1, ) -> None: @@ -1192,3 +1354,86 @@ def _( else: msg = "No request found" raise AssertionError(msg) + + +def there_will_be_a_pending_error( + stacklevel: int = 1, +) -> None: + @then( + parsers.re(r'there will be a pending "(?P[^"]+)" error'), + stacklevel=stacklevel + 1, + ) + def _( + error: str, + verifier_result: tuple[Verifier, Exception | None], + ) -> None: + """ + There will be a pending error. + """ + logger.debug("Checking for pending error") + verifier, err = verifier_result + + if error == "Body had differences": + mismatch = "BodyMismatch" + else: + msg = f"Unknown error type: {error}" + raise ValueError(msg) + + assert err is None + assert "pendingErrors" in verifier.results + for verifier_error in verifier.results["pendingErrors"]: + mismatches = [m["type"] for m in verifier_error["mismatch"]["mismatches"]] + if mismatch in mismatches: + if len(mismatches) > 1: + warnings.warn( + f"Multiple mismatch types found: {mismatches}", + stacklevel=2, + ) + break + else: + msg = "Pending error not found" + raise AssertionError(msg) + + +def the_comment_will_have_been_printed_to_the_console(stacklevel: int = 1) -> None: + @then( + parsers.re( + r'the comment "(?P[^"]+)" will have been printed to the console' + ), + stacklevel=stacklevel + 1, + ) + def _( + comment: str, + verifier_result: tuple[Verifier, Exception | None], + ) -> None: + """ + Check that the given comment was printed to the console. + """ + verifier, err = verifier_result + logger.debug("Checking for comment %r in verifier output", comment) + logger.debug("Verifier output: %s", verifier.output(strip_ansi=True)) + assert err is None + assert comment in verifier.output(strip_ansi=True) + + +def the_name_of_the_test_will_be_displayed_as_the_original_test_name( + stacklevel: int = 1, +) -> None: + @then( + parsers.re( + r'the "(?P[^"]+)" will displayed as the original test name' + ), + stacklevel=stacklevel + 1, + ) + def _( + test_name: str, + verifier_result: tuple[Verifier, Exception | None], + ) -> None: + """ + Check that the given test name was displayed as the original test name. + """ + verifier, err = verifier_result + logger.debug("Checking for test name %r in verifier output", test_name) + logger.debug("Verifier output: %s", verifier.output(strip_ansi=True)) + assert err is None + assert test_name in verifier.output(strip_ansi=True) From c271ed4354299dfb07dd81ae05127a8a96902255 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Mar 2024 15:01:56 +1100 Subject: [PATCH 0301/1376] chore(tests): fix compatibility with py38 Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/conftest.py | 3 +-- tests/v3/compatibility_suite/util/__init__.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/v3/compatibility_suite/conftest.py b/tests/v3/compatibility_suite/conftest.py index f7f249f1e..e5446e1e5 100644 --- a/tests/v3/compatibility_suite/conftest.py +++ b/tests/v3/compatibility_suite/conftest.py @@ -7,9 +7,8 @@ import shutil import subprocess -from collections.abc import Generator from pathlib import Path -from typing import Any, Union +from typing import Any, Generator, Union import pytest from testcontainers.compose import DockerCompose # type: ignore[import-untyped] diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 57d1df6f1..1351b1146 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -31,12 +31,13 @@ def _(): from collections.abc import Collection, Mapping from datetime import date, datetime, time from pathlib import Path -from typing import Any, Self +from typing import Any from xml.etree import ElementTree import flask from flask import request from multidict import MultiDict +from typing_extensions import Self from yarl import URL if typing.TYPE_CHECKING: From 79e6cc6256c84322d8eabf546beec5674a7581f0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 18:37:39 +0000 Subject: [PATCH 0302/1376] chore(deps): update dependency devel/ruff to v0.3.5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a5ae8c3de..419c28664 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ devel-test = [ ] devel = [ "pact-python[devel-types,devel-test]", - "ruff ==0.3.4" + "ruff ==0.3.5" ] ################################################################################ From 5e52872b46fd6f959e958c130ebbe08ef8e11bbc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 18:37:43 +0000 Subject: [PATCH 0303/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.3.5 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d6b8d69b7..5cf986857 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.3.5 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From a77b29c47c56e1bc57ee6cd695b7fe0d2845e69a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 30 Mar 2024 11:27:08 +0000 Subject: [PATCH 0304/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.21.3 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5cf986857..df3d91c55 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.20.0 + rev: v3.21.3 hooks: - id: commitizen stages: [commit-msg] From af388eda438e6457fab7a54b882610aab3cb1f00 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 00:07:20 +0000 Subject: [PATCH 0305/1376] chore(deps): update codecov/codecov-action digest to 7afa10e --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b42b26127..eb247019b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -88,7 +88,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@c16abc29c95fcf9174b58eb7e1abf4c866893bc8 # v4 + uses: codecov/codecov-action@7afa10ed9b269c561c2336fd862446844e0cbf71 # v4 with: token: ${{ secrets.CODECOV_TOKEN }} From 48f476c99b488a246fdf55aafeb4f025bfe9a97c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Apr 2024 22:39:59 +0000 Subject: [PATCH 0306/1376] chore(deps): update pre-commit hook pre-commit/pre-commit-hooks to v4.6.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index df3d91c55..7a2cc8b7c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ default_install_hook_types: repos: # Generic hooks that apply to a lot of files - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-case-conflict From dd633fc4a2d639dccf2ee35a9fe525c820153575 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 6 Oct 2023 14:13:34 +1100 Subject: [PATCH 0307/1376] docs: setup mkdocs MkDocs is a popular tool in the Python ecosystem to generate documentation. It has support for parsing Python docstrings in order to generate API documentation quite easily. Signed-off-by: JP-Ellis --- .github/workflows/docs.yml | 56 +++++++++++++++ .markdownlint.yml | 2 + .pre-commit-config.yaml | 4 ++ docs/SUMMARY.md | 7 ++ docs/scripts/markdown.py | 62 ++++++++++++++++ docs/scripts/other.py | 117 ++++++++++++++++++++++++++++++ docs/scripts/python.py | 70 ++++++++++++++++++ docs/scripts/ruff.toml | 6 ++ mkdocs.yml | 143 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 17 +++-- 10 files changed, 478 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/SUMMARY.md create mode 100644 docs/scripts/markdown.py create mode 100644 docs/scripts/other.py create mode 100644 docs/scripts/python.py create mode 100644 docs/scripts/ruff.toml create mode 100644 mkdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..0092fad92 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,56 @@ +name: docs + +on: + push: + +env: + STABLE_PYTHON_VERSION: "3.12" + PYTEST_ADDOPTS: --color=yes + +jobs: + build: + name: Build docs + + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.STABLE_PYTHON_VERSION }} + cache: pip + + - name: Install Hatch + run: pip install --upgrade hatch + + - name: Build docs + run: | + hatch run mkdocs build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + path: site + + publish: + name: Publish docs + + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/.markdownlint.yml b/.markdownlint.yml index 738e6fc13..5c9eb105d 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -5,3 +5,5 @@ list-marker-space: ol_single: 2 ol_multi: 2 line-length: false +ul-indent: + indent: 4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a2cc8b7c..e1bc7b104 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,10 @@ repos: - id: check-toml - id: check-xml - id: check-yaml + exclude: | + (?x)^( + mkdocs.yml + )$ - repo: https://gitlab.com/bmares/check-json5 rev: v1.0.0 diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 000000000..d71e6061e --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,7 @@ + + +- [Home](README.md) + - [Changelog](CHANGELOG.md) + - [Contributing](CONTRIBUTING.md) +- [Pact](pact/) +- [Examples](examples/) diff --git a/docs/scripts/markdown.py b/docs/scripts/markdown.py new file mode 100644 index 000000000..8333f437f --- /dev/null +++ b/docs/scripts/markdown.py @@ -0,0 +1,62 @@ +""" +Script to merge Markdown documentation from the main codebase into the docs. + +This script is run by mkdocs-gen-files when the documentation is built and +imports Markdown documentation from the main codebase so that it can be included +in the documentation site. For example, a Markdown file located at +`some/path/foo.md` will be treated as if it was located at +`docs/some/path/foo.md` without the need for symlinks or copying the file. + +If the destination file already exists (either because it is a real file, or was +otherwise already generated), the script will raise a RuntimeError. +""" + +import subprocess +import sys +from pathlib import Path + +import mkdocs_gen_files +from mkdocs_gen_files.editor import FilesEditor + +EDITOR = FilesEditor.current() + +# These paths are relative to the project root, *not* the current file. +SRC_ROOT = "." +DOCS_DEST = "." + +# List of all files version controlled files in the SRC_ROOT +ALL_FILES = sorted( + map( + Path, + subprocess.check_output(["git", "ls-files", SRC_ROOT]) # noqa: S603, S607 + .decode("utf-8") + .splitlines(), + ), +) + + +for source_path in filter(lambda p: p.suffix == ".md", ALL_FILES): + if source_path.parts[0] == "docs": + continue + dest_path = Path(DOCS_DEST, source_path) + + if str(dest_path) in EDITOR.files: + print( # noqa: T201 + f"Unable to copy {source_path} to {dest_path} because the file already" + " exists at the destination.", + file=sys.stderr, + ) + msg = f"File {dest_path} already exists." + raise RuntimeError(msg) + + with Path(source_path).open("r", encoding="utf-8") as fi, mkdocs_gen_files.open( + dest_path, + "w", + encoding="utf-8", + ) as fd: + fd.write(fi.read()) + + mkdocs_gen_files.set_edit_path( + dest_path, + f"https://github.com/pact-foundation/pact-python/edit/develop/{source_path}", + ) diff --git a/docs/scripts/other.py b/docs/scripts/other.py new file mode 100644 index 000000000..e6f16148e --- /dev/null +++ b/docs/scripts/other.py @@ -0,0 +1,117 @@ +""" +Create placeholder files for all other files in the codebase. + +This script is run by mkdocs-gen-files when the documentation is built and +creates placeholder files for all other files in the codebase. This is done so +that the documentation site can link to all files in the codebase, even if they +aren't part of the documentation proper. + +If the files are binary, they are copied as-is (e.g. for images), otherwise a +HTML redirect is created. + +If the destination file already exists (either because it is a real file, or was +otherwise already generated), the script will ignore the current file and +continue silently. +""" + +import subprocess +from pathlib import Path +from typing import TYPE_CHECKING + +import mkdocs_gen_files +from mkdocs_gen_files.editor import FilesEditor + +if TYPE_CHECKING: + import io + +EDITOR = FilesEditor.current() + +# These paths are relative to the project root, *not* the current file. +SRC_ROOT = "." +DOCS_DEST = "." + +# List of all files version controlled files in the SRC_ROOT +ALL_FILES = sorted( + map( + Path, + subprocess.check_output(["git", "ls-files", SRC_ROOT]) # noqa: S603, S607 + .decode("utf-8") + .splitlines(), + ), +) + + +def is_binary(buffer: bytes) -> bool: + """ + Determine whether the given buffer is binary or not. + + The check is done by attempting to decode the buffer as UTF-8. If this + succeeds, the buffer is not binary. If it fails, the buffer is binary. + + The entire buffer will be checked, therefore if checking whether a file is + binary, only the start of the file should be passed. + + Args: + buffer: + The buffer to check. + + Returns: + True if the buffer is binary, False otherwise. + """ + try: + buffer.decode("utf-8") + except UnicodeDecodeError: + return True + else: + return False + + +for source_path in ALL_FILES: + if not source_path.is_file(): + continue + if source_path.parts[0] in ["docs"]: + continue + + dest_path = Path(DOCS_DEST, source_path) + + if str(dest_path) in EDITOR.files: + continue + + fi: "io.IOBase" + with Path(source_path).open("rb") as fi: + buf = fi.read(2048) + + if is_binary(buf): + if source_path.stat().st_size < 16 * 2**20: + # Copy the file only if it's less than 16MB. + with Path(source_path).open("rb") as fi, mkdocs_gen_files.open( + dest_path, + "wb", + ) as fd: + fd.write(fi.read()) + else: + # File is too big, create a redirect. + url = ( + "https://github.com" + "/pact-foundation/pact-python" + "/raw" + "/develop" + f"/{source_path}" + ) + with mkdocs_gen_files.open(dest_path, "w", encoding="utf-8") as fd: + fd.write(f'') + fd.write(f"# Redirecting to {url}...") + fd.write(f"[Click here if you are not redirected]({url})") + + mkdocs_gen_files.set_edit_path( + dest_path, + f"https://github.com/pact-foundation/pact-python/edit/develop/{source_path}", + ) + + else: + with Path(source_path).open("r", encoding="utf-8") as fi, mkdocs_gen_files.open( + dest_path, + "w", + encoding="utf-8", + ) as fd: + fd.write(fi.read()) diff --git a/docs/scripts/python.py b/docs/scripts/python.py new file mode 100644 index 000000000..7ee193e2e --- /dev/null +++ b/docs/scripts/python.py @@ -0,0 +1,70 @@ +""" +Script used by mkdocs-gen-files to generate documentation for Pact Python. + +The script is run by mkdocs-gen-files when the documentation is built in order +to generate documentation from Python docstrings. +""" + +import subprocess +from pathlib import Path +from typing import Union + +import mkdocs_gen_files + + +def process_python(src: str, dest: Union[str, None] = None) -> None: + """ + Process the Python files in the given directory. + + The source directory is relative to the root of the repository, and only + Python files which are version controlled are processed. The generated + documentation may optionally written to a different directory. + """ + dest = dest or src + + # List of all files version controlled files in the SRC_ROOT + files = sorted( + map( + Path, + subprocess.check_output(["git", "ls-files", src]) # noqa: S603, S607 + .decode("utf-8") + .splitlines(), + ), + ) + files = sorted(filter(lambda p: p.suffix == ".py", files)) + + for source_path in files: + module_path = source_path.relative_to(src).with_suffix("") + doc_path = source_path.relative_to(src).with_suffix(".md") + full_doc_path = Path(dest, doc_path) + + parts = [src, *module_path.parts] + + # Skip __main__ modules + if parts[-1] == "__main__": + continue + + # The __init__ modules are implicit in the directory structure. + if parts[-1] == "__init__": + parts = parts[:-1] + full_doc_path = full_doc_path.parent / "README.md" + + if full_doc_path.exists(): + with mkdocs_gen_files.open(full_doc_path, "a", encoding="utf-8") as fd: + python_identifier = ".".join(parts) + print("# " + parts[-1], file=fd) + print("::: " + python_identifier, file=fd) + else: + with mkdocs_gen_files.open(full_doc_path, "w", encoding="utf-8") as fd: + python_identifier = ".".join(parts) + print("# " + parts[-1], file=fd) + print("::: " + python_identifier, file=fd) + + mkdocs_gen_files.set_edit_path( + full_doc_path, + f"https://github.com/pact-foundation/pact-python/edit/master/pact/{module_path}.py", + ) + + +process_python("pact") +process_python("examples") diff --git a/docs/scripts/ruff.toml b/docs/scripts/ruff.toml new file mode 100644 index 000000000..7a981ad6f --- /dev/null +++ b/docs/scripts/ruff.toml @@ -0,0 +1,6 @@ +extend = "../../pyproject.toml" + +[lint] +ignore = [ + "INP001", # Forbid implicit namespaces +] diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000..dcd1a51fa --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,143 @@ +site_name: Pact Python +site_url: https://pact-foundation.github.io/pact-python/ + +repo_name: pact-foundation/pact-python +repo_url: https://github.com/pact-foundation/pact-python + +edit_uri: edit/develop/docs + +plugins: + - search + - literate-nav: + nav_file: SUMMARY.md + - section-index + # Library documentation + - gen-files: + scripts: + - docs/scripts/markdown.py + - docs/scripts/python.py + # - docs/scripts/other.py + - mkdocstrings: + default_handler: python + enable_inventory: true + handlers: + python: + import: + - https://docs.python.org/3/objects.inv + options: + # General + allow_inspection: true + show_source: true + show_bases: true + # Headings + heading_level: 2 + show_root_heading: false + show_root_toc_entry: true + show_root_full_path: true + show_root_members_full_path: false + show_object_full_path: false + show_category_heading: true + # Members + filters: + - "!^_" + - "!^__" + group_by_category: true + show_submodules: false + # Docstrings + docstring_style: google + docstring_options: + ignore_init_summary: true + docstring_section_style: spacy + merge_init_into_class: true + show_if_no_docstring: true + # Signature + annotations_path: brief + show_signature: true + show_signature_annotations: true + +markdown_extensions: + # Python Markdown + - abbr + - admonition + - attr_list + - def_list + - footnotes + - meta + - md_in_html + - tables + - toc: + permalink: true + + # Python Markdown Extensions + - pymdownx.arithmatex: + generic: true + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.mark + - pymdownx.pathconverter: + absolute: false + - pymdownx.smartsymbols + - pymdownx.snippets + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + +copyright: Copyright © 2023 Pact Foundation + +theme: + name: material + + icon: + repo: fontawesome/brands/github + + features: + - content.tooltips + - navigation.indexes + - navigation.instant + - navigation.sections + - navigation.tracking + - navigation.tabs + - navigation.top + - search.highlight + - search.share + - search.suggest + + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: cyan + accent: cyan + toggle: + icon: material/weather-sunny + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: cyan + accent: cyan + toggle: + icon: material/weather-night + name: Switch to system preference +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/pact-foundation/pact-python diff --git a/pyproject.toml b/pyproject.toml index 419c28664..98c99e83e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,14 @@ devel-types = [ "types-cffi ~=1.0", "types-requests ~=2.0", ] +devel-docs = [ + "mkdocs ~= 1.5", + "mkdocs-material ~= 9.4", + "mkdocs_gen_files ~= 0.5", + "mkdocs-literate-nav ~= 0.6", + "mkdocs-section-index ~= 0.3", + "mkdocstrings[python] ~= 0.23", +] devel-test = [ "aiohttp[speedups] ~=3.0", "coverage[toml] ~=7.0", @@ -81,10 +89,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = [ - "pact-python[devel-types,devel-test]", - "ruff ==0.3.5" -] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.3.5"] ################################################################################ ## Hatch Build Configuration @@ -188,8 +193,8 @@ filterwarnings = [ "ignore::PendingDeprecationWarning:tests", ] -log_level = "NOTSET" -log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" +log_level = "NOTSET" +log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" log_date_format = "%H:%M:%S" markers = [ From b1e700d5581745c086f8ca5f51b79e5e3eb45e1c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 23 Nov 2023 09:20:23 +1100 Subject: [PATCH 0308/1376] docs: update README Signed-off-by: JP-Ellis --- README.md | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 47d0a1a71..6b86fb340 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,35 @@ -# pact-python - -[![slack](https://slack.pact.io/badge.svg)](https://slack.pact.io) -[![License](https://img.shields.io/github/license/pact-foundation/pact-python.svg?maxAge=2592000)](https://github.com/pact-foundation/pact-python/blob/master/LICENSE) -[![Build and Test](https://github.com/pact-foundation/pact-python/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/pact-foundation/pact-python/actions/workflows/build_and_test.yml) +# Pact Python + + +
+ + + + + + + + + + + + +
Package + Version + Python Versions + Downloads +
CI/CD + CI - Test + CI - Build + CI - Docs +
Meta + Hatch project + linting - Ruff + code style - Ruff + types - Mypy + License +
+ Python version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project. Currently supports version 2 of the [Pact specification]. From 02133950b2d60886c2b78c20c67b356d11288ef5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 28 Mar 2024 12:07:50 +1100 Subject: [PATCH 0309/1376] chore(docs): update emoji indices/generators The emoji extensions have been incorporated directly into mkdocs-material. Signed-off-by: JP-Ellis --- mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index dcd1a51fa..520dd0199 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -76,8 +76,8 @@ markdown_extensions: - pymdownx.caret - pymdownx.details - pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.highlight: anchor_linenums: true - pymdownx.inlinehilite From ac1980f87c3aceeef5db7a2abadf82fb3e01bafd Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 28 Mar 2024 12:18:38 +1100 Subject: [PATCH 0310/1376] docs: rework mkdocs-gen-files scripts The scripts are significantly improved to make them easier to use and maintain. Signed-off-by: JP-Ellis --- docs/scripts/markdown.py | 133 ++++++++++++++++++------ docs/scripts/python.py | 217 +++++++++++++++++++++++++++++++-------- 2 files changed, 275 insertions(+), 75 deletions(-) diff --git a/docs/scripts/markdown.py b/docs/scripts/markdown.py index 8333f437f..5f81ab8ca 100644 --- a/docs/scripts/markdown.py +++ b/docs/scripts/markdown.py @@ -11,52 +11,119 @@ otherwise already generated), the script will raise a RuntimeError. """ +from __future__ import annotations + import subprocess import sys from pathlib import Path +from typing import TYPE_CHECKING, TypeVar import mkdocs_gen_files from mkdocs_gen_files.editor import FilesEditor +from pathspec import PathSpec + +if TYPE_CHECKING: + from collections.abc import Sequence EDITOR = FilesEditor.current() +_T = TypeVar("_T") + + +def is_subsequence(a: Sequence[_T], b: Sequence[_T]) -> int | None: + """ + Checks if a is a sublist of b. + + This will return the index of the first element of a in b if a is a sublist + of b, or None otherwise. + """ + if len(a) > len(b): + return None + for i in range(len(b) - len(a) + 1): + if all(a[j] == b[i + j] for j in range(len(a))): + return i + return None + + +def process_markdown( + src: str, + ignore: list[str] | None = None, + mapping: list[tuple[str, str]] | None = None, +) -> None: + """ + Process out-of-docs Markdown files. -# These paths are relative to the project root, *not* the current file. -SRC_ROOT = "." -DOCS_DEST = "." + The source directory is relative to the root of the repository, and only + Markdown files which are version controlled are processed. Once processed, + they will be available to MkDocs as if they were located in the `docs` + directory. -# List of all files version controlled files in the SRC_ROOT -ALL_FILES = sorted( - map( - Path, - subprocess.check_output(["git", "ls-files", SRC_ROOT]) # noqa: S603, S607 + Args: + src: + The source directory to process. + + ignore: + A list of patterns to ignore. This uses the same syntax as `.gitignore`. + + mapping: + List of tuples containing the source and destination paths to map. + Note that the list is processed in order, with later mappings + applied after earlier mappings. + """ + ignore_spec = PathSpec.from_lines("gitwildmatch", ignore or []) + mapping_parts: list[tuple[Sequence[str], Sequence[str]]] = [ + (Path(a).parts, Path(b).parts) for a, b in mapping or [] + ] + files = sorted( + Path(p) + for p in subprocess.check_output( + ["git", "ls-files", src], # noqa: S603, S607 + ) .decode("utf-8") - .splitlines(), - ), -) + .splitlines() + if p.endswith(".md") and not ignore_spec.match_file(p) + ) + for file in files: + file_parts: list[str] = list(file.parts) + for from_parts, to_parts in mapping_parts: + idx = is_subsequence(from_parts, file_parts) + if idx is not None: + file_parts = [ + *file_parts[:idx], + *to_parts, + *file_parts[idx + len(from_parts) :], + ] + destination = Path(*file_parts) -for source_path in filter(lambda p: p.suffix == ".md", ALL_FILES): - if source_path.parts[0] == "docs": - continue - dest_path = Path(DOCS_DEST, source_path) + if str(destination) in EDITOR.files: + print( # noqa: T201 + f"Unable to copy {file} to {destination} because the file already" + " exists at the destination.", + file=sys.stderr, + ) + msg = f"File {destination} already exists." + raise RuntimeError(msg) - if str(dest_path) in EDITOR.files: - print( # noqa: T201 - f"Unable to copy {source_path} to {dest_path} because the file already" - " exists at the destination.", - file=sys.stderr, + with ( + Path(file).open("r", encoding="utf-8") as fi, + mkdocs_gen_files.open( + destination, + "w", + encoding="utf-8", + ) as fd, + ): + fd.write(fi.read()) + + mkdocs_gen_files.set_edit_path( + destination, + f"https://github.com/pact-foundation/pact-python/edit/master/{file}", ) - msg = f"File {dest_path} already exists." - raise RuntimeError(msg) - - with Path(source_path).open("r", encoding="utf-8") as fi, mkdocs_gen_files.open( - dest_path, - "w", - encoding="utf-8", - ) as fd: - fd.write(fi.read()) - - mkdocs_gen_files.set_edit_path( - dest_path, - f"https://github.com/pact-foundation/pact-python/edit/develop/{source_path}", + + +if __name__ == "": + process_markdown( + ".", + ignore=[ + "docs", + ], ) diff --git a/docs/scripts/python.py b/docs/scripts/python.py index 7ee193e2e..2796c0988 100644 --- a/docs/scripts/python.py +++ b/docs/scripts/python.py @@ -1,70 +1,203 @@ """ -Script used by mkdocs-gen-files to generate documentation for Pact Python. +Script used by mkdocs-gen-files to generate documentation from Python. The script is run by mkdocs-gen-files when the documentation is built in order to generate documentation from Python docstrings. """ +from __future__ import annotations + import subprocess from pathlib import Path -from typing import Union +from typing import TYPE_CHECKING, TypeVar import mkdocs_gen_files +from pathspec import PathSpec + +if TYPE_CHECKING: + from collections.abc import Sequence + +_T = TypeVar("_T") + + +def is_subsequence(a: Sequence[_T], b: Sequence[_T]) -> int | None: + """ + Checks if a is a sublist of b. + + This will return the index of the first element of a in b if a is a sublist + of b, or None otherwise. + """ + if len(a) > len(b): + return None + for i in range(len(b) - len(a) + 1): + if all(a[j] == b[i + j] for j in range(len(a))): + return i + return None + + +def map_destination( + path: Path, + mapping: list[tuple[Sequence[str], Sequence[str]]], +) -> Path | None: + """ + Takes a path to a Python files and maps it to a destination Markdown file. + + A few notes about some special files: + + - `__main__.py` files are ignored. + - `__init__.py` files are mapped to the directory containing the file, with + the name `README.md`. + + Args: + path: + The path to the Python file. + + mapping: + List of tuples containing the source and destination paths to map. + Note that the list is processed in order, with later mappings + applied after earlier mappings. + """ + segments = list(path.with_suffix(".md").parts) + + if segments[-1] == "__main__.md": + return None + + if segments[-1] == "__init__.md": + segments[-1] = "README.md" + + for from_parts, to_parts in mapping: + idx = is_subsequence(from_parts, segments) + if idx is not None: + segments = [ + *segments[:idx], + *to_parts, + *segments[idx + len(from_parts) :], + ] + return Path(*segments) + + +def map_python_identifier( + path: Path, + mapping: list[tuple[str, str]], +) -> str | None: + """ + Takes a path to a Python files and maps it to a destination Markdown file. + + A few notes about some special files: + + - `__main__.py` files are ignored. + - `__init__.py` files are handled as usual within Python, i.e., + `some/path/__init__.py` is identified as `some.path`, and therefore is + equivalent to `some/path.py`. + + Args: + path: + The path to the Python file. + + mapping: + List of tuples containing the source and destination Python + identifiers to map. Note that the list is processed in order, with + later mappings applied after earlier mappings. + """ + segments = list(path.with_suffix("").parts) + + if segments[-1] == "__main__": + return None + + if segments[-1] == "__init__": + segments = segments[:-1] + + python_identifier = ".".join(segments) + for from_identifier, to_identifier in mapping: + idx = is_subsequence(from_identifier.split("."), python_identifier.split(".")) + if idx is not None: + python_identifier = ( + python_identifier[:idx] + + to_identifier + + python_identifier[idx + len(from_identifier) :] + ) + return python_identifier -def process_python(src: str, dest: Union[str, None] = None) -> None: +def process_python( + src: str, + ignore: list[str] | None = None, + destination_mapping: list[tuple[str, str]] | None = None, + python_mapping: list[tuple[str, str]] | None = None, +) -> None: """ Process the Python files in the given directory. The source directory is relative to the root of the repository, and only - Python files which are version controlled are processed. The generated - documentation may optionally written to a different directory. - """ - dest = dest or src + Python files which are version controlled are processed. Once processed, + they will be available to MkDocs as if they were located in the `docs` + directory. + + This makes use of `mkdocstrings` to generate documentation from the Python + docstrings. + + Args: + src: + The source directory to process. + + ignore: + A list of patterns to ignore. This uses the same syntax as `.gitignore`. + + destination_mapping: + List of tuples containing the source and destination paths to map. + Note that the list is processed in order, with later mappings + applied after earlier mappings. - # List of all files version controlled files in the SRC_ROOT + python_mapping: + List of tuples containing the source and destination Python + identifiers to map. Note that the list is processed in order, with + later mappings applied after earlier mappings. This is applied + idependently of the `destination_mapping` argument. + """ + ignore_spec = PathSpec.from_lines("gitwildmatch", ignore or []) files = sorted( - map( - Path, - subprocess.check_output(["git", "ls-files", src]) # noqa: S603, S607 - .decode("utf-8") - .splitlines(), - ), + Path(p) + for p in subprocess.check_output( + ["git", "ls-files", src], # noqa: S603, S607 + ) + .decode("utf-8") + .splitlines() + if p.endswith(".py") and not ignore_spec.match_file(p) ) - files = sorted(filter(lambda p: p.suffix == ".py", files)) - - for source_path in files: - module_path = source_path.relative_to(src).with_suffix("") - doc_path = source_path.relative_to(src).with_suffix(".md") - full_doc_path = Path(dest, doc_path) - parts = [src, *module_path.parts] + for file in files: + destination = map_destination( + file, + [(Path(a).parts, Path(b).parts) for a, b in destination_mapping or []], + ) + python_identifier = map_python_identifier(file, python_mapping or []) - # Skip __main__ modules - if parts[-1] == "__main__": + if not destination or not python_identifier: continue - # The __init__ modules are implicit in the directory structure. - if parts[-1] == "__init__": - parts = parts[:-1] - full_doc_path = full_doc_path.parent / "README.md" - - if full_doc_path.exists(): - with mkdocs_gen_files.open(full_doc_path, "a", encoding="utf-8") as fd: - python_identifier = ".".join(parts) - print("# " + parts[-1], file=fd) - print("::: " + python_identifier, file=fd) - else: - with mkdocs_gen_files.open(full_doc_path, "w", encoding="utf-8") as fd: - python_identifier = ".".join(parts) - print("# " + parts[-1], file=fd) - print("::: " + python_identifier, file=fd) + with mkdocs_gen_files.open( + destination, + "a" if destination.exists() else "w", + encoding="utf-8", + ) as fd: + print( + "# " + python_identifier.split(".")[-1].replace("_", " ").title(), + file=fd, + ) + print("::: " + python_identifier, file=fd) mkdocs_gen_files.set_edit_path( - full_doc_path, - f"https://github.com/pact-foundation/pact-python/edit/master/pact/{module_path}.py", + destination, + f"https://github.com/pact-foundation/pact-python/edit/master/{file}", ) -process_python("pact") -process_python("examples") +if __name__ == "": + process_python( + "src/pact", + destination_mapping=[ + ("src/pact", "pact"), + ], + python_mapping=[("src.pact", "pact")], + ) + process_python("examples") From 586174265afb3c9da5de8560273eef9d9ced1fba Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 2 Apr 2024 13:05:45 +1100 Subject: [PATCH 0311/1376] docs: ignore private python modules Signed-off-by: JP-Ellis --- docs/scripts/python.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/scripts/python.py b/docs/scripts/python.py index 2796c0988..14ee78f33 100644 --- a/docs/scripts/python.py +++ b/docs/scripts/python.py @@ -124,6 +124,8 @@ def process_python( ignore: list[str] | None = None, destination_mapping: list[tuple[str, str]] | None = None, python_mapping: list[tuple[str, str]] | None = None, + *, + ignore_private: bool = True, ) -> None: """ Process the Python files in the given directory. @@ -141,7 +143,8 @@ def process_python( The source directory to process. ignore: - A list of patterns to ignore. This uses the same syntax as `.gitignore`. + A list of patterns to ignore. This uses the same syntax as + `.gitignore`. destination_mapping: List of tuples containing the source and destination paths to map. @@ -153,6 +156,11 @@ def process_python( identifiers to map. Note that the list is processed in order, with later mappings applied after earlier mappings. This is applied idependently of the `destination_mapping` argument. + + ignore_private: + Whether to ignore private modules (those starting with an underscore + `_`, with the exception of special file names such as `__init__.py` + and `__main__.py`). """ ignore_spec = PathSpec.from_lines("gitwildmatch", ignore or []) files = sorted( @@ -166,6 +174,13 @@ def process_python( ) for file in files: + if ( + ignore_private + and file.name.startswith("_") + and file.stem not in ["__init__", "__main__"] + ): + continue + destination = map_destination( file, [(Path(a).parts, Path(b).parts) for a, b in destination_mapping or []], From f48b036a41d9d9e64bdec209b71a2b3468eff605 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 2 Apr 2024 13:06:13 +1100 Subject: [PATCH 0312/1376] chore(docs): fix typos Signed-off-by: JP-Ellis --- docs/releases.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index de0ed18ff..e5b919042 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,7 +2,7 @@ Pact Python is made available through both GitHub releases and PyPI. The GitHub releases also come with a summary of changes and contributions since the last release. -The entire process is automated through the [build](https://github.com/pact-foundation/pact-python/actions?query=workflow%3Abuild) GitHub Action. A description of the process is provided [below](#build-pipeline). +The entire process is automated through the [build](https://github.com/pact-foundation/pact-python/actions/workflows/build.yml?query=branch%3Amaster) GitHub Action. A description of the process is provided [below](#build-pipeline). ## Versioning @@ -44,6 +44,6 @@ The publish step uses the `pypi` GitHub environment, and is gated behind a manua - Generating a changelog based on the conventional commits since the latest release. - Generating a new GitHub release with the changelog. - Uploading the source distribution and wheels to PyPI. -- Creating a PR to update the `CHANGELOD.md` file with the new release notes. +- Creating a PR to update the `CHANGELOG.md` file with the new release notes. While the generated changelog should be accurate, it may require some manual adjustments on the release page and in the PR. From 7294f7665478db4ce716d98dab477b6d503c13e0 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 2 Apr 2024 13:06:26 +1100 Subject: [PATCH 0313/1376] chore(docs): enforce fenced code blocks Signed-off-by: JP-Ellis --- .markdownlint.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.markdownlint.yml b/.markdownlint.yml index 5c9eb105d..acd88acfe 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -7,3 +7,5 @@ list-marker-space: line-length: false ul-indent: indent: 4 +code-block-style: + style: fenced From 8ee853c79643ca8fa2655147feaaa63689d6f353 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 2 Apr 2024 13:14:38 +1100 Subject: [PATCH 0314/1376] docs: overhaul readme The main README has been completely overhauled: - It is now more closely aligned with Pact JS and Pact Go - It is much shorter, with usage documentation having been moved into `/docs` - Reference to Pact Python `v3` has been added. Signed-off-by: JP-Ellis --- .markdownlint.yml | 12 +- README.md | 638 +++++++++------------------------------------- docs/SUMMARY.md | 3 + docs/consumer.md | 356 ++++++++++++++++++++++++++ docs/provider.md | 158 ++++++++++++ 5 files changed, 647 insertions(+), 520 deletions(-) create mode 100644 docs/consumer.md create mode 100644 docs/provider.md diff --git a/.markdownlint.yml b/.markdownlint.yml index acd88acfe..bfafbf68b 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -1,11 +1,21 @@ default: true + +# Do not enforce line length +line-length: false + +# Adjust list indentation for 4 spaces list-marker-space: ul_single: 3 ul_multi: 3 ol_single: 2 ol_multi: 2 -line-length: false ul-indent: indent: 4 + +# Require fenced code blocks code-block-style: style: fenced + +# Disable checking for reference links, as MkDocs generates additional ones that +# are not visible to MarkdownLint. +reference-links-images: false diff --git a/README.md b/README.md index 6b86fb340..50ea83809 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # Pact Python - +
+ Fast, easy and reliable testing for your APIs and microservices. +
+ +
-
Package @@ -13,552 +17,148 @@
CI/CD - CI - Test - CI - Build - CI - Docs + Test Status + Build Status + Build Status
Meta - Hatch project - linting - Ruff - code style - Ruff - types - Mypy - License + Hatch project + linting - Ruff + style - Ruff + types - Mypy + License
- - -Python version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project. Currently supports version 2 of the [Pact specification]. - -For more information about what Pact is, and how it can help you test your code more efficiently, check out the [Pact documentation]. - -Note: As of Version 1.0 deprecates support for python 2.7 to allow us to incorporate python 3.x features more readily. If you want to still use Python 2.7 use the 0.x.y versions. Only bug fixes will now be added to that release. - -## How to use pact-python - -### Installation - -```console -pip install pact-python -``` - -### Getting started - - - -A guide follows but if you go to the [examples](https://github.com/pact-foundation/pact-python/tree/master/examples). This has a consumer, provider and pact-broker set of tests for both FastAPI and Flask. - -### Writing a Pact - -Creating a complete contract is a two step process: - -1. Create a test on the consumer side that declares the expectations it has of the provider -2. Create a provider state that allows the contract to pass when replayed against the provider - -### Writing the Consumer Test - -If we have a method that communicates with one of our external services, which we'll call `Provider`, and our product, `Consumer` is hitting an endpoint on `Provider` at `/users/` to get information about a particular user. - -If the code to fetch a user looked like this: - -```python -import requests - -def user(user_name): - """Fetch a user object by user_name from the server.""" - uri = 'http://localhost:1234/users/' + user_name - return requests.get(uri).json() -``` - -Then `Consumer`'s contract test might look something like this: - -```python -import atexit -import unittest - -from pact import Consumer, Provider - - -pact = Consumer('Consumer').has_pact_with(Provider('Provider')) -pact.start_service() -atexit.register(pact.stop_service) - - -class GetUserInfoContract(unittest.TestCase): - def test_get_user(self): - expected = { - 'username': 'UserA', - 'id': 123, - 'groups': ['Editors'] - } - - (pact - .given('UserA exists and is not an administrator') - .upon_receiving('a request for UserA') - .with_request('get', '/users/UserA') - .will_respond_with(200, body=expected)) - - with pact: - result = user('UserA') - - self.assertEqual(result, expected) - -``` - -This does a few important things: - -- Defines the Consumer and Provider objects that describe our product and our service under test -- Uses `given` to define the setup criteria for the Provider `UserA exists and is not an administrator` -- Defines what the request that is expected to be made by the consumer will contain -- Defines how the server is expected to respond - -Using the Pact object as a [context manager], we call our method under test which will then communicate with the Pact mock service. The mock service will respond with the items we defined, allowing us to assert that the method processed the response and returned the expected value. If you want more control over when the mock service is configured and the interactions verified, use the `setup` and `verify` methods, respectively: - -```python - (pact - .given('UserA exists and is not an administrator') - .upon_receiving('a request for UserA') - .with_request('get', '/users/UserA') - .will_respond_with(200, body=expected)) - - pact.setup() - # Some additional steps before running the code under test - result = user('UserA') - # Some additional steps before verifying all interactions have occurred - pact.verify() -``` - -#### Requests - -When defining the expected HTTP request that your code is expected to make you can specify the method, path, body, headers, and query: - -```python -pact.with_request( - method='GET', - path='/api/v1/my-resources/', - query={'search': 'example'} -) -``` - -`query` is used to specify URL query parameters, so the above example expects a request made to `/api/v1/my-resources/?search=example`. - -```python -pact.with_request( - method='POST', - path='/api/v1/my-resources/123', - body={'user_ids': [1, 2, 3]}, - headers={'Content-Type': 'application/json'}, -) -``` - -You can define exact values for your expected request like the examples above, or you can use the matchers defined later to assist in handling values that are variable. - -The default hostname and port for the Pact mock service will be `localhost:1234` but you can adjust this during Pact creation: - -```python -from pact import Consumer, Provider -pact = Consumer('Consumer').has_pact_with( - Provider('Provider'), host_name='mockservice', port=8080) -``` - -This can be useful if you need to run to create more than one Pact for your test because your code interacts with two different services. It is important to note that the code you are testing with this contract _must_ contact the mock service. So in this example, the `user` method could accept an argument to specify the location of the server, or retrieve it from an environment variable so you can change its URI during the test. - -The mock service offers you several important features when building your contracts: - -- It provides a real HTTP server that your code can contact during the test and provides the responses you defined. -- You provide it with the expectations for the request your code will make and it will assert the contents of the actual requests made based on your expectations. -- If a request is made that does not match one you defined or if a request from your code is missing it will return an error with details. -- Finally, it will record your contracts as a JSON file that you can store in your repository or publish to a Pact broker. - -### Expecting Variable Content - -The above test works great if that user information is always static, but what happens if the user has a last updated field that is set to the current time every time the object is modified? To handle variable data and make your tests more robust, there are 3 helpful matchers: - -#### Term(matcher, generate) - -Asserts the value should match the given regular expression. You could use this to expect a timestamp with a particular format in the request or response where you know you need a particular format, but are unconcerned about the exact date: - -```python -from pact import Term -... -body = { - 'username': 'UserA', - 'last_modified': Term('\d+-\d+-\d+T\d+:\d+:\d+', '2016-12-15T20:16:01') -} - -(pact - .given('UserA exists and is not an administrator') - .upon_receiving('a request for UserA') - .with_request('get', '/users/UserA/info') - .will_respond_with(200, body=body)) -``` - -When you run the tests for the consumer, the mock service will return the value you provided as `generate`, in this case `2016-12-15T20:16:01`. When the contract is verified on the provider, the regex will be used to search the response from the real provider service and the test will be considered successful if the regex finds a match in the response. - -#### Like(matcher) - -Asserts the element's type matches the matcher. For example: - -```python -from pact import Like -Like(123) # Matches if the value is an integer -Like('hello world') # Matches if the value is a string -Like(3.14) # Matches if the value is a float -``` - -The argument supplied to `Like` will be what the mock service responds with. - -When a dictionary is used as an argument for Like, all the child objects (and their child objects etc.) will be matched according to their types, unless you use a more specific matcher like a Term. - -```python -from pact import Like, Term -Like({ - 'username': Term('[a-zA-Z]+', 'username'), - 'id': 123, # integer - 'confirmed': False, # boolean - 'address': { # dictionary - 'street': '200 Bourke St' # string - } -}) - -``` - -#### EachLike(matcher, minimum=1) - -Asserts the value is an array type that consists of elements like the one passed in. It can be used to assert simple arrays: - -```python -from pact import EachLike -EachLike(1) # All items are integers -EachLike('hello') # All items are strings -``` - -Or other matchers can be nested inside to assert more complex objects: - -```python -from pact import EachLike, Term -EachLike({ - 'username': Term('[a-zA-Z]+', 'username'), - 'id': 123, - 'groups': EachLike('administrators') -}) -``` - -> Note, you do not need to specify everything that will be returned from the Provider in a JSON response, any extra data that is received will be ignored and the tests will still pass. - - - -> Note, to get the generated values from an object that can contain matchers like Term, Like, EachLike, etc. for assertion in self.assertEqual(result, expected) you may need to use get_generated_values() helper function: - -```python -from pact.matchers import get_generated_values -self.assertEqual(result, get_generated_values(expected)) -``` - -#### Match common formats - -Often times, you find yourself having to re-write regular expressions for common formats. - -```python -from pact import Format -Format().integer # Matches if the value is an integer -Format().ip_address # Matches if the value is an ip address -``` - -We've created a number of them for you to save you the time: - -| matcher | description | -| ----------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `identifier` | Match an ID (e.g. 42) | -| `integer` | Match all numbers that are integers (both ints and longs) | -| `decimal` | Match all real numbers (floating point and decimal) | -| `hexadecimal` | Match all hexadecimal encoded strings | -| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) | -| `timestamp` | Match a string containing an RFC3339 formatted timestamp (e.g. Mon, 31 Oct 2016 15:21:41 -0400) | -| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) | -| `iso_datetime` | Match string containing ISO 8601 formatted dates (e.g. 2015-08-06T16:53:10+01:00) | -| `iso_datetime_ms` | Match string containing ISO 8601 formatted dates, enforcing millisecond precision (e.g. 2015-08-06T16:53:10.123+01:00) | -| `ip_address` | Match string containing IP4 formatted address | -| `ipv6_address` | Match string containing IP6 formatted address | -| `uuid` | Match strings containing UUIDs | - -These can be used to replace other matchers - -```python -from pact import Like, Format -Like({ - 'id': Format().integer, # integer - 'lastUpdated': Format().timestamp, # timestamp - 'location': { # dictionary - 'host': Format().ip_address # ip address - } -}) -``` - -For more information see [Matching](https://docs.pact.io/getting_started/matching) - -### Uploading pact files to a Pact Broker - -There are two ways to publish your pact files, to a Pact Broker. - -1. [Pact CLI tools](https://docs.pact.io/pact_broker/client_cli) **recommended** -2. Pact Python API - -#### Broker CLI - -See [Publishing and retrieving pacts](https://docs.pact.io/pact_broker/publishing_and_retrieving_pacts) - -Example uploading to a Pact Broker - -```console -pact-broker publish /path/to/pacts/consumer-provider.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-username someUsername --broker-password somePassword -``` - -Example uploading to a PactFlow Broker - -```console -pact-broker publish /path/to/pacts/consumer-provider.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-token SomeToken -``` - -#### Broker Python API - -```python -broker = Broker(broker_base_url="http://localhost") -broker.publish("TestConsumer", - "2.0.1", - branch='consumer-branch', - pact_dir='.') - -output, logs = verifier.verify_pacts('./userserviceclient-userservice.json') - -``` - -The parameters for this differ slightly in naming from their CLI equivalents: -| CLI | native Python | -|-----------------------------------|-----------------------------------| -| `--branch` | `branch` | -| `--build-url` | `build_url` | -| `--auto-detect-version-properties`| `auto_detect_version_properties` | -| `--tag=TAG` | `consumer_tags` | -| `--tag-with-git-branch` | `tag_with_git_branch` | -| `PACT_DIRS_OR_FILES` | `pact_dir` | -| `--consumer-app-version` | `version` | -| `n/a` | `consumer_name` | - -### Verifying Pacts Against a Service - -In addition to writing Pacts for Python consumers, you can also verify those Pacts against a provider of any language. There are two ways to do this. - -#### Verifier CLI - -After installing pact-python a `pact-verifier` application should be available. To get details about its use you can call it with the help argument: - -```bash -pact-verifier --help -``` - -The simplest example is verifying a server with locally stored Pact files and no provider states: - -```bash -pact-verifier --provider-base-url=http://localhost:8080 --pact-url=./pacts/consumer-provider.json -``` - -Which will immediately invoke the Pact verifier, making HTTP requests to the server located at `http://localhost:8080` based on the Pacts in `./pacts/consumer-provider.json` and reporting the results. - -There are several options for configuring how the Pacts are verified: - -- **`--provider-base-url`** - - Required. Defines the URL of the server to make requests to when verifying the Pacts. - -- **`--pact-url`** - - Required if --pact-urls not specified. The location of a Pact file you want to verify. This can be a URL to a [Pact Broker] or a local path, to provide multiple files, specify multiple arguments. - - ```console - pact-verifier --provider-base-url=http://localhost:8080 --pact-url=./pacts/one.json --pact-url=./pacts/two.json - ``` - -- **`--pact-urls`** - - Required if --pact-url not specified. The location of the Pact files you want to verify. This can be a URL to a [Pact Broker] or one or more local paths, separated by a comma. - -- **`--provider-states-url`** - - _DEPRECATED AFTER v 0.6.0._ The URL where your provider application will produce the list of available provider states. The verifier calls this URL to ensure the Pacts specify valid states before making the HTTP requests. - -- **`--provider-states-setup-url`** - - The URL which should be called to setup a specific provider state before a Pact is verified. This URL will be called with a POST request, and the JSON body `{consumer: 'Consumer name', state: 'a thing exists'}`. - -- **`--pact-broker-url`** - - Base URl for the Pact Broker instance to publish pacts to. Can also be specified via the environment variable `PACT_BROKER_BASE_URL`. - -- **`--pact-broker-username`** - - The username to use when contacting the Pact Broker. Can also be specified via the environment variable `PACT_BROKER_USERNAME`. - -- **`--pact-broker-password`** - - The password to use when contacting the Pact Broker. You can also specify this value as the environment variable `PACT_BROKER_PASSWORD`. - -- **`--pact-broker-token`** - - The bearer token to use when contacting the Pact Broker. You can also specify this value as the environment variable `PACT_BROKER_TOKEN`. - -- **`--consumer-version-tag`** - - Retrieve the latest pacts with this consumer version tag. Used in conjunction with `--provider`. May be specified multiple times. - -- **`--consumer-version-selector`** - - You can also retrieve pacts with consumer version selector, a more flexible approach in specifying which pacts you need. May be specified multiple times. Read more about selectors [here](https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/). - -- **`--provider-version-tag`** - - Tag to apply to the provider application version. May be specified multiple times. - -- **`--provider-version-branch`** - - Branch to apply to the provider application version. - -- **`--custom-provider-header`** - - Header to add to provider state set up and pact verification requests e.g.`Authorization: Basic cGFjdDpwYWN0` - May be specified multiple times. - -- **`-t, --timeout`** - - The duration in seconds we should wait to confirm that the verification process was successful. Defaults to 30. - -- **`-a, --provider-app-version`** - - The provider application version. Required for publishing verification results. - -- **`-r, --publish-verification-results`** - - Publish verification results to the broker. - -#### Verifier Python API - -You can use the Verifier class. This allows you to write native python code and the test framework of your choice. - -```python -verifier = Verifier(provider='UserService', - provider_base_url=PACT_URL) - -# Using a local pact file - -success, logs = verifier.verify_pacts('./userserviceclient-userservice.json') -assert success == 0 - -# Using a pact broker - -- For OSS Pact Broker, use broker_username / broker_password -- For PactFlow Pact Broker, use broker_token - -success, logs = verifier.verify_with_broker( - # broker_username=PACT_BROKER_USERNAME, - # broker_password=PACT_BROKER_PASSWORD, - broker_url=PACT_BROKER_URL, - broker_token=PACT_BROKER_TOKEN, - publish_version=APPLICATION_VERSION, - publish_verification_results=True, - verbose=True, - provider_version_branch=PROVIDER_BRANCH, - enable_pending=True, -) -assert success == 0 -``` - -The parameters for this differ slightly in naming from their CLI equivalents: - -| CLI | native Python | -| -------------------------------- | ------------------------------ | -| `--log-dir` | `log_dir` | -| `--log-level` | `log_level` | -| `--provider-app-version` | `provider_app_version` | -| `--headers` | `custom_provider_headers` | -| `--consumer-version-tag` | `consumer_tags` | -| `--provider-version-tag` | `provider_tags` | -| `--provider-states-setup-url` | `provider_states_setup_url` | -| `--verbose` | `verbose` | -| `--consumer-version-selector` | `consumer_selectors` | -| `--publish-verification-results` | `publish_verification_results` | -| `--provider-version-branch` | `provider_version_branch` | - -You can see more details in the examples - -- [Message Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_03_message_provider.py) -- [Flask Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_01_provider_flask.py) -- [FastAPI Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_01_provider_fastapi.py) - -#### Provider States - -In many cases, your contracts will need very specific data to exist on the provider to pass successfully. If you are fetching a user profile, that user needs to exist, if querying a list of records, one or more records needs to exist. To support decoupling the testing of the consumer and provider, Pact offers the idea of provider states to communicate from the consumer what data should exist on the provider. + + Community + + Slack + Stack Overflow + Twitter + + + + +
+Pact is the de-facto API contract testing tool. Replace expensive and brittle end-to-end integration tests with fast, reliable and easy to debug unit tests. + +
    +
  • ⚡ Lightning fast
  • +
  • 🎈 Effortless full-stack integration testing - from the front-end to the back-end
  • +
  • 🔌 Supports HTTP/REST and event-driven systems
  • +
  • 🛠️ Configurable mock server
  • +
  • 😌 Powerful matching rules prevents brittle tests
  • +
  • 🤝 Integrates with Pact Broker / PactFlow for powerful CI/CD workflows
  • +
  • 🔡 Supports 12+ languages
  • +
+ +Why use Pact? Contract testing with Pact lets you: + +
    +
  • ⚡ Test locally
  • +
  • 🚀 Deploy faster
  • +
  • ⬇️ Reduce the lead time for change
  • +
  • 💰 Reduce the cost of API integration testing
  • +
  • 💥 Prevent breaking changes
  • +
  • 🔎 Understand your system usage
  • +
  • 📃 Document your APIs for free
  • +
  • 🗄 Remove the need for complex data fixtures
  • +
  • 🤷‍♂️ Reduce the reliance on complex test environments
  • +
+ +Watch our series on the problems with end-to-end integrated tests, and how contract testing can help. + +
-When setting up the testing of a provider you will also need to setup the management of these provider states. The Pact verifier does this by making additional HTTP requests to the `--provider-states-setup-url` you provide. This URL could be on the provider application or a separate one. Some strategies for managing state include: + -- Having endpoints in your application that are not active in production that create and delete your datastore state -- A separate application that has access to the same datastore to create and delete, like a separate App Engine module or Docker container pointing to the same datastore -- A standalone application that can start and stop the other server with different datastore states +## Documentation -For more information about provider states, refer to the [Pact documentation] on [Provider States]. +This readme provides a high-level overview of the Pact Python library. For detailed documentation, please refer to the [full Pact Python documentation](https://pact-foundation.github.io/pact-python). For a more general overview of Pact and the rest of the ecosystem, please refer to the [Pact documentation](https://docs.pact.io). -## Development +- [Installation](#installation) +- [Consumer testing](docs/consumer.md) +- [Provider testing](docs/provider.md) +- [Examples](examples/README.md) - +Documentation for the API is generated from the docstrings in the code which you can view [here](https://pact-foundation.github.io/pact-python/pact). Please be aware that only the [`pact.v3` module][pact.v3] is thoroughly documented at this time. -Please read [CONTRIBUTING.md](https://github.com/pact-foundation/pact-python/blob/master/CONTRIBUTING.md) +### Need Help -To setup a development environment: +- [Join](https://slack.pact.io) our community [slack workspace][Pact Foundation Slack]. +- [Stack Overflow](https://stackoverflow.com/questions/tagged/pact) is a great place to ask questions. +- Say 👋 on Twitter: [@pact_up](https://twitter.com/pact_up) +- Join a discussion 💬 on [GitHub Discussions] +- [Raise an issue][GitHub Issues] on GitHub -1. If you want to run tests for all Python versions, install 2.7, 3.3, 3.4, 3.5, and 3.6 from source or using a tool like [pyenv] -2. Its recommended to create a Python [virtualenv] for the project +[Pact Foundation Slack]: https://pact-foundation.slack.com/ +[GitHub Discussions]: https://github.com/pact-foundation/pact-python/discussions +[GitHub Issues]: https://github.com/pact-foundation/pact-python/issues -To setup the environment, run tests, and package the application, run: `make release` +## V3 Preview -If you are just interested in packaging pact-python so you can install it using pip: `make package` +Pact Python is currently undergoing a major rewrite which will be released with the `3.0.0` version. This rewrite will replace the existing Ruby backend with a Rust backend which will provide a significant performance improvement and will allow us to support more features in the future. You can find more information about this rewrite in [this tracking issue on GitHub](https://github.com/pact-foundation/pact-python/issues/396). -This creates a `dist/pact-python-N.N.N.tar.gz` file, where the Ns are the current version. From there you can use pip to install it: +You can preview the new version by using the [`pact.v3` module][pact.v3]. The new version is not yet feature complete, and may be subject to changes. Having said that, we would love to get your feedback on the new version: -`pip install ./dist/pact-python-N.N.N.tar.gz` +- For any issues you find, please [raise an issue][GitHub Issues] on GitHub. +- For any feedback you have, please join the discussion either on [GitHub Discussions] or in the [`#pact-python`](https://pact-foundation.slack.com/archives/C9VECUP6E) channel on the [Pact Foundation Slack]. -### Offline Installation of Standalone Packages +## Installation -Although all Ruby standalone applications are predownloaded into the wheel artifact, it may be useful, for development, purposes to install custom Ruby binaries. In which case, use the `bin-path` flag. +The latest version of Pact Python can be installed from PyPi: -```console -pip install pact-python --bin-path=/absolute/path/to/folder/containing/pact/binaries/for/your/os +```sh +pip install pact-python +# 🚀 now write some tests! ``` -Pact binaries can be found at [Pact Ruby Releases](https://github.com/pact-foundation/pact-ruby-standalone/releases). - -### Testing - -This project has unit and end to end tests, which can both be run from make: +### Requirements -Unit: `make test` +Pact Python tries to support all versions of Python that are still supported by the Python Software Foundation. Older version of Python may work, but are not officially supported. -End to end: `make e2e` +In order to support the broadest range of use cases, Pact Python tries to impose the least restrictions on the versions of libraries that it uses. -### Contact +### Do Not Track -Join us in slack: [![slack](https://slack.pact.io/badge.svg)](https://slack.pact.io) +In order to get better statistics as to who is using Pact, we have an anonymous tracking event that triggers when Pact installs for the first time. The only things we [track](https://docs.pact.io/metrics) are your type of OS, and the version information for the package being installed. No personally identifiable information is sent as part of this request. You can disable tracking by setting the environment variable `PACT_DO_NOT_TRACK=1`: -or +## Contributing -- Twitter: [@pact_up](https://twitter.com/pact_up) -- Stack Overflow: [stackoverflow.com/questions/tagged/pact](https://stackoverflow.com/questions/tagged/pact) +We welcome contributions to the Pact Python library in many forms. There are many ways to help, from writing code, to providing new examples, to writing documentation, to testing the library and providing feedback. For more information, see the [contributing guide](CONTRIBUTING.md). -[context manager]: https://en.wikibooks.org/wiki/Python_Programming/Context_Managers -[Pact Broker]: https://docs.pact.io/pact_broker -[Pact documentation]: https://docs.pact.io/ -[Pact specification]: https://github.com/pact-foundation/pact-specification -[Provider States]: https://docs.pact.io/getting_started/provider_states -[pyenv]: https://github.com/pyenv/pyenv -[virtualenv]: http://python-guide-pt-br.readthedocs.io/en/latest/dev/virtualenvs/ +[![Table of contributors](https://contrib.rocks/image?repo=pact-foundation/pact-python)](https://github.com/pact-foundation/pact-python/graphs/contributors) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index d71e6061e..0f84862b0 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,6 +1,9 @@ - [Home](README.md) + - [Consumer](consumer.md) + - [Provider](provider.md) + - [Releases](releases.md) - [Changelog](CHANGELOG.md) - [Contributing](CONTRIBUTING.md) - [Pact](pact/) diff --git a/docs/consumer.md b/docs/consumer.md new file mode 100644 index 000000000..b145ded0f --- /dev/null +++ b/docs/consumer.md @@ -0,0 +1,356 @@ +# Consumer Testing + +Pact is a consumer-driven contract testing tool. This means that the consumer specifies the expected interactions with the provider, and these interactions are used to create a contract. This contract is then used to verify that the provider meets the consumer's expectations. + +The consumer is the client that makes requests, and the provider is the server that responds to those requests. In most straightforward cases, the consumer is a front-end application and the provider is a back-end service; however, a back-end service may also require information from another service, making it a consumer of that service. + +## Writing the Test + +For an illustrative example, consider a simple API client that fetches user data from a service. The client might look like this: + +```python +import requests + + +class UserClient: + def __init__( + self, + base_url: str = "https://example.com/api/v1", + ): + self.base_url = base_url + + def get_user(self, user_id) -> dict[str, str | int | list[str]]: + """ + Fetch a user's data. + + Args: + user_id: The user's ID. + + Returns: + The user's data as a dictionary. It should have the following keys: + + - id: The user's ID. + - username: The user's username. + - groups: A list of groups the user belongs to. + """ + return requests.get("/".join([self.base_url, "user", user_id])).json() +``` + +The Pact test for this client would look like this: + +```python +import atexit +import unittest + +from user_client import UserClient +from pact import Consumer, Provider + +pact = Consumer("UserConsumer").has_pact_with(Provider("UserProvider")) +pact.start_service() +atexit.register(pact.stop_service) + + +class GetUserData(unittest.TestCase): + def test_get_user(self) -> None: + expected = { + "username": "UserA", + "id": 123, + "groups": ["Editors"], + } + + ( + pact.given("User 123 exists") + .upon_receiving("a request for user 123") + .with_request("get", "/user/123") + .will_respond_with(200, body=expected) + ) + + client = UserClient(pact.uri) + + with pact: + result = client.get_user(123) + self.assertEqual(result, expected) +``` + +This test does the following: + +- defines the Consumer and Provider objects that describe the product and the service under test, +- uses `given` to define the setup criteria for the Provider, and +- defines the expected request and response for the interaction. + +The mock service is started when the `pact` object is used as a context manager. The `UserClient` object is created with the URI of the mock service, and the `get_user` method is called. The mock service responds with the expected data, and the test asserts that the response matches the expected data. + + + +!!! info + + A common mistake is to use a generic HTTP client to make requests to the mock service. This defeats the purpose of the test as it does not verify that the client is making the correct requests and handling the responses correctly. + + + +An alternative to using the `pact` object as a context manager is to manually call the `setup` and `verify` methods: + +```python +with pact: + result = client.get_user(123) + self.assertEqual(result, expected) + +# Is equivalent to + +pact.setup() +result = client.get_user(123) +self.assertEqual(result, expected) +pact.verify() +``` + +## Mock Service + +Pact provides a mock service that simulates the provider service based on the defined interactions. The mock service is started when the `pact` object is used as a context manager, or when the `setup` method is called. + +The mock service is started by default on `localhost:1234`, but you can adjust this during Pact creation. This is particularly useful if the consumer interactions with multiple services. + +```python +pact = Consumer('Consumer').has_pact_with( + Provider('Provider'), + host_name='mockservice', + port=8080, +) +``` + +The mock service offers you several important features when building your contracts: + +- It provides a real HTTP server that your code can contact during the test and provides the responses you defined. +- You provide it with the expectations for the request your code will make and it will assert the contents of the actual requests made based on your expectations. +- If a request is made that does not match one you defined or if a request from your code is missing it will return an error with details. +- Finally, it will record your contracts as a JSON file that you can store in your repository or publish to a Pact broker. + +## Requests + +The expected request in the example above is defined with the `with_request` method. It is possible to customize the request further by specifying the method, path, body, headers, and query with the `method`, `path`, `body`, `headers` and `query` keyword arguments. + +- Adding query parameters: + + ```python + pact.with_request( + path="/user/search", + query={"group": "editor"}, + ) + ``` + +- Using different HTTP methods: + + ```python + pact.with_request( + method="DELETE", + path="/user/123", + ) + ``` + +- Adding a request body and headers: + + ```python + pact.with_request( + method="POST", + path="/user/123", + body={"username": "UserA"}, + headers={"Content-Type": "application/json"}, + ) + ``` + +You can define exact values for your expected request like the examples above, or you can use the matchers defined later to assist in handling values that are variable. + +It is important to note that the code you are testing _must_ complete all requests defined. Similarly, if a client makes a request that is not defined in the contract, the test will also fail. + +## Pattern Matching + +Simple equality checks might be sufficient for simple requests, but more realistic tests will require more flexible matching. For example, the above scenario works great if the user information is always static, but will fail if the user has a datetime field that is regularly updated. + +In order to handle variable data and make tests more robust, there are a number of matchers available as described below. + +### Terms + +The `Term` matcher allows you to define a regular expression that the value should match, along with an example value. The pattern is used by Pact for determining the validity of the response, while the example value is returned by Pact in cases where a response needs to be generated. + +This is useful when you need to assert that a value has a particular format, but you are unconcerned about the exact value. + +```python +body = { + "id": 123, + "reference": Term(r"[A-Z]\d{3,6}-[0-9a-f]{6}", "X1234-456def"), + "last_modified": Term( + r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", + "2024-07-20T13:27:03Z", + ), +} + +( + pact.given("User 123 exists") + .upon_receiving("a request for user 123") + .with_request("get", "/user/123") + .will_respond_with(200, body=body, headers={ + "X-Request-ID": Term( + r"[a-z]{4}[0-9]{8}-[A-Z]{3}", + "abcd1234-EFG", + ), + }) +) + +client = UserClient(pact.uri) + +with pact: + result = client.get_user(123) + assert result["id"] == 123 + assert result["reference"] == "X1234-456def" + assert result["last_modified"] == "2024-07-20T13:27:03Z" +``` + +In this example, the `UserClient` must include a `X-Request-ID` header matching the pattern (irrespective of the actual value), and the mock service will respond with the example values. + +### Like + +The `Like` matcher asserts that the element's type matches the matcher. If the mock service needs to produce an answer, the example value provided will be returned. Some examples of the `Like` matcher are: + +```python +from pact import Like + +Like(123) # Requires any integer +Like("hello world") # Requires any string +Like(3.14) # Requires any float +Like(True) # Requires any boolean +``` + +More complex object can be defined, in which case the `Like` matcher will be applied recursively: + +```python +from pact import Like, Term + +Like({ + 'id': 123, # Requires any integer + "reference": Term(r"[A-Z]\d{3,6}-[0-9a-f]{6}", "X1234-456def"), + 'confirmed': False, # Requires any boolean + 'address': { # Requires a dictionary + 'street': '200 Bourke St' # Requires any string + } +}) +``` + +### EachLike + +The `EachLike` matcher asserts the value is an array type that consists of elements like the one passed in. It can be used to assert simple arrays, + +```python +from pact import EachLike + +EachLike(1) # All items are integers +EachLike('hello') # All items are strings +``` + +or other matchers can be nested inside to assert more complex objects + +```python +from pact import EachLike, Term +EachLike({ + 'username': Term('[a-zA-Z]+', 'username'), + 'id': 123, + 'groups': EachLike('administrators') +}) +``` + +> Note, you do not need to specify everything that will be returned from the Provider in a JSON response, any extra data that is received will be ignored and the tests will still pass. + + + +> Note, to get the generated values from an object that can contain matchers like Term, Like, EachLike, etc. for assertion in self.assertEqual(result, expected) you may need to use get_generated_values() helper function: + +```python +from pact.matchers import get_generated_values +self.assertEqual(result, get_generated_values(expected)) +``` + +### Common Formats + +As you have seen above, regular expressions are a powerful tool for matching complex patterns; however, they can be cumbersome to write and maintain. A number of common formats have been predefined for ease of use: + +| matcher | description | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `identifier` | Match an ID (e.g. 42) | +| `integer` | Match all numbers that are integers (both ints and longs) | +| `decimal` | Match all real numbers (floating point and decimal) | +| `hexadecimal` | Match all hexadecimal encoded strings | +| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) | +| `timestamp` | Match a string containing an RFC3339 formatted timestamp (e.g. Mon, 31 Oct 2016 15:21:41 -0400) | +| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) | +| `iso_datetime` | Match string containing ISO 8601 formatted dates (e.g. 2015-08-06T16:53:10+01:00) | +| `iso_datetime_ms` | Match string containing ISO 8601 formatted dates, enforcing millisecond precision (e.g. 2015-08-06T16:53:10.123+01:00) | +| `ip_address` | Match string containing IP4 formatted address | +| `ipv6_address` | Match string containing IP6 formatted address | +| `uuid` | Match strings containing UUIDs | + +These can be used to replace other matchers + +```python +from pact import Like, Format + +Like({ + 'id': Format().integer, + 'lastUpdated': Format().timestamp, + 'location': { + 'host': Format().ip_address + }, +}) +``` + +For more information see [Matching](https://docs.pact.io/getting_started/matching) + +## Broker + +The above example showed how to test a single consumer; however, without also testing the provider, the test is incomplete. The Pact Broker is a service that allows you to share your contracts between your consumer and provider tests. + +The Pact Broker acts as a central repository for all your contracts and verification results, and provides a number of features to help you get the most out of your Pact workflow. + +Once the tests are complete, the contracts can be uploaded to the Pact Broker. The provider can then download the contracts and run its own tests to ensure it meets the consumer's expectations. There are two ways to upload contracts as shown below. + +### Broker CLI (_recommended_) + +The Broker CLI is a command-line tool that is bundled with the Pact Python package. It can be used to publish contracts to the Pact Broker. See [Publishing and retrieving pacts](https://docs.pact.io/pact_broker/publishing_and_retrieving_pacts) + +The general syntax for the CLI is: + +```console +pact-broker publish \ + /path/to/pacts/consumer-provider.json \ + --consumer-app-version 1.0.0 \ + --branch main \ + --broker-base-url https://test.pactflow.io \ + --broker-username someUsername \ + --broker-password somePassword +``` + +If the broker requires a token, you can use the `--broker-token` flag instead of `--broker-username` and `--broker-password`. + +### Python API + +If you wish to use a more programmatic approach within Python, it is possible to use the `Broker` class to publish contracts to the Pact Broker. Note that it is ultimately a wrapper around the CLI, and as a result, the CLI is recommended for most use cases. + +```python +broker = Broker(broker_base_url="http://localhost") +broker.publish( + "TestConsumer", + "2.0.1", + branch="consumer-branch", + pact_dir=".", +) +``` + +The parameters for this differ slightly in naming from their CLI equivalents: + +| CLI | native Python | +| ---------------------------------- | -------------------------------- | +| `--branch` | `branch` | +| `--build-url` | `build_url` | +| `--auto-detect-version-properties` | `auto_detect_version_properties` | +| `--tag=TAG` | `consumer_tags` | +| `--tag-with-git-branch` | `tag_with_git_branch` | +| `PACT_DIRS_OR_FILES` | `pact_dir` | +| `--consumer-app-version` | `version` | +| `n/a` | `consumer_name` | diff --git a/docs/provider.md b/docs/provider.md new file mode 100644 index 000000000..f4dded900 --- /dev/null +++ b/docs/provider.md @@ -0,0 +1,158 @@ +# Provider Testing + +Pact is a consumer-driven contract testng tool. This means that the consumer specifies the expected interactions with the provider, and these interactions are used to create a contract. This contract is then used to verify that the provider behaves as expected. + +The provider verification process works by replaying the interactions from the consumer against the provider and checking that the responses match what was expected. This is done by using the Pact files created by the consumer tests, either by reading them from a local filesystem, or by fetching them from a Pact Broker. + +## Verifying Pacts + +### Command Line Interface + +Pact Python comes bundled[^1] with the `pact-verifier` CLI tool to verify your provider. It is located at within the `{site-packages}/pact/bin` directory, and the following command will add it to your path: + +[^1]: The CLI is available for most architecture, but if you are on a platform where the CLI is not bundled, you can install the [Pact Ruby Standalone](https://github.com/pact-foundation/pact-ruby-standalone) release. + + + +=== "Linux / macOS (`sh`)" + + ```bash + site_packages=$(python -c 'import sysconfig; print(sysconfig.get_path("purelib"))') + if [ -d "$sit_p_packages/pact/bi ]; then]; then + export PATH_p$site_packages/pact/bin:$P + else + echo "Pact CLI not found." + fi + ``` + +=== "Windows (`pwsh`)" + + ```pwsh + $sitePackages = (python -c 'import sysconfig; print(sysconfig.get_path("purelib"))') + if (Test-Path "$sitePackages/pact/bin") { + $env:PATH += ";$sitePackages/pact/bin" + } else { + Write-Host "Pact CLI not found." + } + ``` + + + +You can verify that the CLI is available by running: + +```console +pact-verifier --help +``` + +A minimal invocation of the Pact verifier looks like this: + +```console +pact-verifier ./pacts/ \ + --provider-base-url=http://localhost:8080 +``` + +This will verify all the Pacts in the `./pacts/` directory against the provider located at `http://localhost:8080`. + +#### Options + +| Option | Description | +| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--provider-base-url TEXT` | Base URL of the provider to verify against. [required] | +| `--provider-states-setup-url TEXT` | URL to send POST requests to setup a given provider state. | +| `--pact-broker-username TEXT` | Username for Pact Broker basic authentication. Can also be specified via the environment variable PACT_BROKER_USERNAME. | +| `--pact-broker-url TEXT` | Base URl for the Pact Broker instance to publish pacts to. Can also be specified via the environment variable PACT_BROKER_BASE_URL. | +| `--consumer-version-tag TEXT` | Retrieve the latest pacts with this consumer version tag. Used in conjunction with --provider. May be specified multiple times. | +| `--consumer-version-selector TEXT` | Retrieve the latest pacts with this consumer version selector. Used in conjunction with --provider. May be specified multiple times. | +| `--provider-version-tag TEXT` | Tag to apply to the provider application version. May be specified multiple times. | +| `--provider-version-branch TEXT` | The name of the branch the provider version belongs to. | +| `--pact-broker-password TEXT` | Password for Pact Broker basic authentication. Can also be specified via the environment variable PACT_BROKER_PASSWORD. | +| `--pact-broker-token TEXT` | Bearer token for Pact Broker authentication. Can also be specified via the environment variable PACT_BROKER_TOKEN. | +| `--provider TEXT` | Retrieve the latest pacts for this provider. | +| `--custom-provider-header TEXT` | Header to add to provider state set up and pact verification requests. eg 'Authorization: Basic cGFjdDpwYWN0'. May be specified multiple times. | +| `-t`, `--timeout INTEGER` | The duration in seconds we should wait to confirm that the verification process was successful. Defaults to 30. | +| `-a`, `--provider-app-version TEXT` | The provider application version. Required for publishing verification results. | +| `-r`, -`-publish-verification-results` | Publish verification results to the broker. | +| `--verbose` / `--no-verbose` | Toggle verbose logging, defaults to False. | +| `--log-dir TEXT` | The directory for the pact.log file. | +| `--log-level TEXT` | The logging level. | +| `--enable-pending` / `--no-enable-pending` | Allow pacts which are in pending state to be verified without causing the overall task to fail. For more information, see [`pact.io/pending`](https://pact.io/pending) | +| `--include-wip-pacts-since TEXT` | Automatically include the pending pacts in the verification step. For more information, see [WIP pacts](https://docs.pact.io/pact_broker/advanced_topics/wip_pacts/) | +| `--help` | Show this message and exit. | + + + +??? note "Deprecated Options" + + | Option | Description | + | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `--pact-url TEXT` | specify pacts as arguments instead. The URI of the pact to verify. Can be an HTTP URI, a local file or directory path. It can be specified multiple times to verify several pacts. | + | `--pact-urls TEXT` | specify pacts as arguments instead. The URI(s) of the pact to verify. Can be an HTTP URI(s) or local file path(s). Provide multiple URI separated by a comma. | + | `--provider-states-url TEXT` | URL to fetch the provider states for the given provider API. | + + + +### Python API + +Pact Python also provides a pythonic wrapper around the command line interface, allowing you to use the Pact verifier directly from your Python code. This can be useful if you want to integrate the verifier into your test suite or CI/CD pipeline. + +To use the Python API, you need to import the `Verifier` class from the `pact` module: + +```python +verifier = Verifier( + provider='UserService', + provider_base_url="http://localhost:8080", +) +``` + +If you are verifying Pacts from the local filesystem, you can use the `verify_pacts` method: + +```python +success, logs = verifier.verify_pacts('./userserviceclient-userservice.json') +assert success == 0 +``` + +On the other hand, if you are using a Pact Broker, you can use the `verify_with_broker` method: + +```python +success, logs = verifier.verify_with_broker( + broker_url=PACT_BROKER_URL, + # Auth options +) +assert success == 0 +``` + +Where the auth options can either be `broker_username` and `broker_password` for OSS Pact Broker, or `broker_token` for PactFlow. + +The CLI options are available as keyword arguments to the various methods of the `Verifier` class: + +| CLI | native Python | +| -------------------------------- | ------------------------------ | +| `--log-dir` | `log_dir` | +| `--log-level` | `log_level` | +| `--provider-app-version` | `provider_app_version` | +| `--headers` | `custom_provider_headers` | +| `--consumer-version-tag` | `consumer_tags` | +| `--provider-version-tag` | `provider_tags` | +| `--provider-states-setup-url` | `provider_states_setup_url` | +| `--verbose` | `verbose` | +| `--consumer-version-selector` | `consumer_selectors` | +| `--publish-verification-results` | `publish_verification_results` | +| `--provider-version-branch` | `provider_version_branch` | + +You can see more details in the examples + +- [Message Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_03_message_provider.py) +- [Flask Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_01_provider_flask.py) +- [FastAPI Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_01_provider_fastapi.py) + +## Provider States + +In general, the consumer will make a request to the provider under the assumption that the provider has certain data, or is in a certain state. This is expressed in the consumer side through the `.given(...)` method. For example, `given("user 123 exists")` assumes that the provider knows about a user with the ID 123. + +To support this, the provider needs to be able to set up the state of the provider to match the expected state of the consumer. This is done through the `--provider-states-setup-url` option, which is a URL that the verifier will call to set up the provider state. + +Managing the provider state is an important part of the provider testing process, and the best way to manage it will depend on your application. A couple of options include: + +1. Having an endpoint is part of the provider application, but not active in production. A call to this endpoint will set up the provider state, typically by [mocking][unittest.mock] the data store or external services. This method is used in the examples above. + +2. A separate application that has access to the same data store as the provider. This application can be started and stopped with different data store states. From b8c94d18c522ed800d9e2ee3cb791a5188b7ec02 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 2 Apr 2024 13:16:04 +1100 Subject: [PATCH 0315/1376] chore(docs): minor fixes in examples/ Signed-off-by: JP-Ellis --- examples/README.md | 12 ++++++------ examples/src/consumer.py | 8 ++++---- examples/src/fastapi.py | 4 ++-- examples/src/flask.py | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/README.md b/examples/README.md index ff4cfecdd..2a41b85c4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -67,10 +67,10 @@ sequenceDiagram In the first stage, the consumer defines a number of interactions in the form below. Pact sets up a mock server that will respond to the requests as defined by the consumer. All these interactions, containing both the request and expected response, are all sent to the Pact Broker. -> Given {provider state} \ -> Upon receiving {description} \ -> With {request} \ -> Will respond with {response} +> Given {provider state}
+> Upon receiving {description}
+> With {request}
+> Will respond with {response}
In the second stage, the provider retrieves the interactions from the Pact Broker. It then sets up a mock client that will make the requests as defined by the consumer. Pact then verifies that the responses from the provider match the expected responses defined by the consumer. @@ -78,7 +78,7 @@ In this way, Pact is consumer driven and can ensure that the provider is compati ### Consumer -The consumer in this example is a simple Python script that makes a HTTP GET request to a server. It is defined in [`src/consumer.py`](src/consumer.py). The tests for the consumer are defined in [`tests/test_00_consumer.py`](tests/test_00_consumer.py). Each interaction is defined using the format mentioned above. Programmatically, this looks like: +The consumer in this example is a simple Python script that makes a HTTP GET request to a server. It is defined in [`src/consumer.py`][examples.src.consumer]. The tests for the consumer are defined in [`tests/test_00_consumer.py`][examples.tests.test_00_consumer]. Each interaction is defined using the format mentioned above. Programmatically, this looks like: ```py expected: dict[str, Any] = { @@ -97,7 +97,7 @@ expected: dict[str, Any] = { ### Provider -This example showcases to different providers, one written in Flask and one written in FastAPI. Both are simple Python web servers that respond to a HTTP GET request. The Flask provider is defined in [`src/flask.py`](src/flask.py) and the FastAPI provider is defined in [`src/fastapi.py`](src/fastapi.py). The tests for the providers are defined in [`tests/test_01_provider_flask.py`](tests/test_01_provider_flask.py) and [`tests/test_01_provider_fastapi.py`](tests/test_01_provider_fastapi.py). +This example showcases to different providers, one written in Flask and one written in FastAPI. Both are simple Python web servers that respond to a HTTP GET request. The Flask provider is defined in [`src/flask.py`][examples.src.flask] and the FastAPI provider is defined in [`src/fastapi.py`][examples.src.fastapi]. The tests for the providers are defined in [`tests/test_01_provider_flask.py`][examples.tests.test_01_provider_flask] and [`tests/test_01_provider_fastapi.py`][examples.tests.test_01_provider_fastapi]. Unlike the consumer side, the provider side is responsible to responding to the interactions defined by the consumers. In this regard, the provider testing is rather simple: diff --git a/examples/src/consumer.py b/examples/src/consumer.py index 42a249fe6..fa260d214 100644 --- a/examples/src/consumer.py +++ b/examples/src/consumer.py @@ -4,13 +4,13 @@ This modules defines a simple [consumer](https://docs.pact.io/getting_started/terminology#service-consumer) which will be tested with Pact in the [consumer -test](../tests/test_00_consumer.py). As Pact is a consumer-driven framework, the -consumer defines the interactions which the provider must then satisfy. +test][examples.tests.test_00_consumer]. As Pact is a consumer-driven framework, +the consumer defines the interactions which the provider must then satisfy. The consumer is the application which makes requests to another service (the provider) and receives a response to process. In this example, we have a simple -[`User`](User) class and the consumer fetches a user's information from a HTTP -endpoint. +[`User`][examples.src.consumer.User] class and the consumer fetches a user's +information from a HTTP endpoint. Note that the code in this module is agnostic of Pact. The `pact-python` dependency only appears in the tests. This is because the consumer is not diff --git a/examples/src/fastapi.py b/examples/src/fastapi.py index d7d2ac9a0..b25d3c1a8 100644 --- a/examples/src/fastapi.py +++ b/examples/src/fastapi.py @@ -4,7 +4,7 @@ This modules defines a simple [provider](https://docs.pact.io/getting_started/terminology#service-provider) which will be tested with Pact in the [provider -test](../tests/test_01_provider_fastapi.py). As Pact is a consumer-driven +test][examples.tests.test_01_provider_fastapi]. As Pact is a consumer-driven framework, the consumer defines the contract which the provider must then satisfy. @@ -32,7 +32,7 @@ When testing the provider in a real application, the calls to the database would be mocked out to avoid the need for a real database. An example of this can be -found in the [test suite](../tests/test_01_provider_fastapi.py). +found in the [test suite][examples.tests.test_01_provider_fastapi]. """ FAKE_DB: Dict[int, Dict[str, Any]] = {} diff --git a/examples/src/flask.py b/examples/src/flask.py index da5424087..31d614042 100644 --- a/examples/src/flask.py +++ b/examples/src/flask.py @@ -4,7 +4,7 @@ This modules defines a simple [provider](https://docs.pact.io/getting_started/terminology#service-provider) which will be tested with Pact in the [provider -test](../tests/test_01_provider_flask.py). As Pact is a consumer-driven +test][examples.tests.test_01_provider_flask]. As Pact is a consumer-driven framework, the consumer defines the contract which the provider must then satisfy. @@ -29,9 +29,9 @@ As this is a simple example, we'll use a simple dict to represent a database. This would be replaced with a real database in a real application. -When testing the provider in a real application, the calls to the database -would be mocked out to avoid the need for a real database. An example of this -can be found in the [test suite](../tests/test_01_provider_flask.py). +When testing the provider in a real application, the calls to the database would +be mocked out to avoid the need for a real database. An example of this can be +found in the [test suite][examples.tests.test_01_provider_flask]. """ FAKE_DB: Dict[int, Dict[str, Any]] = {} From 7994dee76348960dcf1cfa63fcda1443cfe3a18d Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 2 Apr 2024 13:17:14 +1100 Subject: [PATCH 0316/1376] docs: update v3 docs This is a major write up of docs for V3. Including the following changes: - Adding a timeline to make it clearly visible - Added usage documentation within the modules - Consolidated all `Interaction` classes into one module, and hiding the sub-modules from the docs. Signed-off-by: JP-Ellis --- src/pact/v3/__init__.py | 91 ++- src/pact/v3/ffi.py | 10 +- src/pact/v3/interaction/__init__.py | 529 ++------------ ...ction.py => _async_message_interaction.py} | 15 +- src/pact/v3/interaction/_base.py | 447 ++++++++++++ ...tp_interaction.py => _http_interaction.py} | 98 ++- .../interaction/_sync_message_interaction.py | 61 ++ .../interaction/sync_message_interaction.py | 685 ------------------ src/pact/v3/pact.py | 138 +++- src/pact/v3/verifier.py | 77 +- 10 files changed, 908 insertions(+), 1243 deletions(-) rename src/pact/v3/interaction/{async_message_interaction.py => _async_message_interaction.py} (79%) create mode 100644 src/pact/v3/interaction/_base.py rename src/pact/v3/interaction/{http_interaction.py => _http_interaction.py} (80%) create mode 100644 src/pact/v3/interaction/_sync_message_interaction.py delete mode 100644 src/pact/v3/interaction/sync_message_interaction.py diff --git a/src/pact/v3/__init__.py b/src/pact/v3/__init__.py index 826a0d790..91329f2dc 100644 --- a/src/pact/v3/__init__.py +++ b/src/pact/v3/__init__.py @@ -1,34 +1,77 @@ """ Pact Python V3. -The next major release of Pact Python will make use of the Pact reference -library written in Rust. This will allow us to support all of the features of -Pact, and bring the Python library in line with the other Pact libraries. - -The migration will happen in stages, and this module will be used to provide -access to the new functionality without breaking existing code. The stages will -be as follows: - -- **Stage 1**: The new library is exposed within `pact.v3` and can be used - alongside the existing library. During this stage, no guarantees are made - about the stability of the `pact.v3` module. -- **Stage 2**: The library within `pact.v3` is considered stable, and we begin - the process of deprecating the existing library by raising deprecation - warnings when it is used. A detailed migration guide will be provided. -- **Stage 3**: The `pact.v3` module is renamed to `pact`, and the existing - library is moved to the `pact.v2` scope. The `pact.v2` module will be - considered deprecated, and will be removed in a future release. +This module provides a preview of the next major version of Pact Python. It is +subject to breaking changes and may have bugs; however, it is available for +testing and feedback. If you encounter any issues, please report them on +[GitHub](https://github.com/pact-foundation/pact-python/issues), and if you have +any feedback, please let us know on either the [GitHub +discussions](https://github.com/pact-foundation/pact-python/discussions) or on +[Slack](https://slack.pact.io/). + +The next major release will use the [Pact Rust +library](https://github.com/pact-foundation/pact-reference) to provide full +support for all Pact features, and bring feature parity between the Python +library and the other Pact libraries. + +## Migration Plan + +This change will introduce some breaking changes where needed, but it will be +done in a staged manner to give everyone the opportunity to migrate. + +### :construction: Stage 1 (from v2.2) + +- The main Pact Python library remains the same. Bugs and minor features will + continue to be added to the existing library, but no new major features will + be added as the focus will be on the new library. +- The new library is exposed within `pact.v3` and can be used alongside the + existing library. During this stage, no guarantees are made about the + stability of the `pact.v3` module. +- Users are **not** recommended to use the new library in any production + critical code at this stage, but are encouraged to try it out and provide + feedback. +- The existing library will raise + [`PendingDeprecationWarning`][PendingDeprecationWarning] warnings when it is + used (if these warnings are enabled). + +### :hammer_and_wrench: Stage 2 (from v2.3, tbc) + +- The library within `pact.v3` is considered generally stable and users are + encouraged to start migrating to it. +- A detailed migration guide will be provided. +- The existing library will raise [`DeprecationWarning`][DeprecationWarning] + warnings when it is used to help raise awareness of the upcoming change. +- This stage will likely last a few months to give everyone the opportunity to + migrate. + +### :rocket: Stage 3 (from v3) + +- The `pact.v3` module is renamed to `pact` + + - People who have previously migrated to `pact.v3` should be able to do a + `s/pact.v3/pact/` and have everything work. + - If the previous stage identifies any breaking changes as necessary, they + will be made at this point and a detailed migration guide will be + provided. + +- The existing library is moved to the `pact.v2` scope. + + - :bangbang: This will be a very major and breaking change. Previous code + running against `v2` of Pact Python will **not** work against `v3` of + Pact Python. + - Users still wanting to use the `v2` library will need to update their + code to use the new `pact.v2` module. A `s/pact/pact.v2/` should be + sufficient. + - The `pact.v2` module will be considered deprecated, and will eventually + be removed in a future release. No new features and only critical bug + fixes will be made to this part of the library. + """ import warnings -from pact.v3.pact import Pact -from pact.v3.verifier import Verifier - -__all__ = [ - "Pact", - "Verifier", -] +from pact.v3.pact import Pact # noqa: F401 +from pact.v3.verifier import Verifier # noqa: F401 warnings.warn( "The `pact.v3` module is not yet stable. Use at your own risk, and expect " diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 16fd47dfb..7c37e624b 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -5,9 +5,11 @@ around the C API, and is intended to be used by the Pact Python client library to provide a Pythonic interface to Pact. -This module is not intended to be used directly by Pact users. Pact users should -use the Pact Python client library instead. No guarantees are made about the -stability of this module's API. +!!! warning + + This module is not intended to be used directly by Pact users. Pact users + should use the Pact Python client library instead. No guarantees are made + about the stability of this module's API. ## Developer Notes @@ -25,7 +27,7 @@ Python and not worry about allocating or freeing memory. During initial implementation, a lot of these functions will simply raise a -`NotImplementedError`. +[`NotImplementedError`][NotImplementedError]. For those unfamiliar with CFFI, please make sure to read the [CFFI documentation](https://cffi.readthedocs.io/en/latest/using.html). diff --git a/src/pact/v3/interaction/__init__.py b/src/pact/v3/interaction/__init__.py index 8b50b3aa5..661fed172 100644 --- a/src/pact/v3/interaction/__init__.py +++ b/src/pact/v3/interaction/__init__.py @@ -1,460 +1,83 @@ """ -Pact between a consumer and a provider. - -This module defines the classes that are used to define a Pact between a -consumer and a provider. It defines the interactions between the two parties, -and provides the functionality to verify that the interactions are satisfied. - -For the roles of consumer and provider, see the documentation for the -`pact.v3.service` module. +Interaction module. + +This module defines the classes that are used to define individual interactions +within a [`Pact`][pact.v3.pact.Pact] between a consumer and a provider. These +interactions can be of different types, such as HTTP requests, synchronous +messages, or asynchronous messages. + +An interaction is a specific request that the consumer makes to the provider, +and the response that the provider should return. On the consumer side, the +interaction clearly defines the request that the consumer will make to the +provider and the response that the consumer expects to receive. On the provider +side, the interaction is replayed to the provider to ensure that the provider is +able to handle the request and return the expected response. + +## Best Practices + +When defining an interaction, it is important to ensure that the interaction is +as minimal as possible (which is in contrast to the way specifications like +OpenAPI are often written). This is because the interaction is used to verify +that the consumer and provider can communicate correctly, not to define the +entire API. + +For example, consider a simple user API that has a `GET /user/{id}` endpoint +which returns an object of the form: + +```json +{ + "id": 123, + "username": "Alice" + "email": "alice@example.com", + "registered": "2021-02-26T10:17:51+11:00", + "last_login": "2024-07-04T13:25:45+10:00" +} +``` + +The user client might have two functionalities: + +1. To check if the user exists, and +2. To retrieve the user's username. + +The implementation of these two would be: + +```python +from pact.v3 import Pact + + +pact = Pact(consumer="UserClient", provider="UserService") + +# Checking if a user exists +( + pact.upon_receiving("A request to check if a user exists") + .given("A user with ID 123 exists") + .with_request("GET", "/user/123") + .will_respond_with(200) +) + +# Getting a user's username +( + pact.upon_receiving("A request to get a user's username") + .given("A user with ID 123 exists") + .with_request("GET", "/user/123") + .will_respond_with(200) + .with_body({"username": "Alice"}) +) +``` + +Importantly, even if the server returns more information than just the username, +since the client does not care about this information, it should not be included +in the interaction. """ -from __future__ import annotations - -import abc -import json -from typing import TYPE_CHECKING, Any, Literal, overload - -import pact.v3.ffi - -if TYPE_CHECKING: - from pathlib import Path - - try: - from typing import Self - except ImportError: - from typing_extensions import Self - +from pact.v3.interaction._async_message_interaction import AsyncMessageInteraction +from pact.v3.interaction._base import Interaction +from pact.v3.interaction._http_interaction import HttpInteraction +from pact.v3.interaction._sync_message_interaction import SyncMessageInteraction __all__ = [ "Interaction", + "HttpInteraction", + "AsyncMessageInteraction", + "SyncMessageInteraction", ] - - -class Interaction(abc.ABC): - """ - Interaction between a consumer and a provider. - - This abstract class defines an interaction between a consumer and a - provider. The concrete subclasses define the type of interaction, and include: - - - [`HttpInteraction`][pact.v3.pact.interaction.HttpInteraction] - - [`AsyncMessageInteraction`][pact.v3.pact.interaction.AsyncMessageInteraction] - - [`SyncMessageInteraction`][pact.v3.pact.interaction.SyncMessageInteraction] - - A set of interactions between a consumer and a provider is called a Pact. - """ - - def __init__(self, description: str) -> None: - """ - Create a new Interaction. - - As this class is abstract, this function should not be called directly - but should instead be called through one of the concrete subclasses. - - Args: - description: - Description of the interaction. This must be unique within the - Pact. - """ - self._description = description - - def __str__(self) -> str: - """ - Nice representation of the Interaction. - """ - return f"{self.__class__.__name__}({self._description})" - - def __repr__(self) -> str: - """ - Debugging representation of the Interaction. - """ - return f"{self.__class__.__name__}({self._handle!r})" - - @property - @abc.abstractmethod - def _handle(self) -> pact.v3.ffi.InteractionHandle: - """ - Handle for the Interaction. - - This is used internally by the library to pass the Interaction to the - underlying Pact library. - """ - - @property - @abc.abstractmethod - def _interaction_part(self) -> pact.v3.ffi.InteractionPart: - """ - Interaction part. - - Where interactions have multiple parts, this property keeps track - of which part is currently being set. - """ - - def _parse_interaction_part( - self, - part: Literal["Request", "Response", None], - ) -> pact.v3.ffi.InteractionPart: - """ - Convert the input into an InteractionPart. - """ - if part == "Request": - return pact.v3.ffi.InteractionPart.REQUEST - if part == "Response": - return pact.v3.ffi.InteractionPart.RESPONSE - if part is None: - return self._interaction_part - msg = f"Invalid part: {part}" - raise ValueError(msg) - - @overload - def given(self, state: str) -> Self: ... - - @overload - def given(self, state: str, *, name: str, value: str) -> Self: ... - - @overload - def given(self, state: str, *, parameters: dict[str, Any] | str) -> Self: ... - - def given( - self, - state: str, - *, - name: str | None = None, - value: str | None = None, - parameters: dict[str, Any] | str | None = None, - ) -> Self: - """ - Set the provider state. - - This is the state that the provider should be in when the Interaction is - executed. When the provider is being verified, the provider state is - passed to the provider so that its internal state can be set to match - the provider state. - - In its simplest form, the provider state is a string. For example, to - match a provider state of `a user exists`, you would use: - - ```python - pact.upon_receiving("a request").given("a user exists") - ``` - - It is also possible to specify a parameter that will be used to match - the provider state. For example, to match a provider state of `a user - exists` with a parameter `id` that has the value `123`, you would use: - - ```python - ( - pact.upon_receiving("a request").given( - "a user exists", name="id", value="123" - ) - ) - ``` - - Lastly, it is possible to specify multiple parameters that will be used - to match the provider state. For example, to match a provider state of - `a user exists` with a parameter `id` that has the value `123` and a - parameter `name` that has the value `John`, you would use: - - ```python - ( - pact.upon_receiving("a request").given( - "a user exists", - parameters={ - "id": "123", - "name": "John", - }, - ) - ) - ``` - - This function can be called repeatedly to specify multiple provider - states for the same Interaction. If the same `state` is specified with - different parameters, then the parameters are merged together. The above - example with multiple parameters can equivalently be specified as: - - ```python - ( - pact.upon_receiving("a request") - .given("a user exists", name="id", value="123") - .given("a user exists", name="name", value="John") - ) - ``` - - Args: - state: - Provider state for the Interaction. - - name: - Name of the parameter. This must be specified in conjunction - with `value`. - - value: - Value of the parameter. This must be specified in conjunction - with `name`. - - parameters: - Key-value pairs of parameters to use for the provider state. - These must be encodable using [`json.dumps(...)`][json.dumps]. - Alternatively, a string contained the JSON object can be passed - directly. - - If the string does not contain a valid JSON object, then the - string is passed directly as follows: - - ```python - ( - pact.upon_receiving("a request").given( - "a user exists", name="value", value=parameters - ) - ) - ``` - - Raises: - ValueError: - If the combination of arguments is invalid or inconsistent. - """ - if name is not None and value is not None and parameters is None: - pact.v3.ffi.given_with_param(self._handle, state, name, value) - elif name is None and value is None and parameters is not None: - if isinstance(parameters, dict): - pact.v3.ffi.given_with_params( - self._handle, - state, - json.dumps(parameters), - ) - else: - pact.v3.ffi.given_with_params(self._handle, state, parameters) - elif name is None and value is None and parameters is None: - pact.v3.ffi.given(self._handle, state) - else: - msg = "Invalid combination of arguments." - raise ValueError(msg) - return self - - def with_body( - self, - body: str | None = None, - content_type: str | None = None, - part: Literal["Request", "Response"] | None = None, - ) -> Self: - """ - Set the body of the request or response. - - Args: - body: - Body of the request. If this is `None`, then the body is - empty. - - content_type: - Content type of the body. This is ignored if the `Content-Type` - header has already been set. - - part: - Whether the body should be added to the request or the response. - If `None`, then the function intelligently determines whether - the body should be added to the request or the response, based - on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - """ - pact.v3.ffi.with_body( - self._handle, - self._parse_interaction_part(part), - content_type, - body, - ) - return self - - def with_binary_body( - self, - body: bytes | None, - content_type: str | None = None, - part: Literal["Request", "Response"] | None = None, - ) -> Self: - """ - Adds a binary body to the request or response. - - Note that for HTTP interactions, this function will overwrite the body - if it has been set using - [`with_body(...)`][pact.v3.Interaction.with_body]. - - Args: - part: - Whether the body should be added to the request or the response. - If `None`, then the function intelligently determines whether - the body should be added to the request or the response, based - on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - - content_type: - Content type of the body. This is ignored if the `Content-Type` - header has already been set. - - body: - Body of the request. - """ - pact.v3.ffi.with_binary_file( - self._handle, - self._parse_interaction_part(part), - content_type, - body, - ) - return self - - def with_multipart_file( # noqa: PLR0913 - self, - part_name: str, - path: Path | None, - content_type: str | None = None, - part: Literal["Request", "Response"] | None = None, - boundary: str | None = None, - ) -> Self: - """ - Adds a binary file as the body of a multipart request or response. - - The content type of the body will be set to a MIME multipart message. - """ - pact.v3.ffi.with_multipart_file_v2( - self._handle, - self._parse_interaction_part(part), - content_type, - path, - part_name, - boundary, - ) - return self - - def set_key(self, key: str | None) -> Self: - """ - Sets the key for the interaction. - - This is used by V4 interactions to set the key of the interaction, which - can subsequently used to reference the interaction. - """ - pact.v3.ffi.set_key(self._handle, key) - return self - - def set_pending(self, *, pending: bool) -> Self: - """ - Mark the interaction as pending. - - This is used by V4 interactions to mark the interaction as pending, in - which case the provider is not expected to honour the interaction. - """ - pact.v3.ffi.set_pending(self._handle, pending=pending) - return self - - def set_comment(self, key: str, value: Any | None) -> Self: # noqa: ANN401 - """ - Set a comment for the interaction. - - This is used by V4 interactions to set a comment for the interaction. A - comment consists of a key-value pair, where the key is a string and the - value is anything that can be encoded as JSON. - - Args: - key: - Key for the comment. - - value: - Value for the comment. This must be encodable using - [`json.dumps(...)`][json.dumps], or an existing JSON string. The - value of `None` will remove the comment with the given key. - """ - if isinstance(value, str) or value is None: - pact.v3.ffi.set_comment(self._handle, key, value) - else: - pact.v3.ffi.set_comment(self._handle, key, json.dumps(value)) - return self - - def test_name( - self, - name: str, - ) -> Self: - """ - Set the test name annotation for the interaction. - - This is used by V4 interactions to set the name of the test. - - Args: - name: - Name of the test. - """ - pact.v3.ffi.interaction_test_name(self._handle, name) - return self - - def with_plugin_contents( - self, - contents: dict[str, Any] | str, - content_type: str, - part: Literal["Request", "Response"] | None = None, - ) -> Self: - """ - Set the interaction content using a plugin. - - The value of `contents` is passed directly to the plugin as a JSON - string. The plugin will document the format of the JSON content. - - Args: - contents: - Body of the request. If this is `None`, then the body is empty. - - content_type: - Content type of the body. This is ignored if the `Content-Type` - header has already been set. - - part: - Whether the body should be added to the request or the response. - If `None`, then the function intelligently determines whether - the body should be added to the request or the response, based - on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - """ - if isinstance(contents, dict): - contents = json.dumps(contents) - - pact.v3.ffi.interaction_contents( - self._handle, - self._parse_interaction_part(part), - content_type, - contents, - ) - return self - - def with_matching_rules( - self, - rules: dict[str, Any] | str, - part: Literal["Request", "Response"] | None = None, - ) -> Self: - """ - Add matching rules to the interaction. - - Matching rules are used to specify how the request or response should be - matched. This is useful for specifying that certain parts of the request - or response are flexible, such as the date or time. - - Args: - rules: - Matching rules to add to the interaction. This must be - encodable using [`json.dumps(...)`][json.dumps], or a string. - - part: - Whether the matching rules should be added to the request or the - response. If `None`, then the function intelligently determines - whether the matching rules should be added to the request or the - response, based on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] - method has been called. - """ - if isinstance(rules, dict): - rules = json.dumps(rules) - - pact.v3.ffi.with_matching_rules( - self._handle, - self._parse_interaction_part(part), - rules, - ) - return self diff --git a/src/pact/v3/interaction/async_message_interaction.py b/src/pact/v3/interaction/_async_message_interaction.py similarity index 79% rename from src/pact/v3/interaction/async_message_interaction.py rename to src/pact/v3/interaction/_async_message_interaction.py index 19436c857..011853af3 100644 --- a/src/pact/v3/interaction/async_message_interaction.py +++ b/src/pact/v3/interaction/_async_message_interaction.py @@ -1,18 +1,11 @@ """ -Pact between a consumer and a provider. - -This module defines the classes that are used to define a Pact between a -consumer and a provider. It defines the interactions between the two parties, -and provides the functionality to verify that the interactions are satisfied. - -For the roles of consumer and provider, see the documentation for the -`pact.v3.service` module. +Asynchronous message interaction. """ from __future__ import annotations import pact.v3.ffi -from pact.v3.interaction import Interaction +from pact.v3.interaction._base import Interaction class AsyncMessageInteraction(Interaction): @@ -23,6 +16,10 @@ class AsyncMessageInteraction(Interaction): and a provider. It defines the kind of messages a consumer can accept, and the is agnostic of the underlying protocol, be it a message queue, Apache Kafka, or some other asynchronous protocol. + + !!! warning + + This class is not yet fully implemented and is not yet usable. """ def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: diff --git a/src/pact/v3/interaction/_base.py b/src/pact/v3/interaction/_base.py new file mode 100644 index 000000000..baebef64d --- /dev/null +++ b/src/pact/v3/interaction/_base.py @@ -0,0 +1,447 @@ +""" +Pact between a consumer and a provider. + +This module defines the classes that are used to define a Pact between a +consumer and a provider. It defines the interactions between the two parties, +and provides the functionality to verify that the interactions are satisfied. + +For the roles of consumer and provider, see the documentation for the +`pact.v3.service` module. +""" + +from __future__ import annotations + +import abc +import json +from typing import TYPE_CHECKING, Any, Literal, overload + +import pact.v3.ffi + +if TYPE_CHECKING: + from pathlib import Path + + try: + from typing import Self + except ImportError: + from typing_extensions import Self + + +class Interaction(abc.ABC): + """ + Interaction between a consumer and a provider. + + This abstract class defines an interaction between a consumer and a + provider. The concrete subclasses define the type of interaction, and + include: + + - [`HttpInteraction`][pact.v3.interaction.HttpInteraction] + - [`AsyncMessageInteraction`][pact.v3.interaction.AsyncMessageInteraction] + - [`SyncMessageInteraction`][pact.v3.interaction.SyncMessageInteraction] + """ + + def __init__(self, description: str) -> None: + """ + Create a new Interaction. + + As this class is abstract, this function should not be called directly + but should instead be called through one of the concrete subclasses. + + Args: + description: + Description of the interaction. This must be unique within the + Pact. + """ + self._description = description + + def __str__(self) -> str: + """ + Nice representation of the Interaction. + """ + return f"{self.__class__.__name__}({self._description})" + + def __repr__(self) -> str: + """ + Debugging representation of the Interaction. + """ + return f"{self.__class__.__name__}({self._handle!r})" + + @property + @abc.abstractmethod + def _handle(self) -> pact.v3.ffi.InteractionHandle: + """ + Handle for the Interaction. + + This is used internally by the library to pass the Interaction to the + underlying Pact library. + """ + + @property + @abc.abstractmethod + def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + """ + Interaction part. + + Where interactions have multiple parts, this property keeps track + of which part is currently being set. + """ + + def _parse_interaction_part( + self, + part: Literal["Request", "Response", None], + ) -> pact.v3.ffi.InteractionPart: + """ + Convert the input into an InteractionPart. + """ + if part == "Request": + return pact.v3.ffi.InteractionPart.REQUEST + if part == "Response": + return pact.v3.ffi.InteractionPart.RESPONSE + if part is None: + return self._interaction_part + msg = f"Invalid part: {part}" + raise ValueError(msg) + + @overload + def given(self, state: str) -> Self: ... + + @overload + def given(self, state: str, *, name: str, value: str) -> Self: ... + + @overload + def given(self, state: str, *, parameters: dict[str, Any] | str) -> Self: ... + + def given( + self, + state: str, + *, + name: str | None = None, + value: str | None = None, + parameters: dict[str, Any] | str | None = None, + ) -> Self: + """ + Set the provider state. + + This is the state that the provider should be in when the Interaction is + executed. When the provider is being verified, the provider state is + passed to the provider so that its internal state can be set to match + the provider state. + + In its simplest form, the provider state is a string. For example, to + match a provider state of `a user exists`, you would use: + + ```python + pact.upon_receiving("a request").given("a user exists") + ``` + + It is also possible to specify a parameter that will be used to match + the provider state. For example, to match a provider state of `a user + exists` with a parameter `id` that has the value `123`, you would use: + + ```python + ( + pact.upon_receiving("a request").given( + "a user exists", + name="id", + value="123", + ) + ) + ``` + + Lastly, it is possible to specify multiple parameters that will be used + to match the provider state. For example, to match a provider state of + `a user exists` with a parameter `id` that has the value `123` and a + parameter `name` that has the value `John`, you would use: + + ```python + ( + pact.upon_receiving("a request").given( + "a user exists", + parameters={ + "id": "123", + "name": "John", + }, + ) + ) + ``` + + This function can be called repeatedly to specify multiple provider + states for the same Interaction. If the same `state` is specified with + different parameters, then the parameters are merged together. The above + example with multiple parameters can equivalently be specified as: + + ```python + ( + pact.upon_receiving("a request") + .given("a user exists", name="id", value="123") + .given("a user exists", name="name", value="John") + ) + ``` + + Args: + state: + Provider state for the Interaction. + + name: + Name of the parameter. This must be specified in conjunction + with `value`. + + value: + Value of the parameter. This must be specified in conjunction + with `name`. + + parameters: + Key-value pairs of parameters to use for the provider state. + These must be encodable using [`json.dumps(...)`][json.dumps]. + Alternatively, a string contained the JSON object can be passed + directly. + + If the string does not contain a valid JSON object, then the + string is passed directly as follows: + + ```python + ( + pact.upon_receiving("a request").given( + "a user exists", + name="value", + value=parameters, + ) + ) + ``` + + Raises: + ValueError: + If the combination of arguments is invalid or inconsistent. + """ + if name is not None and value is not None and parameters is None: + pact.v3.ffi.given_with_param(self._handle, state, name, value) + elif name is None and value is None and parameters is not None: + if isinstance(parameters, dict): + pact.v3.ffi.given_with_params( + self._handle, + state, + json.dumps(parameters), + ) + else: + pact.v3.ffi.given_with_params(self._handle, state, parameters) + elif name is None and value is None and parameters is None: + pact.v3.ffi.given(self._handle, state) + else: + msg = "Invalid combination of arguments." + raise ValueError(msg) + return self + + def with_body( + self, + body: str | None = None, + content_type: str | None = None, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Set the body of the request or response. + + Args: + body: + Body of the request. If this is `None`, then the body is + empty. + + content_type: + Content type of the body. This is ignored if the `Content-Type` + header has already been set. + + part: + Whether the body should be added to the request or the response. + If `None`, then the function intelligently determines whether + the body should be added to the request or the response. + """ + pact.v3.ffi.with_body( + self._handle, + self._parse_interaction_part(part), + content_type, + body, + ) + return self + + def with_binary_body( + self, + body: bytes | None, + content_type: str | None = None, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Adds a binary body to the request or response. + + Note that for HTTP interactions, this function will overwrite the body + if it has been set using + [`with_body(...)`][pact.v3.interaction.Interaction.with_body]. + + Args: + part: + Whether the body should be added to the request or the response. + If `None`, then the function intelligently determines whether + the body should be added to the request or the response. + + content_type: + Content type of the body. This is ignored if the `Content-Type` + header has already been set. + + body: + Body of the request. + """ + pact.v3.ffi.with_binary_file( + self._handle, + self._parse_interaction_part(part), + content_type, + body, + ) + return self + + def with_multipart_file( # noqa: PLR0913 + self, + part_name: str, + path: Path | None, + content_type: str | None = None, + part: Literal["Request", "Response"] | None = None, + boundary: str | None = None, + ) -> Self: + """ + Adds a binary file as the body of a multipart request or response. + + The content type of the body will be set to a MIME multipart message. + """ + pact.v3.ffi.with_multipart_file_v2( + self._handle, + self._parse_interaction_part(part), + content_type, + path, + part_name, + boundary, + ) + return self + + def set_key(self, key: str | None) -> Self: + """ + Sets the key for the interaction. + + This is used by V4 interactions to set the key of the interaction, which + can subsequently used to reference the interaction. + """ + pact.v3.ffi.set_key(self._handle, key) + return self + + def set_pending(self, *, pending: bool) -> Self: + """ + Mark the interaction as pending. + + This is used by V4 interactions to mark the interaction as pending, in + which case the provider is not expected to honour the interaction. + """ + pact.v3.ffi.set_pending(self._handle, pending=pending) + return self + + def set_comment(self, key: str, value: Any | None) -> Self: # noqa: ANN401 + """ + Set a comment for the interaction. + + This is used by V4 interactions to set a comment for the interaction. A + comment consists of a key-value pair, where the key is a string and the + value is anything that can be encoded as JSON. + + Args: + key: + Key for the comment. + + value: + Value for the comment. This must be encodable using + [`json.dumps(...)`][json.dumps], or an existing JSON string. The + value of `None` will remove the comment with the given key. + """ + if isinstance(value, str) or value is None: + pact.v3.ffi.set_comment(self._handle, key, value) + else: + pact.v3.ffi.set_comment(self._handle, key, json.dumps(value)) + return self + + def test_name( + self, + name: str, + ) -> Self: + """ + Set the test name annotation for the interaction. + + This is used by V4 interactions to set the name of the test. + + Args: + name: + Name of the test. + """ + pact.v3.ffi.interaction_test_name(self._handle, name) + return self + + def with_plugin_contents( + self, + contents: dict[str, Any] | str, + content_type: str, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Set the interaction content using a plugin. + + The value of `contents` is passed directly to the plugin as a JSON + string. The plugin will document the format of the JSON content. + + Args: + contents: + Body of the request. If this is `None`, then the body is empty. + + content_type: + Content type of the body. This is ignored if the `Content-Type` + header has already been set. + + part: + Whether the body should be added to the request or the response. + If `None`, then the function intelligently determines whether + the body should be added to the request or the response. + """ + if isinstance(contents, dict): + contents = json.dumps(contents) + + pact.v3.ffi.interaction_contents( + self._handle, + self._parse_interaction_part(part), + content_type, + contents, + ) + return self + + def with_matching_rules( + self, + rules: dict[str, Any] | str, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Add matching rules to the interaction. + + Matching rules are used to specify how the request or response should be + matched. This is useful for specifying that certain parts of the request + or response are flexible, such as the date or time. + + Args: + rules: + Matching rules to add to the interaction. This must be + encodable using [`json.dumps(...)`][json.dumps], or a string. + + part: + Whether the matching rules should be added to the request or the + response. If `None`, then the function intelligently determines + whether the matching rules should be added to the request or the + response. + """ + if isinstance(rules, dict): + rules = json.dumps(rules) + + pact.v3.ffi.with_matching_rules( + self._handle, + self._parse_interaction_part(part), + rules, + ) + return self diff --git a/src/pact/v3/interaction/http_interaction.py b/src/pact/v3/interaction/_http_interaction.py similarity index 80% rename from src/pact/v3/interaction/http_interaction.py rename to src/pact/v3/interaction/_http_interaction.py index 8fdf64e7e..9a2837cf0 100644 --- a/src/pact/v3/interaction/http_interaction.py +++ b/src/pact/v3/interaction/_http_interaction.py @@ -1,12 +1,5 @@ """ -Pact between a consumer and a provider. - -This module defines the classes that are used to define a Pact between a -consumer and a provider. It defines the interactions between the two parties, -and provides the functionality to verify that the interactions are satisfied. - -For the roles of consumer and provider, see the documentation for the -`pact.v3.service` module. +HTTP interaction. """ from __future__ import annotations @@ -15,7 +8,7 @@ from typing import TYPE_CHECKING, Iterable, Literal import pact.v3.ffi -from pact.v3.interaction import Interaction +from pact.v3.interaction._base import Interaction if TYPE_CHECKING: try: @@ -31,14 +24,44 @@ class HttpInteraction(Interaction): This class defines a synchronous HTTP interaction between a consumer and a provider. It defines a specific request that the consumer makes to the provider, and the response that the provider should return. + + This class provides a simple way to define the request and response for an + HTTP interaction. As many elements are shared between the request and + response, this class provides a common interface for both. The functions + intelligently determine whether the element should be added to the request + or the response based on whether + [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] + has been called. + + For example, the following two interactions are equivalent: + + ```python + ( + pact.upon_receiving("a request") + .with_request("GET", "/") + .with_header("X-Foo", "bar") + .will_respond_with(200) + .with_header("X-Hello", "world") + ) + ``` + + ```python + ( + pact.upon_receiving("a request") + .with_request("GET", "/") + .will_respond_with(200) + .with_header("X-Foo", "bar", part="Request") + .with_header("X-Hello", "world", part="Response") + ) + ``` """ def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: """ Initialise a new HTTP Interaction. - This function should not be called directly. Instead, an Interaction - should be created using the + This class should not be instantiated directly. Instead, an + `HttpInteraction` should be created using the [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a [`Pact`][pact.v3.Pact] instance. """ @@ -163,7 +186,8 @@ def with_header( If the value of the header is expected to be a JSON object and clashes with the above syntax, then it is recommended to make use of the - [`set_header(...)`][pact.v3.Interaction.set_header] method instead. + [`set_header(...)`][pact.v3.interaction.HttpInteraction.set_header] + method instead. Args: name: @@ -177,7 +201,7 @@ def with_header( response. If `None`, then the function intelligently determines whether the header should be added to the request or the response, based on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] method has been called. """ interaction_part = self._parse_interaction_part(part) @@ -201,24 +225,23 @@ def with_headers( """ Add multiple headers to the request. - Note that due to the requirement of Python dictionaries to - have unique keys, it is _not_ possible to specify a header multiple - times to create a multi-valued header. Instead, you may: + Note that due to the requirement of Python dictionaries to have unique + keys, it is _not_ possible to specify a header multiple times to create + a multi-valued header. Instead, you may: - Use an alternative data structure. Any iterable of key-value pairs is accepted, including a list of tuples, a list of lists, or a dictionary view. - Make multiple calls to - [`with_header(...)`][pact.v3.Interaction.with_header] or - [`with_headers(...)`][pact.v3.Interaction.with_headers]. + [`with_header(...)`][pact.v3.interaction.HttpInteraction.with_header] + or + [`with_headers(...)`][pact.v3.interaction.HttpInteraction.with_headers]. - Specify the multiple values in a JSON object of the form: - ```python - ( - pact.upon_receiving("a request") - .with_headers({ + ```python ( + pact.upon_receiving("a request") .with_headers({ "X-Foo": json.dumps({ "value": ["bar", "baz"], }), @@ -226,8 +249,9 @@ def with_headers( ) ``` - See [`with_header(...)`][pact.v3.Interaction.with_header] for more - information. + See + [`with_header(...)`][pact.v3.interaction.HttpInteraction.with_header] + for more information. Args: headers: @@ -238,7 +262,7 @@ def with_headers( response. If `None`, then the function intelligently determines whether the header should be added to the request or the response, based on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] method has been called. """ if isinstance(headers, dict): @@ -256,8 +280,9 @@ def set_header( r""" Add a header to the request. - Unlike [`with_header(...)`][pact.v3.Interaction.with_header], this - function does no additional processing of the header value. This is + Unlike + [`with_header(...)`][pact.v3.interaction.HttpInteraction.with_header], + this function does no additional processing of the header value. This is useful for headers that contain a JSON object. Args: @@ -272,7 +297,7 @@ def set_header( response. If `None`, then the function intelligently determines whether the header should be added to the request or the response, based on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] method has been called. """ pact.v3.ffi.set_header( @@ -293,11 +318,11 @@ def set_headers( This function intelligently determines whether the header should be added to the request or the response, based on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] method - has been called. + [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] + method has been called. - See [`set_header(...)`][pact.v3.Interaction.set_header] for more - information. + See [`set_header(...)`][pact.v3.interaction.HttpInteraction.set_header] for + more information. Args: headers: @@ -308,7 +333,7 @@ def set_headers( response. If `None`, then the function intelligently determines whether the header should be added to the request or the response, based on whether the - [`will_respond_with(...)`][pact.v3.Interaction.will_respond_with] + [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] method has been called. """ if isinstance(headers, dict): @@ -392,7 +417,8 @@ def with_query_parameters( """ Add multiple query parameters to the request. - See [`with_query_parameter(...)`][pact.v3.Interaction.with_query_parameter] + See + [`with_query_parameter(...)`][pact.v3.interaction.HttpInteraction.with_query_parameter] for more information. Args: @@ -411,8 +437,8 @@ def will_respond_with(self, status: int) -> Self: Ideally, this function is called once all of the request information has been set. This allows functions such as - [`with_header(...)`][pact.v3.Interaction.with_header] to intelligently - determine whether this is a request or response header. + [`with_header(...)`][pact.v3.interaction.HttpInteraction.with_header] + to intelligently determine whether this is a request or response header. Alternatively, the `part` argument can be used to explicitly specify whether the header should be added to the request or the response. diff --git a/src/pact/v3/interaction/_sync_message_interaction.py b/src/pact/v3/interaction/_sync_message_interaction.py new file mode 100644 index 000000000..92072d114 --- /dev/null +++ b/src/pact/v3/interaction/_sync_message_interaction.py @@ -0,0 +1,61 @@ +""" +Synchronous message interaction. +""" + +from __future__ import annotations + +import pact.v3.ffi +from pact.v3.interaction._base import Interaction + + +class SyncMessageInteraction(Interaction): + """ + A synchronous message interaction. + + This class defines a synchronous message interaction between a consumer and + a provider. As with [`HttpInteraction`][pact.v3.pact.HttpInteraction], it + defines a specific request that the consumer makes to the provider, and the + response that the provider should return. + + !!! warning + + This class is not yet fully implemented and is not yet usable. + """ + + def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: + """ + Initialise a new Synchronous Message Interaction. + + This function should not be called directly. Instead, an + AsyncMessageInteraction should be created using the + [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a + [`Pact`][pact.v3.Pact] instance using the `"Sync"` interaction type. + + Args: + pact_handle: + Handle for the Pact. + + description: + Description of the interaction. This must be unique within the + Pact. + """ + super().__init__(description) + self.__handle = pact.v3.ffi.new_sync_message_interaction( + pact_handle, + description, + ) + self.__interaction_part = pact.v3.ffi.InteractionPart.REQUEST + + @property + def _handle(self) -> pact.v3.ffi.InteractionHandle: + """ + Handle for the Interaction. + + This is used internally by the library to pass the Interaction to the + underlying Pact library. + """ + return self.__handle + + @property + def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + return self.__interaction_part diff --git a/src/pact/v3/interaction/sync_message_interaction.py b/src/pact/v3/interaction/sync_message_interaction.py deleted file mode 100644 index 59e32487e..000000000 --- a/src/pact/v3/interaction/sync_message_interaction.py +++ /dev/null @@ -1,685 +0,0 @@ -""" -Pact between a consumer and a provider. - -This module defines the classes that are used to define a Pact between a -consumer and a provider. It defines the interactions between the two parties, -and provides the functionality to verify that the interactions are satisfied. - -For the roles of consumer and provider, see the documentation for the -`pact.v3.service` module. -""" - -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Set, overload - -from yarl import URL - -import pact.v3.ffi -from pact.v3.interaction import Interaction -from pact.v3.interaction.async_message_interaction import AsyncMessageInteraction -from pact.v3.interaction.http_interaction import HttpInteraction - -if TYPE_CHECKING: - from types import TracebackType - - try: - from typing import Self - except ImportError: - from typing_extensions import Self - - -class SyncMessageInteraction(Interaction): - """ - A synchronous message interaction. - - This class defines a synchronous message interaction between a consumer and - a provider. As with [`HttpInteraction`][pact.v3.pact.HttpInteraction], it - defines a specific request that the consumer makes to the provider, and the - response that the provider should return. - """ - - def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: - """ - Initialise a new Synchronous Message Interaction. - - This function should not be called directly. Instead, an - AsyncMessageInteraction should be created using the - [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a - [`Pact`][pact.v3.Pact] instance using the `"Sync"` interaction type. - - Args: - pact_handle: - Handle for the Pact. - - description: - Description of the interaction. This must be unique within the - Pact. - """ - super().__init__(description) - self.__handle = pact.v3.ffi.new_sync_message_interaction( - pact_handle, - description, - ) - self.__interaction_part = pact.v3.ffi.InteractionPart.REQUEST - - @property - def _handle(self) -> pact.v3.ffi.InteractionHandle: - """ - Handle for the Interaction. - - This is used internally by the library to pass the Interaction to the - underlying Pact library. - """ - return self.__handle - - @property - def _interaction_part(self) -> pact.v3.ffi.InteractionPart: - return self.__interaction_part - - -class Pact: - """ - A Pact between a consumer and a provider. - - This class defines a Pact between a consumer and a provider. It is the - central class in Pact's framework, and is responsible for defining the - interactions between the two parties. - - One Pact instance should be created for each provider that a consumer - interacts with. This instance can then be used to define the interactions - between the two parties. - """ - - def __init__( - self, - consumer: str, - provider: str, - ) -> None: - """ - Initialise a new Pact. - - Args: - consumer: - Name of the consumer. - - provider: - Name of the provider. - """ - if not consumer: - msg = "Consumer name cannot be empty." - raise ValueError(msg) - if not provider: - msg = "Provider name cannot be empty." - raise ValueError(msg) - - self._consumer = consumer - self._provider = provider - self._interactions: Set[Interaction] = set() - self._handle: pact.v3.ffi.PactHandle = pact.v3.ffi.new_pact( - consumer, - provider, - ) - - def __str__(self) -> str: - """ - Informal string representation of the Pact. - """ - return f"{self.consumer} -> {self.provider}" - - def __repr__(self) -> str: - """ - Information-rich string representation of the Pact. - """ - return "".format( - ", ".join( - [ - f"consumer={self.consumer!r}", - f"provider={self.provider!r}", - f"handle={self._handle!r}", - ], - ), - ) - - @property - def consumer(self) -> str: - """ - Consumer name. - """ - return self._consumer - - @property - def provider(self) -> str: - """ - Provider name. - """ - return self._provider - - def with_specification( - self, - version: str | pact.v3.ffi.PactSpecification, - ) -> Self: - """ - Set the Pact specification version. - - The Pact specification version indicates the features which are - supported by the Pact, and certain default behaviours. - - Args: - version: - Pact specification version. The can be either a string or a - [`PactSpecification`][pact.v3.ffi.PactSpecification] instance. - - The version string is case insensitive and has an optional `v` - prefix. - """ - if isinstance(version, str): - version = version.upper().replace(".", "_") - if version.startswith("V"): - version = pact.v3.ffi.PactSpecification[version] - else: - version = pact.v3.ffi.PactSpecification["V" + version] - pact.v3.ffi.with_specification(self._handle, version) - return self - - def using_plugin(self, name: str, version: str | None = None) -> Self: - """ - Add a plugin to be used by the test. - - Plugins extend the functionality of Pact. - - Args: - name: - Name of the plugin. - - version: - Version of the plugin. This is optional and can be `None`. - """ - pact.v3.ffi.using_plugin(self._handle, name, version) - return self - - def with_metadata( - self, - namespace: str, - metadata: dict[str, str], - ) -> Self: - """ - Set additional metadata for the Pact. - - A common use for this function is to add information about the client - library (name, version, hash, etc.) to the Pact. - - Args: - namespace: - Namespace for the metadata. This is used to group the metadata - together. - - metadata: - Key-value pairs of metadata to add to the Pact. - """ - for k, v in metadata.items(): - pact.v3.ffi.with_pact_metadata(self._handle, namespace, k, v) - return self - - @overload - def upon_receiving( - self, - description: str, - interaction: Literal["HTTP"] = ..., - ) -> HttpInteraction: ... - - @overload - def upon_receiving( - self, - description: str, - interaction: Literal["Async"], - ) -> AsyncMessageInteraction: ... - - @overload - def upon_receiving( - self, - description: str, - interaction: Literal["Sync"], - ) -> SyncMessageInteraction: ... - - def upon_receiving( - self, - description: str, - interaction: Literal["HTTP", "Sync", "Async"] = "HTTP", - ) -> HttpInteraction | AsyncMessageInteraction | SyncMessageInteraction: - """ - Create a new Interaction. - - This is an alias for [`interaction(...)`][pact.v3.Pact.interaction]. - - Args: - description: - Description of the interaction. This must be unique - within the Pact. - - interaction: - Type of interaction. Defaults to `HTTP`. This must be one of - `HTTP`, `Async`, or `Sync`. - """ - if interaction == "HTTP": - return HttpInteraction(self._handle, description) - if interaction == "Async": - return AsyncMessageInteraction(self._handle, description) - if interaction == "Sync": - return SyncMessageInteraction(self._handle, description) - - msg = f"Invalid interaction type: {interaction}" - raise ValueError(msg) - - def serve( # noqa: PLR0913 - self, - addr: str = "localhost", - port: int = 0, - transport: str = "http", - transport_config: str | None = None, - *, - raises: bool = True, - ) -> PactServer: - """ - Return a mock server for the Pact. - - This function configures a mock server for the Pact. The mock server - is then started when the Pact is entered into a `with` block: - - ```python - pact = Pact("consumer", "provider") - with pact.serve() as srv: - ... - ``` - - Args: - addr: - Address to bind the mock server to. Defaults to `localhost`. - - port: - Port to bind the mock server to. Defaults to `0`, which will - select a random port. - - transport: - Transport to use for the mock server. Defaults to `HTTP`. - - transport_config: - Configuration for the transport. This is specific to the - transport being used and should be a JSON string. - - raises: Whether to raise an exception if there are mismatches - between the Pact and the server. If set to `False`, then the - mismatches must be handled manually. - - Returns: - A [`PactServer`][pact.v3.pact.PactServer] instance. - """ - return PactServer( - self._handle, - addr, - port, - transport, - transport_config, - raises=raises, - ) - - def messages(self) -> pact.v3.ffi.PactMessageIterator: - """ - Iterate over the messages in the Pact. - - This function returns an iterator over the messages in the Pact. This - is useful for validating the Pact against the provider. - - ```python - pact = Pact("consumer", "provider") - with pact.serve() as srv: - for message in pact.messages(): - # Validate the message against the provider. - ... - ``` - - Note that the Pact must be written to a file before the messages can be - iterated over. This is because the messages are not stored in memory, - but rather are streamed directly from the file. - """ - return pact.v3.ffi.pact_handle_get_message_iter(self._handle) - - @overload - def interactions( - self, - kind: Literal["HTTP"], - ) -> pact.v3.ffi.PactSyncHttpIterator: ... - - @overload - def interactions( - self, - kind: Literal["Sync"], - ) -> pact.v3.ffi.PactSyncMessageIterator: ... - - @overload - def interactions( - self, - kind: Literal["Async"], - ) -> pact.v3.ffi.PactMessageIterator: ... - - def interactions( - self, - kind: str = "HTTP", - ) -> ( - pact.v3.ffi.PactSyncHttpIterator - | pact.v3.ffi.PactSyncMessageIterator - | pact.v3.ffi.PactMessageIterator - ): - """ - Return an iterator over the Pact's interactions. - - The kind is used to specify the type of interactions that will be - iterated over. - """ - # TODO: Add an iterator for `All` interactions. - # https://github.com/pact-foundation/pact-python/issues/451 - if kind == "HTTP": - return pact.v3.ffi.pact_handle_get_sync_http_iter(self._handle) - if kind == "Sync": - return pact.v3.ffi.pact_handle_get_sync_message_iter(self._handle) - if kind == "Async": - return pact.v3.ffi.pact_handle_get_message_iter(self._handle) - msg = f"Unknown interaction type: {kind}" - raise ValueError(msg) - - def write_file( - self, - directory: Path | str | None = None, - *, - overwrite: bool = False, - ) -> None: - """ - Write out the pact to a file. - - This function should be called once all of the consumer tests have been - run. It writes the Pact to a file, which can then be used to validate - the provider. - - Args: - directory: - The directory to write the pact to. If the directory does not - exist, it will be created. The filename will be - automatically generated from the underlying Pact. - - overwrite: - If set to True, the file will be overwritten if it already - exists. Otherwise, the contents of the file will be merged with - the existing file. - """ - if directory is None: - directory = Path.cwd() - pact.v3.ffi.pact_handle_write_file( - self._handle, - directory, - overwrite=overwrite, - ) - - -class MismatchesError(Exception): - """ - Exception raised when there are mismatches between the Pact and the server. - """ - - def __init__(self, mismatches: list[dict[str, Any]]) -> None: - """ - Initialise a new MismatchesError. - - Args: - mismatches: - Mismatches between the Pact and the server. - """ - super().__init__(f"Mismatched interaction (count: {len(mismatches)})") - self._mismatches = mismatches - - @property - def mismatches(self) -> list[dict[str, Any]]: - """ - Mismatches between the Pact and the server. - """ - return self._mismatches - - -class PactServer: - """ - Pact Server. - - This class handles the lifecycle of the Pact mock server. It is responsible - for starting the mock server when the Pact is entered into a `with` block, - and stopping the mock server when the block is exited. - """ - - def __init__( # noqa: PLR0913 - self, - pact_handle: pact.v3.ffi.PactHandle, - host: str = "localhost", - port: int = 0, - transport: str = "HTTP", - transport_config: str | None = None, - *, - raises: bool = True, - ) -> None: - """ - Initialise a new Pact Server. - - This function should not be called directly. Instead, a Pact Server - should be created using the - [`serve(...)`][pact.v3.Pact.serve] method of a - [`Pact`][pact.v3.Pact] instance: - - ```python - pact = Pact("consumer", "provider") - with pact.serve(...) as srv: - ... - ``` - - Args: - pact_handle: - Handle for the Pact. - - host: - Hostname of IP for the mock server. - - port: - Port to bind the mock server to. The value of `0` will select a - random available port. - - transport: - Transport to use for the mock server. - - transport_config: - Configuration for the transport. This is specific to the - transport being used and should be a JSON string. - - raises: Whether or not to raise an exception if the server - is not matched upon exit. - """ - self._host = host - self._port = port - self._transport = transport - self._transport_config = transport_config - self._pact_handle = pact_handle - self._handle: None | pact.v3.ffi.PactServerHandle = None - self._raises = raises - - @property - def port(self) -> int: - """ - Port on which the server is running. - - If the server is not running, then this will be `0`. - """ - # Unlike the other properties, this value might be different to what was - # passed in to the constructor as the server can be started on a random - # port. - return self._handle.port if self._handle else 0 - - @property - def host(self) -> str: - """ - Address to which the server is bound. - """ - return self._host - - @property - def transport(self) -> str: - """ - Transport method. - """ - return self._transport - - @property - def url(self) -> URL: - """ - Base URL for the server. - """ - return URL(str(self)) - - @property - def matched(self) -> bool: - """ - Whether or not the server has been matched. - - This is `True` if the server has been matched, and `False` otherwise. - """ - if not self._handle: - msg = "The server is not running." - raise RuntimeError(msg) - return pact.v3.ffi.mock_server_matched(self._handle) - - @property - def mismatches(self) -> list[dict[str, Any]]: - """ - Mismatches between the Pact and the server. - - This is a string containing the mismatches between the Pact and the - server. If there are no mismatches, then this is an empty string. - """ - if not self._handle: - msg = "The server is not running." - raise RuntimeError(msg) - return pact.v3.ffi.mock_server_mismatches(self._handle) - - @property - def logs(self) -> str | None: - """ - Logs from the server. - - This is a string containing the logs from the server. If there are no - logs, then this is `None`. For this to be populated, the logging must - be configured to make use of the internal buffer. - """ - if not self._handle: - msg = "The server is not running." - raise RuntimeError(msg) - - try: - return pact.v3.ffi.mock_server_logs(self._handle) - except RuntimeError: - return None - - def __str__(self) -> str: - """ - URL for the server. - """ - return f"{self.transport}://{self.host}:{self.port}" - - def __repr__(self) -> str: - """ - Information-rich string representation of the Pact Server. - """ - return "".format( - ", ".join( - [ - f"transport={self.transport!r}", - f"host={self.host!r}", - f"port={self.port!r}", - f"handle={self._handle!r}", - f"pact={self._pact_handle!r}", - ], - ), - ) - - def __enter__(self) -> Self: - """ - Launch the server. - - Once the server is running, it is generally no possible to make - modifications to the underlying Pact. - """ - self._handle = pact.v3.ffi.create_mock_server_for_transport( - self._pact_handle, - self._host, - self._port, - self._transport, - self._transport_config, - ) - - return self - - def __exit__( - self, - _exc_type: type[BaseException] | None, - _exc_value: BaseException | None, - _traceback: TracebackType | None, - ) -> None: - """ - Stop the server. - - Raises: - MismatchesError: - If the server has not been fully matched and the server is - configured to raise an exception. - """ - if self._handle: - if self._raises and not self.matched: - raise MismatchesError(self.mismatches) - self._handle = None - - def __truediv__(self, other: str | object) -> URL: - """ - URL for the server. - """ - if isinstance(other, str): - return self.url / other - return NotImplemented - - def write_file( - self, - directory: str | Path | None = None, - *, - overwrite: bool = False, - ) -> None: - """ - Write out the pact to a file. - - Args: - directory: - The directory to write the pact to. If the directory does not - exist, it will be created. The filename will be - automatically generated from the underlying Pact. - - overwrite: - Whether or not to overwrite the file if it already exists. - """ - if not self._handle: - msg = "The server is not running." - raise RuntimeError(msg) - - directory = Path(directory) if directory else Path.cwd() - if not directory.exists(): - directory.mkdir(parents=True) - elif not directory.is_dir(): - msg = f"{directory} is not a directory" - raise ValueError(msg) - - pact.v3.ffi.write_pact_file( - self._handle, - str(directory), - overwrite=overwrite, - ) diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index 18bccac0b..231c01dd7 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -5,8 +5,59 @@ consumer and a provider. It defines the interactions between the two parties, and provides the functionality to verify that the interactions are satisfied. -For the roles of consumer and provider, see the documentation for the -`pact.v3.service` module. +As Pact is a consumer-driven contract testing tool, the consumer is responsible +for defining the interactions between the two parties. The provider is then +responsible for ensuring that these interactions are satisfied. + +## Usage + +The main class in this module is the [`Pact`][pact.v3.Pact] class. This class +defines the Pact between the consumer and the provider. It is responsible for +defining the interactions between the two parties. + +The general usage of this module is as follows: + +```python +from pact.v3 import Pact +import aiohttp + + +pact = Pact("consumer", "provider") + +interaction = pact.upon_receiving("a basic request") +interaction.given("user 123 exists") +interaction.with_request("GET", "/user/123") +interaction.will_respond_with(200) +interaction.with_header("Content-Type", "application/json") +interaction.with_body({"id": 123, "name": "Alice"}) + +with pact.serve() as srv: + async with aiohttp.ClientSession(srv.url) as session: + async with session.get("/user/123") as resp: + assert resp.status == 200 + assert resp.headers["Content-Type"] == "application/json" + data = await resp.json() + assert data == {"id": 123, "name": "Alice"} +``` + +The repeated calls to `interaction` can be chained together to define the +interaction in a more concise manner: + +```python +pact = Pact("consumer", "provider") + +( + pact.upon_receiving("a basic request") + .given("user 123 exists") + .with_request("GET", "/user/123") + .will_respond_with(200) + .with_header("Content-Type", "application/json") + .with_body({"id": 123, "name": "Alice"}) +) +``` + +Note that the parentheses are required to ensure that the method chaining works +correctly, as this form of method chaining is not typical in Python. """ from __future__ import annotations @@ -19,9 +70,9 @@ from yarl import URL import pact.v3.ffi -from pact.v3.interaction.async_message_interaction import AsyncMessageInteraction -from pact.v3.interaction.http_interaction import HttpInteraction -from pact.v3.interaction.sync_message_interaction import SyncMessageInteraction +from pact.v3.interaction._async_message_interaction import AsyncMessageInteraction +from pact.v3.interaction._http_interaction import HttpInteraction +from pact.v3.interaction._sync_message_interaction import SyncMessageInteraction if TYPE_CHECKING: from types import TracebackType @@ -44,9 +95,15 @@ class Pact: central class in Pact's framework, and is responsible for defining the interactions between the two parties. - One Pact instance should be created for each provider that a consumer - interacts with. This instance can then be used to define the interactions - between the two parties. + One `Pact` instance should be created for each provider that a consumer + interacts with. The methods on this class are used to define the broader + attributes of the Pact, such as the consumer and provider names, the Pact + specification, any plugins that are used, and any metadata that is attached + to the Pact. + + Each interaction between the consumer and the provider is defined through + the [`upon_receiving`][pact.v3.pact.Pact.upon_receiving] method, which + returns a sub-class of [`Interaction`][pact.v3.interaction.Interaction]. """ def __init__( @@ -211,8 +268,6 @@ def upon_receiving( """ Create a new Interaction. - This is an alias for [`interaction(...)`][pact.v3.Pact.interaction]. - Args: description: Description of the interaction. This must be unique @@ -404,6 +459,8 @@ def __init__(self, mismatches: list[dict[str, Any]]) -> None: super().__init__(f"Mismatched interaction (count: {len(mismatches)})") self._mismatches = mismatches + # TODO: Replace the list of dicts with a more structured object. + # https://github.com/pact-foundation/pact-python/issues/644 @property def mismatches(self) -> list[dict[str, Any]]: """ @@ -417,8 +474,35 @@ class PactServer: Pact Server. This class handles the lifecycle of the Pact mock server. It is responsible - for starting the mock server when the Pact is entered into a `with` block, - and stopping the mock server when the block is exited. + for starting the mock server when the Pact is entered into a [`with` + block](https://docs.python.org/3/reference/compound_stmts.html#with), and + stopping the mock server when the block is exited. + + Note that the server should not be started directly, but rather through the + [`serve(...)`][pact.v3.Pact.serve] method of a [`Pact`][pact.v3.Pact]: + + ```python + pact = Pact("consumer", "provider") + # Define interactions... + with pact.serve() as srv: + ... + ``` + + The URL for the server can be accessed through its + [`url`][pact.v3.pact.PactServer.url] attribute, which will be required in + order to point the consumer client to the mock server: + + ```python + pact = Pact("consumer", "provider") + with pact.serve() as srv: + api_client = MyApiClient(srv.url) + # Test the client... + ``` + + If the server is instantiated with `raises=True` (the default), then the + server will raise a `MismatchesError` if there are mismatches in any of the + interactions. If `raises=False`, then the mismatches must be handled + manually. """ def __init__( # noqa: PLR0913 @@ -435,17 +519,6 @@ def __init__( # noqa: PLR0913 """ Initialise a new Pact Server. - This function should not be called directly. Instead, a Pact Server - should be created using the - [`serve(...)`][pact.v3.Pact.serve] method of a - [`Pact`][pact.v3.Pact] instance: - - ```python - pact = Pact("consumer", "provider") - with pact.serve(...) as srv: - ... - ``` - Args: pact_handle: Handle for the Pact. @@ -520,6 +593,10 @@ def matched(self) -> bool: Whether or not the server has been matched. This is `True` if the server has been matched, and `False` otherwise. + + Raises: + RuntimeError: + If the server is not running. """ if not self._handle: msg = "The server is not running." @@ -533,6 +610,10 @@ def mismatches(self) -> list[dict[str, Any]]: This is a string containing the mismatches between the Pact and the server. If there are no mismatches, then this is an empty string. + + Raises: + RuntimeError: + If the server is not running. """ if not self._handle: msg = "The server is not running." @@ -547,6 +628,10 @@ def logs(self) -> str | None: This is a string containing the logs from the server. If there are no logs, then this is `None`. For this to be populated, the logging must be configured to make use of the internal buffer. + + Raises: + RuntimeError: + If the server is not running. """ if not self._handle: msg = "The server is not running." @@ -645,6 +730,13 @@ def write_file( overwrite: Whether or not to overwrite the file if it already exists. + + Raises: + RuntimeError: + If the server is not running. + + ValueError: + If the path specified is not a directory. """ if not self._handle: msg = "The server is not running." diff --git a/src/pact/v3/verifier.py b/src/pact/v3/verifier.py index 2e8ce85b5..ea6f95ed5 100644 --- a/src/pact/v3/verifier.py +++ b/src/pact/v3/verifier.py @@ -5,6 +5,64 @@ consumer. This is done by replaying interactions from the consumer against the provider, and ensuring that the provider's responses match the expectations set by the consumer. + +The interactions to be verified can be sourced either from local Pact files or +from a Pact Broker. The Verifier can be configured to filter interactions based +on their description and state, and to set the provider information and +transports. + +When performing the verification, Pact will replay the interactions from the +consumer against the provider and ensure that the provider's responses match the +expectations set by the consumer. + +!!! info + + The interface provided by this module could be improved. If you have any + suggestions, please consider creating a new [GitHub + discussion](https://github.com/pact-foundation/pact-python/discussions) or + reaching out over [Slack](https://slack.pact.io). + +## Usage + +The general usage of the Verifier is as follows: + +```python +from pact.v3 import Verifier + + +# In the case of local Pact files +verifier = Verifier().set_info("My Provider", url="http://localhost:8080") +verifier.add_source("pact/to/pacts/") +verifier.verify() + +# In the case of a Pact Broker +verifier = Verifier().set_info("My Provider", url="http://localhost:8080") +verifier.broker_source("https://broker.example.com/") +verifier.verify() +``` + +## State Handling + +In general, the consumer will write interactions assuming that the provider is +in a certain state. For example, a consumer requesting information about a user +with ID `123` will have specified `given("user with ID 123 exists")`. It is the +responsibility of the provider to ensure that this state is met before the +interaction is replayed. + +In order to change the provider's internal state, Pact relies on a callback +endpoint. The specific manner in which this endpoint is implemented is up to the +provider as it is highly dependent on the provider's architecture. + +One common approach is to define the endpoint during testing only, and for the +endpoint to [mock][unittest.mock] the expected calls to the database and/or +external services. This allows the provider to be tested in isolation from the +rest of the system, and assertions can be made about the calls made to the +endpoint. + +An alternative approach might be to run a dedicated service which is responsible +for writing to the database such that the provider can retrieve the expected +data. This approach is more complex, but could be useful in cases where test +databases are already in use. """ from __future__ import annotations @@ -76,9 +134,9 @@ def set_info( # noqa: PLR0913 HTTP(S) transport method is always added. For a provider which uses other protocols (such as message queues), the - [`add_provider_transport`][pact.v3.verifier.Verifier.add_provider_transport] - must be used. This method can be called multiple times to add multiple - transport methods. + [`add_transport`][pact.v3.verifier.Verifier.add_transport] must be used. + This method can be called multiple times to add multiple transport + methods. Args: name: @@ -160,7 +218,8 @@ def add_transport( If the provider supports multiple transport methods, or non-HTTP(S) methods, this method allows these additional transport methods to be - added. It can be called multiple times to add multiple transport methods. + added. It can be called multiple times to add multiple transport + methods. As some transport methods may not use ports, paths or schemes, these parameters are optional. @@ -171,10 +230,10 @@ def add_transport( - `http` for communications over HTTP(S). Note that when setting up the provider information in - [`set_provider_info`][pact.v3.verifier.Verifier.set_provider_info], - a HTTP transport method is always added and it is unlikely - that an additional HTTP transport method will be needed - unless the provider is running on additional ports. + [`set_info`][pact.v3.verifier.Verifier.set_info], a HTTP + transport method is always added and it is unlikely that an + additional HTTP transport method will be needed unless the + provider is running on additional ports. - `message` for non-plugin synchronous message-based communications. @@ -278,7 +337,7 @@ def set_state( Args: url: - The URL to which a `POST` request will be made to change the + The URL to which a `GET` request will be made to change the provider's internal state. teardown: From 481105ddf4a4034de600f262e2e24e277a1b57a1 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 2 Apr 2024 13:19:13 +1100 Subject: [PATCH 0317/1376] chore: remove redundant __all__ Signed-off-by: JP-Ellis --- src/pact/constants.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/pact/constants.py b/src/pact/constants.py index f29805940..f06a4d24f 100644 --- a/src/pact/constants.py +++ b/src/pact/constants.py @@ -10,14 +10,6 @@ import warnings from pathlib import Path -__all__ = [ - "BROKER_CLIENT_PATH", - "MESSAGE_PATH", - "MOCK_SERVICE_PATH", - "VERIFIER_PATH", -] - - _USE_SYSTEM_BINS = os.getenv("PACT_USE_SYSTEM_BINS", "").upper() in ("TRUE", "YES") _BIN_DIR = Path(__file__).parent.resolve() / "bin" From 5a4f6c259ccbf96e7871a2a2aa494cddd39a462d Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 2 Apr 2024 13:19:45 +1100 Subject: [PATCH 0318/1376] refactor: remove relative imports Signed-off-by: JP-Ellis --- src/pact/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pact/__init__.py b/src/pact/__init__.py index df07abf83..519086e7c 100644 --- a/src/pact/__init__.py +++ b/src/pact/__init__.py @@ -1,15 +1,15 @@ """Python methods for interactive with a Pact Mock Service.""" -from .broker import Broker -from .consumer import Consumer -from .matchers import EachLike, Like, SomethingLike, Term, Format -from .message_consumer import MessageConsumer -from .message_pact import MessagePact -from .message_provider import MessageProvider -from .pact import Pact -from .provider import Provider -from .verifier import Verifier +from pact.broker import Broker +from pact.consumer import Consumer +from pact.matchers import EachLike, Like, SomethingLike, Term, Format +from pact.message_consumer import MessageConsumer +from pact.message_pact import MessagePact +from pact.message_provider import MessageProvider +from pact.pact import Pact +from pact.provider import Provider +from pact.verifier import Verifier -from .__version__ import __version__, __version_tuple__ +from pact.__version__ import __version__, __version_tuple__ __url__ = "https://github.com/pactflow/accord" __license__ = "MIT" From b8fb270cac99accad00115c5ce8324a99a4b47de Mon Sep 17 00:00:00 2001 From: Joe Joyce Date: Thu, 28 Mar 2024 11:08:32 +0000 Subject: [PATCH 0319/1376] chore(docs): update examples/readme.md Fix typos Authored-By: JosephBJoyce Cherry-Picked-By: JP-Ellis --- examples/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/README.md b/examples/README.md index 2a41b85c4..0034bdafa 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,7 +8,7 @@ Assuming you have [hatch](https://hatch.pypa.io/latest/) installed, the example hatch run example ``` -The code within the examples is intended to be well documented and you are encouraged to look through the code as well (or submit a PR if anything is unclear!). +The code within the examples is intended to be well-documented and you are encouraged to look through the code as well (or submit a PR if anything is unclear!). ## Overview @@ -34,7 +34,7 @@ sequenceDiagram To test this interaction naively would require both the consumer and provider to be running at the same time. While this is straightforward in the above example, this quickly becomes impractical as the number of interactions grows between many microservices. Pact solves this by allowing the consumer and provider to be tested independently. -Pact achieves this be mocking the other side of the interaction: +Pact achieves this by mocking the other side of the interaction:
@@ -65,7 +65,7 @@ sequenceDiagram
-In the first stage, the consumer defines a number of interactions in the form below. Pact sets up a mock server that will respond to the requests as defined by the consumer. All these interactions, containing both the request and expected response, are all sent to the Pact Broker. +In the first stage, the consumer defines a number of interactions in the form below. Pact sets up a mock server that will respond to the requests as defined by the consumer. All these interactions, containing both the request and expected response, are sent to the Pact Broker. > Given {provider state}
> Upon receiving {description}
@@ -74,7 +74,7 @@ In the first stage, the consumer defines a number of interactions in the form be In the second stage, the provider retrieves the interactions from the Pact Broker. It then sets up a mock client that will make the requests as defined by the consumer. Pact then verifies that the responses from the provider match the expected responses defined by the consumer. -In this way, Pact is consumer driven and can ensure that the provider is compatible with the consumer. While this example showcases both sides in Python, this is absolutely not required. The provider could be written in any language, and satisfy contracts from a number of consumers all written in different languages. +In this way, Pact is consumer-driven and can ensure that the provider is compatible with the consumer. While this example showcases both sides in Python, this is absolutely not required. The provider could be written in any language, and satisfy contracts from a number of consumers all written in different languages. ### Consumer @@ -97,9 +97,9 @@ expected: dict[str, Any] = { ### Provider -This example showcases to different providers, one written in Flask and one written in FastAPI. Both are simple Python web servers that respond to a HTTP GET request. The Flask provider is defined in [`src/flask.py`][examples.src.flask] and the FastAPI provider is defined in [`src/fastapi.py`][examples.src.fastapi]. The tests for the providers are defined in [`tests/test_01_provider_flask.py`][examples.tests.test_01_provider_flask] and [`tests/test_01_provider_fastapi.py`][examples.tests.test_01_provider_fastapi]. +This example showcases two different providers; one written in Flask and one written in FastAPI. Both are simple Python web servers that respond to a HTTP GET request. The Flask provider is defined in [`src/flask.py`][examples.src.flask] and the FastAPI provider is defined in [`src/fastapi.py`][examples.src.fastapi]. The tests for the providers are defined in [`tests/test_01_provider_flask.py`][examples.tests.test_01_provider_flask] and [`tests/test_01_provider_fastapi.py`][examples.tests.test_01_provider_fastapi]. -Unlike the consumer side, the provider side is responsible to responding to the interactions defined by the consumers. In this regard, the provider testing is rather simple: +Unlike the consumer side, the provider side is responsible for responding to the interactions defined by the consumers. In this regard, the provider testing is rather simple: ```py code, _ = verifier.verify_with_broker( From ca6072db81d7e36cab27351b7296e87129762d97 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 5 Apr 2024 17:26:37 +1100 Subject: [PATCH 0320/1376] docs: fix links to docs/ Due to the processing that is done on files outside of `docs/`, links to file _inside_ the `docs/` directory do not work. This adds a hook to MkDocs which rewrites these links. Signed-off-by: JP-Ellis --- docs/scripts/rewrite-docs-links.py | 58 ++++++++++++++++++++++++++++++ mkdocs.yml | 3 ++ 2 files changed, 61 insertions(+) create mode 100644 docs/scripts/rewrite-docs-links.py diff --git a/docs/scripts/rewrite-docs-links.py b/docs/scripts/rewrite-docs-links.py new file mode 100644 index 000000000..d0364aa89 --- /dev/null +++ b/docs/scripts/rewrite-docs-links.py @@ -0,0 +1,58 @@ +""" +Rewrite links to docs. + +This hook is used to rewrite links within the documentation. Due to the way +Markdown files are collected across the repository (specifically, within `docs/` +and outside of `docs/`), links that cross this boundary don't already work +correctly. + +This hook is used to rewrite links dynamically. +""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +import mkdocs.plugins + +if TYPE_CHECKING: + from mkdocs.config.defaults import MkDocsConfig + from mkdocs.structure.files import Files + from mkdocs.structure.pages import Page + + +@mkdocs.plugins.event_priority(-50) +def on_page_markdown( + markdown: str, + page: Page, # noqa: ARG001 + config: MkDocsConfig, # noqa: ARG001 + files: Files, # noqa: ARG001 +) -> str | None: + """ + Rewrite links to docs. + + Performs a simple regex substitution on the Markdown content. Specifically, + any link to a file within `docs/{path}` is rewritten to just `/{path}`, and + any links containing `../docs/..` are rewritten to just `../..`. + + This is clearly fragile, but until a better solution is needed, this should + be sufficient. + """ + # Find all links that start with `docs/` and rewrite them. + markdown = re.sub( + r"\]\((?Pdocs/[^)]+)\)", + lambda match: f"]({match.group('link')[5:]})", + markdown, + count=0, + flags=re.MULTILINE, + ) + + # Find links that have an embedded `/docs/` and rewrite them. + return re.sub( + r"\]\((?P[^)]+/docs/[^)]+)\)", + lambda match: f"]({match.group('link').replace('/docs/', '/')})", + markdown, + count=0, + flags=re.MULTILINE, + ) diff --git a/mkdocs.yml b/mkdocs.yml index 520dd0199..d35686741 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,9 @@ repo_url: https://github.com/pact-foundation/pact-python edit_uri: edit/develop/docs +hooks: + - docs/scripts/rewrite-docs-links.py + plugins: - search - literate-nav: From 10c0b8db4b9b341c6cd80e5fef9c793b187586e4 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 11 Apr 2024 09:15:06 +1000 Subject: [PATCH 0321/1376] chore(ci): update environment variables Specifically, adding `FORCE_COLOR` to ensure we have coloured output (it is prettier afterall), and making sure `HATCH_VERBOSE` is always enabled in case there's an error. Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 3 ++- .github/workflows/docs.yml | 3 ++- .github/workflows/test.yml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f87f70c52..f7f5313c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,8 @@ concurrency: env: STABLE_PYTHON_VERSION: "3.12" - HATCH_VERBOSE: 1 + HATCH_VERBOSE: "1" + FORCE_COLOR: "1" CIBW_BUILD_FRONTEND: build jobs: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0092fad92..b86a04a33 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,7 +5,8 @@ on: env: STABLE_PYTHON_VERSION: "3.12" - PYTEST_ADDOPTS: --color=yes + FORCE_COLOR: "1" + HATCH_VERBOSE: "1" jobs: build: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eb247019b..d0081876f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,8 @@ concurrency: env: STABLE_PYTHON_VERSION: "3.12" PYTEST_ADDOPTS: --color=yes - HATCH_VERBOSE: 1 + HATCH_VERBOSE: "1" + FORCE_COLOR: "1" jobs: test-container: From 94984f403ca88f76611b74d8238fe8f70889b739 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 11 Apr 2024 09:17:24 +1000 Subject: [PATCH 0322/1376] chore(docs): only publish from master Signed-off-by: JP-Ellis --- .github/workflows/docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b86a04a33..52509029e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -41,6 +41,8 @@ jobs: publish: name: Publish docs + if: github.ref == 'refs/heads/master' + needs: build runs-on: ubuntu-latest permissions: From 2cea12117fc7422c235d96ad45e4b7991eb528bb Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 11 Apr 2024 10:34:30 +1000 Subject: [PATCH 0323/1376] docs: add social image support MkDocs material has a plugin to help create nice social previews. For more information, see: https://squidfunk.github.io/mkdocs-material/plugins/social/ Signed-off-by: JP-Ellis --- mkdocs.yml | 1 + pyproject.toml | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index d35686741..2fcfd47c2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -57,6 +57,7 @@ plugins: annotations_path: brief show_signature: true show_signature_annotations: true + - social markdown_extensions: # Python Markdown diff --git a/pyproject.toml b/pyproject.toml index 98c99e83e..2376c5fac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,12 +68,12 @@ devel-types = [ "types-requests ~=2.0", ] devel-docs = [ - "mkdocs ~= 1.5", - "mkdocs-material ~= 9.4", - "mkdocs_gen_files ~= 0.5", - "mkdocs-literate-nav ~= 0.6", - "mkdocs-section-index ~= 0.3", - "mkdocstrings[python] ~= 0.23", + "mkdocs ~= 1.5", + "mkdocs-material[imaging] ~= 9.4", + "mkdocs_gen_files ~= 0.5", + "mkdocs-literate-nav ~= 0.6", + "mkdocs-section-index ~= 0.3", + "mkdocstrings[python] ~= 0.23", ] devel-test = [ "aiohttp[speedups] ~=3.0", From 8e8c25504c2be07741da30fa56883925a3bb71ad Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 11 Apr 2024 10:42:07 +1000 Subject: [PATCH 0324/1376] docs: add blog post about v2.2 Signed-off-by: JP-Ellis --- docs/SUMMARY.md | 1 + docs/blog/.authors.yml | 10 +++ docs/blog/index.md | 1 + ... sneak peek into the pact python future.md | 68 +++++++++++++++++++ mkdocs.yml | 3 + 5 files changed, 83 insertions(+) create mode 100644 docs/blog/.authors.yml create mode 100644 docs/blog/index.md create mode 100644 docs/blog/posts/2024/04-11 a sneak peek into the pact python future.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 0f84862b0..892e74a0a 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -8,3 +8,4 @@ - [Contributing](CONTRIBUTING.md) - [Pact](pact/) - [Examples](examples/) +- [Blog](blog/index.md) diff --git a/docs/blog/.authors.yml b/docs/blog/.authors.yml new file mode 100644 index 000000000..8c6fb0a9b --- /dev/null +++ b/docs/blog/.authors.yml @@ -0,0 +1,10 @@ +authors: + JP-Ellis: + name: Joshua Ellis + description: | + Joshua Ellis is a software engineer at SmartBear and open-source + contributor. He has been helping upgrade the Pact Python library to + v3 which will make use of the Rust FFI library. + avatar: https://gravatar.com/avatar/d82f8662c8eadcbfef09de9872b3f060ce4ead0fbae2e58a94435f31fcd00e55?s=512 + slug: jp-ellis + url: https://jpellis.me diff --git a/docs/blog/index.md b/docs/blog/index.md new file mode 100644 index 000000000..05761ac57 --- /dev/null +++ b/docs/blog/index.md @@ -0,0 +1 @@ +# Blog diff --git a/docs/blog/posts/2024/04-11 a sneak peek into the pact python future.md b/docs/blog/posts/2024/04-11 a sneak peek into the pact python future.md new file mode 100644 index 000000000..f082130cf --- /dev/null +++ b/docs/blog/posts/2024/04-11 a sneak peek into the pact python future.md @@ -0,0 +1,68 @@ +--- +authors: + - JP-Ellis +date: + created: 2024-04-11 +--- + +# A Sneak Peek into the Pact Python Future + +We are thrilled to announce the release of [Pact Python `v2.2`](https://github.com/pact-foundation/pact-python/releases/tag/v2.2.0), a significant milestone that not only improves upon the existing features but also offers an exclusive preview into the future of contract testing with Python. + +## A Glimpse Ahead with [`pact.v3`][pact.v3] + +The work is taking shape in a branch-new module – [`pact.v3`][pact.v3] – that serves as an early preview of what will become Pact Python `v3`. This will provide full support for Pact Specifications `v3` and `v4`. + +This new version harnesses the power of Rust's foreign function interface (FFI) library, promising enhanced performance and reliability. It will also make it easier to incorporate upstream changes in the future. Although it's just a sneak peek, it's an open invitation for you to explore what's coming and contribute to shaping its final form. + + + +## Your Feedback Is Invaluable + +The journey toward perfection is never solitary. We count on your insights and experiences to refine our offerings. If you run into any hiccups or have thoughts you'd like to share: + +- Report issues on our GitHub page: [Pact Python Issues](https://github.com/pact-foundation/pact-python/issues). +- Join the conversation on GitHub discussions: [Pact Python Discussions](https://github.com/pact-foundation/pact-python/discussions). +- Connect with us on Slack: [Pact Foundation Slack](https://slack.pact.io/). + +We eagerly await your input! + +## The Roadmap Ahead + +Transitioning to a new version can be daunting; thus, we've planned a staged migration: + +### :construction: Stage 1 (from v2.2) + +- The existing library remains operational with continued support for minor updates. +- The new `pact.v3` is available for trial but should be used cautiously as changes are expected. +- It's not recommended for production use yet, but feedback from experimentation is encouraged. +- Expect [`PendingDeprecationWarning`](https://docs.python.org/3/library/exceptions.html#PendingDeprecationWarning) alerts when using the current library. + +### :hammer_and_wrench: Stage 2 (from v2.3, to be confirmed) + +- The `pact.v3` module is anticipated to stabilize and we urge users to start planning their migration. +- Comprehensive migration guidance will be provided for a seamless transition. +- More assertive [`DeprecationWarning`](https://docs.python.org/3/library/exceptions.html#DeprecationWarning) notifications will prompt users to switch to the new module. +- This phase will provide ample time, likely spanning a few months, for users to adapt. + +### :rocket: Stage 3 (from v3) + +- The `pact.v3` module graduates to simply `pact`, signaling its readiness as the primary library. + - Migrators from `pact.v3` can expect minimal effort adjustments – mostly a find-and-replace task from `pact.v3` to `pact`. + - Any necessary breaking changes identified during Stage 2 will be implemented, with detailed guidance provided. +- The original library moves under the `pact.v2` umbrella. + - This significant shift means that code written for Pact Python v2 will require attention to function with `v3`. + - Users loyal to `v2` must update their imports to accommodate the new `pact.v2` scope. + - With its move, `pact.v2` steps into the sunset of its lifecycle, focusing on critical fixes until its eventual retirement. + +## Embrace the Evolution + +Ready to dive in? Check out Pact Python `v2.2` today and start exploring what's ahead with `pact.v3`. As developers and testers, your role in this evolution is pivotal. By engaging with this preview and sharing your findings, you help us refine and perfect the tools you rely on. + +We want to make this transition as smooth as possible for our community. We encourage you to explore, test drive the new features, and share your experiences. + +Stay tuned for more updates as we progress through each stage of this exciting journey! + +Lastly, a big thank you to all our contributors! + +Happy testing! diff --git a/mkdocs.yml b/mkdocs.yml index 2fcfd47c2..e0319c477 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -58,6 +58,9 @@ plugins: show_signature: true show_signature_annotations: true - social + - blog: + blog_toc: true + post_excerpt: required markdown_extensions: # Python Markdown From b2ada0a47452278984fe61cda98cc3d921a1fca8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 23:34:50 +0000 Subject: [PATCH 0325/1376] chore(deps): pin dependencies --- .github/workflows/docs.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 52509029e..77e67c3d8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,12 +16,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} cache: pip @@ -34,7 +34,7 @@ jobs: hatch run mkdocs build - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@a753861a5debcf57bf8b404356158c8e1e33150c # v2 with: path: site @@ -56,4 +56,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@de14547edc9944350dc0481aa5b7afb08e75f254 # v2 From 97df2d002870b476dc92941180530558d1ca0689 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 23:19:02 +0000 Subject: [PATCH 0326/1376] chore(deps): update codecov/codecov-action digest to 8450866 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d0081876f..e28f75d49 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,7 +89,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@7afa10ed9b269c561c2336fd862446844e0cbf71 # v4 + uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # v4 with: token: ${{ secrets.CODECOV_TOKEN }} From b290d78cad90789dfb9d62a7672dd0ff882355e5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 00:47:09 +0000 Subject: [PATCH 0327/1376] chore(deps): update actions/deploy-pages action to v4 --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 77e67c3d8..c11b4f305 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -56,4 +56,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@de14547edc9944350dc0481aa5b7afb08e75f254 # v2 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 From 7cfe950959580b3baaa724809b5bd2a83b431cb9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 00:47:12 +0000 Subject: [PATCH 0328/1376] chore(deps): update actions/setup-python action to v5 --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c11b4f305..38a0da645 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,7 +21,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} cache: pip From 743f34740c7ea80925f9ab19713abddfe4903ff1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 00:47:15 +0000 Subject: [PATCH 0329/1376] chore(deps): update actions/upload-pages-artifact action to v3 --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 38a0da645..00e6e567b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,7 +34,7 @@ jobs: hatch run mkdocs build - name: Upload artifact - uses: actions/upload-pages-artifact@a753861a5debcf57bf8b404356158c8e1e33150c # v2 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: path: site From 6970b4128ed758ce6f59229424e14343846f8dc5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 11 Apr 2024 12:52:56 +1000 Subject: [PATCH 0330/1376] feat: upgrade FFI to 0.4.19 Signed-off-by: JP-Ellis --- hatch_build.py | 4 +- src/pact/v3/ffi.py | 587 ++++++++++--------- src/pact/v3/interaction/_base.py | 26 + src/pact/v3/interaction/_http_interaction.py | 4 +- 4 files changed, 335 insertions(+), 286 deletions(-) diff --git a/hatch_build.py b/hatch_build.py index b135d3809..9842fb6a4 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -36,7 +36,7 @@ # Latest version available at: # https://github.com/pact-foundation/pact-reference/releases -PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.18") +PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.19") PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{machine}.{ext}" @@ -254,7 +254,7 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 platform = next(sys_tags()).platform if platform.startswith("macosx"): - os = "osx" + os = "macos" if platform.endswith("arm64"): machine = "aarch64-apple-darwin" elif platform.endswith("x86_64"): diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 7c37e624b..e23d94b86 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -134,7 +134,7 @@ class InteractionHandle: Handle to a HTTP Interaction. [Rust - `InteractionHandle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/mock_server/handles/struct.InteractionHandle.html) + `InteractionHandle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/mock_server/handles/struct.InteractionHandle.html) """ def __init__(self, ref: int) -> None: @@ -225,7 +225,7 @@ class PactHandle: Handle to a Pact. [Rust - `PactHandle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/mock_server/handles/struct.PactHandle.html) + `PactHandle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/mock_server/handles/struct.PactHandle.html) """ def __init__(self, ref: int) -> None: @@ -539,7 +539,7 @@ class VerifierHandle: """ Handle to a Verifier. - [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/verifier/handle/struct.VerifierHandle.html) + [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/verifier/handle/struct.VerifierHandle.html) """ def __init__(self, ref: cffi.FFI.CData) -> None: @@ -575,7 +575,7 @@ class ExpressionValueType(Enum): """ Expression Value Type. - [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/models/expressions/enum.ExpressionValueType.html) + [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/models/expressions/enum.ExpressionValueType.html) """ UNKNOWN = lib.ExpressionValueType_Unknown @@ -602,7 +602,7 @@ class GeneratorCategory(Enum): """ Generator Category. - [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/models/generators/enum.GeneratorCategory.html) + [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/models/generators/enum.GeneratorCategory.html) """ METHOD = lib.GeneratorCategory_METHOD @@ -630,7 +630,7 @@ class InteractionPart(Enum): """ Interaction Part. - [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/mock_server/handles/enum.InteractionPart.html) + [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/mock_server/handles/enum.InteractionPart.html) """ REQUEST = lib.InteractionPart_Request @@ -676,7 +676,7 @@ class MatchingRuleCategory(Enum): """ Matching Rule Category. - [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) + [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) """ METHOD = lib.MatchingRuleCategory_METHOD @@ -705,7 +705,7 @@ class PactSpecification(Enum): """ Pact Specification. - [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/models/pact_specification/enum.PactSpecification.html) + [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/models/pact_specification/enum.PactSpecification.html) """ UNKNOWN = lib.PactSpecification_Unknown @@ -758,7 +758,7 @@ class _StringResult(Enum): """ Internal enum from Pact FFI. - [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/mock_server/enum.StringResult.html) + [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/mock_server/enum.StringResult.html) """ FAILED = lib.StringResult_Failed @@ -905,7 +905,7 @@ def version() -> str: """ Return the version of the pact_ffi library. - [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_version) + [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_version) Returns: The version of the pact_ffi library as a string, in the form of `x.y.z`. @@ -925,7 +925,7 @@ def init(log_env_var: str) -> None: tracing subscriber. [Rust - `pactffi_init`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_init) + `pactffi_init`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_init) # Safety @@ -942,7 +942,7 @@ def init_with_log_level(level: str = "INFO") -> None: tracing subscriber. [Rust - `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_init_with_log_level) + `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_init_with_log_level) Args: level: @@ -962,7 +962,7 @@ def enable_ansi_support() -> None: On non-Windows platforms, this function is a no-op. [Rust - `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_enable_ansi_support) + `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_enable_ansi_support) # Safety @@ -980,7 +980,7 @@ def log_message( Log using the shared core logging facility. [Rust - `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_log_message) + `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_message) This is useful for callers to have a single set of logs. @@ -1014,7 +1014,7 @@ def match_message(msg_1: Message, msg_2: Message) -> Mismatches: If the messages match, the returned collection will be empty. [Rust - `pactffi_match_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_match_message) + `pactffi_match_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_match_message) """ raise NotImplementedError @@ -1024,7 +1024,7 @@ def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: Get an iterator over mismatches. [Rust - `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatches_get_iter) + `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatches_get_iter) """ raise NotImplementedError @@ -1033,7 +1033,7 @@ def mismatches_delete(mismatches: Mismatches) -> None: """ Delete mismatches. - [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatches_delete) + [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatches_delete) """ raise NotImplementedError @@ -1042,7 +1042,7 @@ def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: """ Get the next mismatch from a mismatches iterator. - [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatches_iter_next) + [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatches_iter_next) Returns a null pointer if no mismatches remain. """ @@ -1053,7 +1053,7 @@ def mismatches_iter_delete(iter: MismatchesIterator) -> None: """ Delete a mismatches iterator when you're done with it. - [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatches_iter_delete) + [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatches_iter_delete) """ raise NotImplementedError @@ -1062,7 +1062,7 @@ def mismatch_to_json(mismatch: Mismatch) -> str: """ Get a JSON representation of the mismatch. - [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatch_to_json) + [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_to_json) """ raise NotImplementedError @@ -1071,7 +1071,7 @@ def mismatch_type(mismatch: Mismatch) -> str: """ Get the type of a mismatch. - [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatch_type) + [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_type) """ raise NotImplementedError @@ -1080,7 +1080,7 @@ def mismatch_summary(mismatch: Mismatch) -> str: """ Get a summary of a mismatch. - [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatch_summary) + [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_summary) """ raise NotImplementedError @@ -1089,7 +1089,7 @@ def mismatch_description(mismatch: Mismatch) -> str: """ Get a description of a mismatch. - [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatch_description) + [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_description) """ raise NotImplementedError @@ -1098,7 +1098,7 @@ def mismatch_ansi_description(mismatch: Mismatch) -> str: """ Get an ANSI-compatible description of a mismatch. - [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mismatch_ansi_description) + [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_ansi_description) """ raise NotImplementedError @@ -1108,7 +1108,7 @@ def get_error_message(length: int = 1024) -> str | None: Provide the error message from `LAST_ERROR` to the calling C code. [Rust - `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_get_error_message) + `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_get_error_message) This function should be called after any other function in the pact_matching FFI indicates a failure with its own error message, if the caller wants to @@ -1161,7 +1161,7 @@ def log_to_stdout(level_filter: LevelFilter) -> int: """ Convenience function to direct all logging to stdout. - [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_log_to_stdout) + [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_stdout) """ raise NotImplementedError @@ -1171,7 +1171,7 @@ def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: Convenience function to direct all logging to stderr. [Rust - `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_log_to_stderr) + `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_stderr) Args: level_filter: @@ -1195,7 +1195,7 @@ def log_to_file(file_name: str, level_filter: LevelFilter) -> int: Convenience function to direct all logging to a file. [Rust - `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_log_to_file) + `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_file) # Safety @@ -1209,7 +1209,7 @@ def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: """ Convenience function to direct all logging to a task local memory buffer. - [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_log_to_buffer) + [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_buffer) """ if isinstance(level_filter, str): level_filter = LevelFilter[level_filter.upper()] @@ -1223,7 +1223,7 @@ def logger_init() -> None: """ Initialize the FFI logger with no sinks. - [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_logger_init) + [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_logger_init) This initialized logger does nothing until `pactffi_logger_apply` has been called. @@ -1245,7 +1245,7 @@ def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: Attach an additional sink to the thread-local logger. [Rust - `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_logger_attach_sink) + `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_logger_attach_sink) This logger does nothing until `pactffi_logger_apply` has been called. @@ -1292,7 +1292,7 @@ def logger_apply() -> int: Apply the previously configured sinks and levels to the program. [Rust - `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_logger_apply) + `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_logger_apply) If no sinks have been setup, will set the log level to info and the target to standard out. @@ -1308,7 +1308,7 @@ def fetch_log_buffer(log_id: str) -> str: Fetch the in-memory logger buffer contents. [Rust - `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_fetch_log_buffer) + `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_fetch_log_buffer) This will only have any contents if the `buffer` sink has been configured to log to. The contents will be allocated on the heap and will need to be freed @@ -1334,7 +1334,7 @@ def parse_pact_json(json: str) -> Pact: Parses the provided JSON into a Pact model. [Rust - `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_parse_pact_json) + `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_parse_pact_json) The returned Pact model must be freed with the `pactffi_pact_model_delete` function when no longer needed. @@ -1351,7 +1351,7 @@ def pact_model_delete(pact: Pact) -> None: """ Frees the memory used by the Pact model. - [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_model_delete) + [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_model_delete) """ raise NotImplementedError @@ -1361,7 +1361,7 @@ def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: Returns an iterator over all the interactions in the Pact. [Rust - `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_model_interaction_iterator) + `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_model_interaction_iterator) The iterator will have to be deleted using the `pactffi_pact_interaction_iter_delete` function. The iterator will contain a @@ -1380,7 +1380,7 @@ def pact_spec_version(pact: Pact) -> PactSpecification: """ Returns the Pact specification enum that the Pact is for. - [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_spec_version) + [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_spec_version) """ raise NotImplementedError @@ -1389,7 +1389,7 @@ def pact_interaction_delete(interaction: PactInteraction) -> None: """ Frees the memory used by the Pact interaction model. - [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_delete) + [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_delete) """ raise NotImplementedError @@ -1398,7 +1398,7 @@ def async_message_new() -> AsynchronousMessage: """ Get a mutable pointer to a newly-created default message on the heap. - [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_new) + [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_new) # Safety @@ -1415,7 +1415,7 @@ def async_message_delete(message: AsynchronousMessage) -> None: """ Destroy the `AsynchronousMessage` being pointed to. - [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_delete) + [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_delete) """ raise NotImplementedError @@ -1425,7 +1425,7 @@ def async_message_get_contents(message: AsynchronousMessage) -> MessageContents: Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. [Rust - `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_contents) + `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents) # Safety @@ -1444,7 +1444,7 @@ def async_message_get_contents_str(message: AsynchronousMessage) -> str: """ Get the message contents of an `AsynchronousMessage` in string form. - [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_contents_str) + [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents_str) # Safety @@ -1471,7 +1471,7 @@ def async_message_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_set_contents_str) + `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_set_contents_str) - `message` - the message to set the contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -1499,7 +1499,7 @@ def async_message_get_contents_length(message: AsynchronousMessage) -> int: Get the length of the contents of a `AsynchronousMessage`. [Rust - `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_contents_length) + `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents_length) # Safety @@ -1518,7 +1518,7 @@ def async_message_get_contents_bin(message: AsynchronousMessage) -> str: Get the contents of an `AsynchronousMessage` as bytes. [Rust - `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_contents_bin) + `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents_bin) # Safety @@ -1545,7 +1545,7 @@ def async_message_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_set_contents_bin) + `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_set_contents_bin) * `message` - the message to set the contents for * `contents` - pointer to contents to copy from @@ -1572,7 +1572,7 @@ def async_message_get_description(message: AsynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_description) + `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_description) # Safety @@ -1598,7 +1598,7 @@ def async_message_set_description( """ Write the `description` field on the `AsynchronousMessage`. - [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_set_description) + [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_set_description) # Safety @@ -1623,7 +1623,7 @@ def async_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_provider_state) + `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_provider_state) # Safety @@ -1648,7 +1648,7 @@ def async_message_get_provider_state_iter( """ Get an iterator over provider states. - [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) + [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) # Safety @@ -1665,7 +1665,7 @@ def consumer_get_name(consumer: Consumer) -> str: r""" Get a copy of this consumer's name. - [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_consumer_get_name) + [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_consumer_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -1711,7 +1711,7 @@ def pact_get_consumer(pact: Pact) -> Consumer: `pactffi_pact_consumer_delete` when no longer required. [Rust - `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_get_consumer) + `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_get_consumer) # Errors @@ -1725,7 +1725,7 @@ def pact_consumer_delete(consumer: Consumer) -> None: """ Frees the memory used by the Pact consumer. - [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_consumer_delete) + [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_consumer_delete) """ raise NotImplementedError @@ -1734,7 +1734,7 @@ def message_contents_get_contents_str(contents: MessageContents) -> str: """ Get the message contents in string form. - [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_get_contents_str) + [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_str) # Safety @@ -1761,7 +1761,7 @@ def message_contents_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_set_contents_str) + `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_set_contents_str) * `contents` - the message contents to set the contents for * `contents_str` - pointer to contents to copy from. Must be a valid @@ -1788,7 +1788,7 @@ def message_contents_get_contents_length(contents: MessageContents) -> int: """ Get the length of the message contents. - [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_get_contents_length) + [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_length) # Safety @@ -1807,7 +1807,7 @@ def message_contents_get_contents_bin(contents: MessageContents) -> str: Get the contents of a message as a pointer to an array of bytes. [Rust - `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_get_contents_bin) + `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_bin) # Safety @@ -1834,7 +1834,7 @@ def message_contents_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_set_contents_bin) + `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_set_contents_bin) * `message` - the message contents to set the contents for * `contents_bin` - pointer to contents to copy from @@ -1863,7 +1863,7 @@ def message_contents_get_metadata_iter( Get an iterator over the metadata of a message. [Rust - `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) + `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) The returned pointer must be deleted with `pactffi_message_metadata_iter_delete` when done with it. @@ -1894,7 +1894,7 @@ def message_contents_get_matching_rule_iter( Get an iterator over the matching rules for a category of a message. [Rust - `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) + `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -1934,7 +1934,7 @@ def request_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP request. - [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) + [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -1971,7 +1971,7 @@ def response_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP response. - [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) + [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -2009,7 +2009,7 @@ def message_contents_get_generators_iter( Get an iterator over the generators for a category of a message. [Rust - `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_contents_get_generators_iter) + `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -2034,7 +2034,7 @@ def request_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP request. [Rust - `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_request_contents_get_generators_iter) + `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_request_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -2059,7 +2059,7 @@ def response_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP response. [Rust - `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_response_contents_get_generators_iter) + `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_response_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -2084,7 +2084,7 @@ def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: any generator. [Rust - `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_parse_matcher_definition) + `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_parse_matcher_definition) The following are examples of matching rule definitions: @@ -2120,7 +2120,7 @@ def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: using the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matcher_definition_error) + `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_error) """ raise NotImplementedError @@ -2134,7 +2134,7 @@ def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matcher_definition_value) + `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_value) Note that different expressions values can have types other than a string. Use `pactffi_matcher_definition_value_type` to get the actual type of the @@ -2148,7 +2148,7 @@ def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: """ Frees the memory used by the result of parsing the matching definition expression. - [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matcher_definition_delete) + [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_delete) """ raise NotImplementedError @@ -2161,7 +2161,7 @@ def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Ge NULL pointer, otherwise returns the generator as a pointer. [Rust - `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matcher_definition_generator) + `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_generator) The generator pointer will be a valid pointer as long as `pactffi_matcher_definition_delete` has not been called on the definition. @@ -2180,7 +2180,7 @@ def matcher_definition_value_type( If there was an error parsing the expression, it will return Unknown. [Rust - `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matcher_definition_value_type) + `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_value_type) """ raise NotImplementedError @@ -2189,7 +2189,7 @@ def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: """ Free the iterator when you're done using it. - [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_iter_delete) + [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_iter_delete) """ raise NotImplementedError @@ -2204,7 +2204,7 @@ def matcher_definition_iter( `pactffi_matching_rule_iter_delete` function once done with it. [Rust - `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matcher_definition_iter) + `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_iter) If there was an error parsing the expression, this function will return a NULL pointer. @@ -2220,7 +2220,7 @@ def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: deleted but will be cleaned up when the iterator is deleted. [Rust - `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_iter_next) + `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_iter_next) Will return a NULL pointer when the iterator has advanced past the end of the list. @@ -2242,7 +2242,7 @@ def matching_rule_id(rule_result: MatchingRuleResult) -> int: Return the ID of the matching rule. [Rust - `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_id) + `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_id) The ID corresponds to the following rules: @@ -2288,7 +2288,7 @@ def matching_rule_value(rule_result: MatchingRuleResult) -> str: pointer. [Rust - `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_value) + `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_value) The associated values for the rules are: @@ -2336,7 +2336,7 @@ def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: Will return a NULL pointer if the matching rule result was a reference. [Rust - `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_pointer) + `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_pointer) # Safety @@ -2354,7 +2354,7 @@ def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: structure. I.e., [Rust - `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_reference_name) + `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_reference_name) ```json { @@ -2388,7 +2388,7 @@ def validate_datetime(value: str, format: str) -> None: `pactffi_get_error_message`. [Rust - `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_validate_datetime) + `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_validate_datetime) # Errors If the function receives a panic, it will return 2 and the message associated with the panic can be retrieved with `pactffi_get_error_message`. @@ -2416,7 +2416,7 @@ def generator_to_json(generator: Generator) -> str: Get the JSON form of the generator. [Rust - `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generator_to_json) + `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generator_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -2439,7 +2439,7 @@ def generator_generate_string(generator: Generator, context_json: str) -> str: function). [Rust - `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generator_generate_string) + `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generator_generate_string) If anything goes wrong, it will return a NULL pointer. """ @@ -2455,7 +2455,7 @@ def generator_generate_integer(generator: Generator, context_json: str) -> int: should be the values returned from the Provider State callback function). [Rust - `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generator_generate_integer) + `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generator_generate_integer) If anything goes wrong or the generator is not a type that can generate an integer value, it will return a zero value. @@ -2468,7 +2468,7 @@ def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generators_iter_delete) + `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generators_iter_delete) """ raise NotImplementedError @@ -2478,7 +2478,7 @@ def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePa Get the next path and generator out of the iterator, if possible. [Rust - `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generators_iter_next) + `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generators_iter_next) The returned pointer must be deleted with `pactffi_generator_iter_pair_delete`. @@ -2500,7 +2500,7 @@ def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: Free a pair of key and value returned from `pactffi_generators_iter_next`. [Rust - `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generators_iter_pair_delete) + `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generators_iter_pair_delete) """ raise NotImplementedError @@ -2509,7 +2509,7 @@ def sync_http_new() -> SynchronousHttp: """ Get a mutable pointer to a newly-created default interaction on the heap. - [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_new) + [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_new) # Safety @@ -2527,7 +2527,7 @@ def sync_http_delete(interaction: SynchronousHttp) -> None: Destroy the `SynchronousHttp` interaction being pointed to. [Rust - `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_delete) + `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_delete) """ raise NotImplementedError @@ -2537,7 +2537,7 @@ def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: Get the request of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_request) + `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request) # Safety @@ -2557,7 +2557,7 @@ def sync_http_get_request_contents(interaction: SynchronousHttp) -> str: Get the request contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_request_contents) + `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request_contents) # Safety @@ -2584,7 +2584,7 @@ def sync_http_set_request_contents( Sets the request contents of the interaction. [Rust - `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_set_request_contents) + `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_request_contents) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -2612,7 +2612,7 @@ def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: Get the length of the request contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) + `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) # Safety @@ -2631,7 +2631,7 @@ def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes: Get the request contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) + `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) # Safety @@ -2658,7 +2658,7 @@ def sync_http_set_request_contents_bin( Sets the request contents of the interaction as an array of bytes. [Rust - `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) + `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from @@ -2685,7 +2685,7 @@ def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: Get the response of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_response) + `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response) # Safety @@ -2705,7 +2705,7 @@ def sync_http_get_response_contents(interaction: SynchronousHttp) -> str: Get the response contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_response_contents) + `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response_contents) # Safety @@ -2733,7 +2733,7 @@ def sync_http_set_response_contents( Sets the response contents of the interaction. [Rust - `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_set_response_contents) + `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_response_contents) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -2761,7 +2761,7 @@ def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: Get the length of the response contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) + `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) # Safety @@ -2780,7 +2780,7 @@ def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes: Get the response contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) + `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) # Safety @@ -2807,7 +2807,7 @@ def sync_http_set_response_contents_bin( Sets the response contents of the `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) + `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from @@ -2834,7 +2834,7 @@ def sync_http_get_description(interaction: SynchronousHttp) -> str: Get a copy of the description. [Rust - `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_description) + `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_description) # Safety @@ -2858,7 +2858,7 @@ def sync_http_set_description(interaction: SynchronousHttp, description: str) -> Write the `description` field on the `SynchronousHttp`. [Rust - `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_set_description) + `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_description) # Safety @@ -2883,7 +2883,7 @@ def sync_http_get_provider_state( Get a copy of the provider state at the given index from this interaction. [Rust - `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_provider_state) + `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_provider_state) # Safety @@ -2909,7 +2909,7 @@ def sync_http_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) + `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) # Safety @@ -2934,7 +2934,7 @@ def pact_interaction_as_synchronous_http( longer required. [Rust - `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) + `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) # Safety This function is safe as long as the interaction pointer is a valid pointer. @@ -2953,7 +2953,7 @@ def pact_interaction_as_message(interaction: PactInteraction) -> Message: must be freed with `pactffi_message_delete` when no longer required. [Rust - `pactffi_pact_interaction_as_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_as_message) + `pactffi_pact_interaction_as_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_as_message) Note that if the interaction is a V4 `AsynchronousMessage`, it will be converted to a V3 `Message` before being returned. @@ -2978,7 +2978,7 @@ def pact_interaction_as_asynchronous_message( no longer required. [Rust - `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) + `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) Note that if the interaction is a V3 `Message`, it will be converted to a V4 `AsynchronousMessage` before being returned. @@ -3003,7 +3003,7 @@ def pact_interaction_as_synchronous_message( no longer required. [Rust - `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) + `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) # Safety This function is safe as long as the interaction pointer is a valid pointer. @@ -3018,7 +3018,7 @@ def pact_message_iter_delete(iter: PactMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_message_iter_delete) + `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_message_iter_delete) """ lib.pactffi_pact_message_iter_delete(iter._ptr) @@ -3028,7 +3028,7 @@ def pact_message_iter_next(iter: PactMessageIterator) -> Message: Get the next message from the message pact. [Rust - `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_message_iter_next) + `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_message_iter_next) """ ptr = lib.pactffi_pact_message_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -3042,7 +3042,7 @@ def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMes Get the next synchronous request/response message from the V4 pact. [Rust - `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_sync_message_iter_next) + `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_message_iter_next) """ ptr = lib.pactffi_pact_sync_message_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -3056,7 +3056,7 @@ def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) + `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) """ lib.pactffi_pact_sync_message_iter_delete(iter._ptr) @@ -3066,7 +3066,7 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: Get the next synchronous HTTP request/response interaction from the V4 pact. [Rust - `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_sync_http_iter_next) + `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_http_iter_next) """ ptr = lib.pactffi_pact_sync_http_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -3080,7 +3080,7 @@ def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) + `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) """ lib.pactffi_pact_sync_http_iter_delete(iter._ptr) @@ -3090,7 +3090,7 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction Get the next interaction from the pact. [Rust - `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_iter_next) + `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_iter_next) """ ptr = lib.pactffi_pact_interaction_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -3104,7 +3104,7 @@ def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_interaction_iter_delete) + `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_iter_delete) """ lib.pactffi_pact_interaction_iter_delete(iter._ptr) @@ -3114,7 +3114,7 @@ def matching_rule_to_json(rule: MatchingRule) -> str: Get the JSON form of the matching rule. [Rust - `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rule_to_json) + `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -3131,7 +3131,7 @@ def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rules_iter_delete) + `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rules_iter_delete) """ raise NotImplementedError @@ -3143,7 +3143,7 @@ def matching_rules_iter_next( Get the next path and matching rule out of the iterator, if possible. [Rust - `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rules_iter_next) + `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rules_iter_next) The returned pointer must be deleted with `pactffi_matching_rules_iter_pair_delete`. @@ -3165,7 +3165,7 @@ def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) + `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) """ raise NotImplementedError @@ -3175,7 +3175,7 @@ def message_new() -> Message: Get a mutable pointer to a newly-created default message on the heap. [Rust - `pactffi_message_new`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_new) + `pactffi_message_new`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_new) # Safety @@ -3197,7 +3197,7 @@ def message_new_from_json( Constructs a `Message` from the JSON string. [Rust - `pactffi_message_new_from_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_new_from_json) + `pactffi_message_new_from_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_new_from_json) # Safety @@ -3215,7 +3215,7 @@ def message_new_from_body(body: str, content_type: str) -> Message: Constructs a `Message` from a body with a given content-type. [Rust - `pactffi_message_new_from_body`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_new_from_body) + `pactffi_message_new_from_body`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_new_from_body) # Safety @@ -3233,7 +3233,7 @@ def message_delete(message: Message) -> None: Destroy the `Message` being pointed to. [Rust - `pactffi_message_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_delete) + `pactffi_message_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_delete) """ raise NotImplementedError @@ -3243,7 +3243,7 @@ def message_get_contents(message: Message) -> OwnedString | None: Get the contents of a `Message` in string form. [Rust - `pactffi_message_get_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_contents) + `pactffi_message_get_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_contents) # Safety @@ -3269,7 +3269,7 @@ def message_set_contents(message: Message, contents: str, content_type: str) -> Sets the contents of the message. [Rust - `pactffi_message_set_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_set_contents) + `pactffi_message_set_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_set_contents) # Safety @@ -3291,7 +3291,7 @@ def message_get_contents_length(message: Message) -> int: Get the length of the contents of a `Message`. [Rust - `pactffi_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_contents_length) + `pactffi_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_contents_length) # Safety @@ -3310,7 +3310,7 @@ def message_get_contents_bin(message: Message) -> str: Get the contents of a `Message` as a pointer to an array of bytes. [Rust - `pactffi_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_contents_bin) + `pactffi_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_contents_bin) # Safety @@ -3337,7 +3337,7 @@ def message_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_set_contents_bin) + `pactffi_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_set_contents_bin) # Safety @@ -3358,7 +3358,7 @@ def message_get_description(message: Message) -> OwnedString: Get a copy of the description. [Rust - `pactffi_message_get_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_description) + `pactffi_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_description) # Safety @@ -3381,7 +3381,7 @@ def message_set_description(message: Message, description: str) -> int: Write the `description` field on the `Message`. [Rust - `pactffi_message_set_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_set_description) + `pactffi_message_set_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_set_description) # Safety @@ -3403,7 +3403,7 @@ def message_get_provider_state(message: Message, index: int) -> ProviderState: Get a copy of the provider state at the given index from this message. [Rust - `pactffi_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_provider_state) + `pactffi_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_provider_state) # Safety @@ -3426,7 +3426,7 @@ def message_get_provider_state_iter(message: Message) -> ProviderStateIterator: Get an iterator over provider states. [Rust - `pactffi_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_provider_state_iter) + `pactffi_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_provider_state_iter) # Safety @@ -3444,7 +3444,7 @@ def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: Get the next value from the iterator. [Rust - `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_iter_next) + `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_iter_next) # Safety @@ -3465,7 +3465,7 @@ def provider_state_iter_delete(iter: ProviderStateIterator) -> None: Delete the iterator. [Rust - `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_iter_delete) + `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_iter_delete) """ raise NotImplementedError @@ -3475,7 +3475,7 @@ def message_find_metadata(message: Message, key: str) -> str: Get a copy of the metadata value indexed by `key`. [Rust - `pactffi_message_find_metadata`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_find_metadata) + `pactffi_message_find_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_find_metadata) # Safety @@ -3501,7 +3501,7 @@ def message_insert_metadata(message: Message, key: str, value: str) -> int: Insert the (`key`, `value`) pair into this Message's `metadata` HashMap. [Rust - `pactffi_message_insert_metadata`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_insert_metadata) + `pactffi_message_insert_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_insert_metadata) # Safety @@ -3521,7 +3521,7 @@ def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadata Get the next key and value out of the iterator, if possible. [Rust - `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_metadata_iter_next) + `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_metadata_iter_next) The returned pointer must be deleted with `pactffi_message_metadata_pair_delete`. @@ -3544,7 +3544,7 @@ def message_get_metadata_iter(message: Message) -> MessageMetadataIterator: Get an iterator over the metadata of a message. [Rust - `pactffi_message_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_get_metadata_iter) + `pactffi_message_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_metadata_iter) # Safety @@ -3569,7 +3569,7 @@ def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: Free the metadata iterator when you're done using it. [Rust - `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_metadata_iter_delete) + `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_metadata_iter_delete) """ raise NotImplementedError @@ -3579,7 +3579,7 @@ def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_metadata_pair_delete) + `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_metadata_pair_delete) """ raise NotImplementedError @@ -3591,7 +3591,7 @@ def message_pact_new_from_json(file_name: str, json_str: str) -> MessagePact: The provided file name is used when generating error messages. [Rust - `pactffi_message_pact_new_from_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_new_from_json) + `pactffi_message_pact_new_from_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_new_from_json) # Safety @@ -3610,7 +3610,7 @@ def message_pact_delete(message_pact: MessagePact) -> None: Delete the `MessagePact` being pointed to. [Rust - `pactffi_message_pact_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_delete) + `pactffi_message_pact_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_delete) """ raise NotImplementedError @@ -3623,7 +3623,7 @@ def message_pact_get_consumer(message_pact: MessagePact) -> Consumer: pointer. [Rust - `pactffi_message_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_get_consumer) + `pactffi_message_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_get_consumer) # Safety @@ -3645,7 +3645,7 @@ def message_pact_get_provider(message_pact: MessagePact) -> Provider: pointer. [Rust - `pactffi_message_pact_get_provider`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_get_provider) + `pactffi_message_pact_get_provider`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_get_provider) # Safety @@ -3666,7 +3666,7 @@ def message_pact_get_message_iter( Get an iterator over the messages of a message pact. [Rust - `pactffi_message_pact_get_message_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_get_message_iter) + `pactffi_message_pact_get_message_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_get_message_iter) # Safety @@ -3691,7 +3691,7 @@ def message_pact_message_iter_next(iter: MessagePactMessageIterator) -> Message: Get the next message from the message pact. [Rust - `pactffi_message_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_message_iter_next) + `pactffi_message_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_message_iter_next) # Safety @@ -3710,7 +3710,7 @@ def message_pact_message_iter_delete(iter: MessagePactMessageIterator) -> None: Delete the iterator. [Rust - `pactffi_message_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_message_iter_delete) + `pactffi_message_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_message_iter_delete) """ raise NotImplementedError @@ -3720,7 +3720,7 @@ def message_pact_find_metadata(message_pact: MessagePact, key1: str, key2: str) Get a copy of the metadata value indexed by `key1` and `key2`. [Rust - `pactffi_message_pact_find_metadata`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_find_metadata) + `pactffi_message_pact_find_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_find_metadata) # Safety @@ -3748,7 +3748,7 @@ def message_pact_get_metadata_iter( Get an iterator over the metadata of a message pact. [Rust - `pactffi_message_pact_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_get_metadata_iter) + `pactffi_message_pact_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_get_metadata_iter) # Safety @@ -3775,7 +3775,7 @@ def message_pact_metadata_iter_next( Get the next triple out of the iterator, if possible. [Rust - `pactffi_message_pact_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_metadata_iter_next) + `pactffi_message_pact_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_metadata_iter_next) # Safety @@ -3793,7 +3793,7 @@ def message_pact_metadata_iter_delete(iter: MessagePactMetadataIterator) -> None """ Free the metadata iterator when you're done using it. - [Rust `pactffi_message_pact_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_metadata_iter_delete) + [Rust `pactffi_message_pact_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_metadata_iter_delete) """ raise NotImplementedError @@ -3802,7 +3802,7 @@ def message_pact_metadata_triple_delete(triple: MessagePactMetadataTriple) -> No """ Free a triple returned from `pactffi_message_pact_metadata_iter_next`. - [Rust `pactffi_message_pact_metadata_triple_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_pact_metadata_triple_delete) + [Rust `pactffi_message_pact_metadata_triple_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_metadata_triple_delete) """ raise NotImplementedError @@ -3812,7 +3812,7 @@ def provider_get_name(provider: Provider) -> str: Get a copy of this provider's name. [Rust - `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_get_name) + `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -3858,7 +3858,7 @@ def pact_get_provider(pact: Pact) -> Provider: `pactffi_pact_provider_delete` when no longer required. [Rust - `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_get_provider) + `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_get_provider) # Errors @@ -3873,7 +3873,7 @@ def pact_provider_delete(provider: Provider) -> None: Frees the memory used by the Pact provider. [Rust - `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_provider_delete) + `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_provider_delete) """ raise NotImplementedError @@ -3885,7 +3885,7 @@ def provider_state_get_name(provider_state: ProviderState) -> str: This needs to be deleted with `pactffi_string_delete`. [Rust - `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_get_name) + `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_get_name) # Safety @@ -3905,7 +3905,7 @@ def provider_state_get_param_iter( Get an iterator over the params of a provider state. [Rust - `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_get_param_iter) + `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_get_param_iter) # Safety @@ -3932,7 +3932,7 @@ def provider_state_param_iter_next( Get the next key and value out of the iterator, if possible. [Rust - `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_param_iter_next) + `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_param_iter_next) Returns a pointer to a heap allocated array of 2 elements, the pointer to the key string on the heap, and the pointer to the value string on the heap. @@ -3955,7 +3955,7 @@ def provider_state_delete(provider_state: ProviderState) -> None: Free the provider state when you're done using it. [Rust - `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_delete) + `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_delete) """ raise NotImplementedError @@ -3965,7 +3965,7 @@ def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: Free the provider state param iterator when you're done using it. [Rust - `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_param_iter_delete) + `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_param_iter_delete) """ raise NotImplementedError @@ -3975,7 +3975,7 @@ def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: Free a pair of key and value. [Rust - `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_provider_state_param_pair_delete) + `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_param_pair_delete) """ raise NotImplementedError @@ -3985,7 +3985,7 @@ def sync_message_new() -> SynchronousMessage: Get a mutable pointer to a newly-created default message on the heap. [Rust - `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_new) + `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_new) # Safety @@ -4003,7 +4003,7 @@ def sync_message_delete(message: SynchronousMessage) -> None: Destroy the `Message` being pointed to. [Rust - `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_delete) + `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_delete) """ raise NotImplementedError @@ -4013,7 +4013,7 @@ def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: Get the request contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) + `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) # Safety @@ -4040,7 +4040,7 @@ def sync_message_set_request_contents_str( Sets the request contents of the message. [Rust - `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) + `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) - `message` - the message to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -4068,7 +4068,7 @@ def sync_message_get_request_contents_length(message: SynchronousMessage) -> int Get the length of the request contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) + `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) # Safety @@ -4087,7 +4087,7 @@ def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes: Get the request contents of a `SynchronousMessage` as a bytes. [Rust - `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) + `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) # Safety @@ -4114,7 +4114,7 @@ def sync_message_set_request_contents_bin( Sets the request contents of the message as an array of bytes. [Rust - `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) + `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) * `message` - the message to set the request contents for * `contents` - pointer to contents to copy from @@ -4141,7 +4141,7 @@ def sync_message_get_request_contents(message: SynchronousMessage) -> MessageCon Get the request contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_request_contents) + `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_request_contents) # Safety @@ -4161,7 +4161,7 @@ def sync_message_get_number_responses(message: SynchronousMessage) -> int: Get the number of response messages in the `SynchronousMessage`. [Rust - `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_number_responses) + `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_number_responses) # Safety @@ -4182,7 +4182,7 @@ def sync_message_get_response_contents_str( Get the response contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) + `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) # Safety @@ -4215,7 +4215,7 @@ def sync_message_set_response_contents_str( with default values. [Rust - `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) + `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response. @@ -4247,7 +4247,7 @@ def sync_message_get_response_contents_length( Get the length of the response contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) + `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) # Safety @@ -4269,7 +4269,7 @@ def sync_message_get_response_contents_bin( Get the response contents of a `SynchronousMessage` as bytes. [Rust - `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) + `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) # Safety @@ -4300,7 +4300,7 @@ def sync_message_set_response_contents_bin( responses will be padded with default values. [Rust - `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) + `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response @@ -4331,7 +4331,7 @@ def sync_message_get_response_contents( Get the response contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_response_contents) + `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_response_contents) # Safety @@ -4351,7 +4351,7 @@ def sync_message_get_description(message: SynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_description) + `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_description) # Safety @@ -4375,7 +4375,7 @@ def sync_message_set_description(message: SynchronousMessage, description: str) Write the `description` field on the `SynchronousMessage`. [Rust - `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_set_description) + `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_description) # Safety @@ -4400,7 +4400,7 @@ def sync_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_provider_state) + `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_provider_state) # Safety @@ -4426,7 +4426,7 @@ def sync_message_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) + `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) # Safety @@ -4444,7 +4444,7 @@ def string_delete(string: OwnedString) -> None: Delete a string previously returned by this FFI. [Rust - `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_string_delete) + `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_string_delete) """ lib.pactffi_string_delete(string._ptr) @@ -4459,7 +4459,7 @@ def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: the mock server is returned. [Rust - `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_create_mock_server) + `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_create_mock_server) * `pact_str` - Pact JSON * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:80) @@ -4495,7 +4495,7 @@ def get_tls_ca_certificate() -> OwnedString: Fetch the CA Certificate used to generate the self-signed certificate. [Rust - `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_get_tls_ca_certificate) + `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_get_tls_ca_certificate) **NOTE:** The string for the result is allocated on the heap, and will have to be freed by the caller using pactffi_string_delete. @@ -4516,7 +4516,7 @@ def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) - operating system. The port of the mock server is returned. [Rust - `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_create_mock_server_for_pact) + `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_create_mock_server_for_pact) * `pact` - Handle to a Pact model created with created with `pactffi_new_pact`. @@ -4559,7 +4559,7 @@ def create_mock_server_for_transport( Create a mock server for the provided Pact handle and transport. [Rust - `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_create_mock_server_for_transport) + `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_create_mock_server_for_transport) Args: pact: @@ -4620,7 +4620,7 @@ def mock_server_matched(mock_server_handle: PactServerHandle) -> bool: if any request has not been successfully matched, or the method panics. [Rust - `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mock_server_matched) + `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mock_server_matched) """ return lib.pactffi_mock_server_matched(mock_server_handle._ref) @@ -4632,7 +4632,7 @@ def mock_server_mismatches( External interface to get all the mismatches from a mock server. [Rust - `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mock_server_mismatches) + `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mock_server_mismatches) # Errors @@ -4658,7 +4658,7 @@ def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: and cleanup any memory allocated for it. [Rust - `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_cleanup_mock_server) + `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_cleanup_mock_server) Args: mock_server_handle: @@ -4686,7 +4686,7 @@ def write_pact_file( directory to write the file to is passed as the second parameter. [Rust - `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_write_pact_file) + `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_write_pact_file) Args: mock_server_handle: @@ -4737,7 +4737,7 @@ def mock_server_logs(mock_server_handle: PactServerHandle) -> str: started. [Rust - `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_mock_server_logs) + `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mock_server_logs) Raises: RuntimeError: If the logs for the mock server can not be retrieved. @@ -4760,7 +4760,7 @@ def generate_datetime_string(format: str) -> StringResult: string needs to be freed with the `pactffi_string_delete` function [Rust - `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generate_datetime_string) + `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generate_datetime_string) # Safety @@ -4777,7 +4777,7 @@ def check_regex(regex: str, example: str) -> bool: Checks that the example string matches the given regex. [Rust - `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_check_regex) + `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_check_regex) # Safety @@ -4796,7 +4796,7 @@ def generate_regex_value(regex: str) -> StringResult: `pactffi_string_delete` function. [Rust - `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_generate_regex_value) + `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generate_regex_value) # Safety @@ -4811,7 +4811,7 @@ def free_string(s: str) -> None: [DEPRECATED] Frees the memory allocated to a string by another function. [Rust - `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_free_string) + `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_free_string) This function is deprecated. Use `pactffi_string_delete` instead. @@ -4833,7 +4833,7 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle: Creates a new Pact model and returns a handle to it. [Rust - `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_new_pact) + `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_pact) Args: consumer_name: @@ -4874,7 +4874,7 @@ def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: will result in that interaction being replaced with the new one. [Rust - `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_new_interaction) + `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_interaction) Args: pact: @@ -4902,7 +4902,7 @@ def new_message_interaction(pact: PactHandle, description: str) -> InteractionHa will result in that interaction being replaced with the new one. [Rust - `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_new_message_interaction) + `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_message_interaction) Args: pact: @@ -4933,7 +4933,7 @@ def new_sync_message_interaction( will result in that interaction being replaced with the new one. [Rust - `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_new_sync_message_interaction) + `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_sync_message_interaction) Args: pact: @@ -4958,7 +4958,7 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> None: Sets the description for the Interaction. [Rust - `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_upon_receiving) + `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_upon_receiving) This function @@ -4995,7 +4995,7 @@ def given(interaction: InteractionHandle, description: str) -> None: Adds a provider state to the Interaction. [Rust - `pactffi_given`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_given) + `pactffi_given`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_given) Args: interaction: @@ -5021,7 +5021,7 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> Non used with V4 interactions. [Rust - `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_interaction_test_name) + `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_interaction_test_name) Args: interaction: @@ -5081,7 +5081,7 @@ def given_with_param( be parsed as JSON. [Rust - `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_given_with_param) + `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_given_with_param) Args: interaction: @@ -5122,7 +5122,7 @@ def given_with_params( with a `value` key. [Rust - `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_given_with_params) + `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_given_with_params) Args: interaction: @@ -5172,7 +5172,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None Configures the request for the Interaction. [Rust - `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_request) + `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_request) Args: interaction: @@ -5186,7 +5186,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md) + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) which allows regex patterns. For examples: ```json @@ -5220,7 +5220,7 @@ def with_query_parameter_v2( Configures a query parameter for the Interaction. [Rust - `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_query_parameter_v2) + `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_query_parameter_v2) To setup a query parameter with multiple values, you can either call this function multiple times with a different index value: @@ -5257,7 +5257,7 @@ def with_query_parameter_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) If you want the matching rules to apply to all values (and not just the one with the given index), make sure to set the value to be an array. @@ -5291,7 +5291,7 @@ def with_query_parameter_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: If there was an error setting the query parameter. @@ -5312,7 +5312,7 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None: Sets the specification version for a given Pact model. [Rust - `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_specification) + `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_specification) Args: pact: @@ -5332,7 +5332,7 @@ def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: Fetches the Pact specification version for the given Pact model. [Rust - `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_handle_get_pact_spec_version) + `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_handle_get_pact_spec_version) Args: handle: @@ -5358,7 +5358,7 @@ def with_pact_metadata( mock server for it has already started) [Rust - `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_pact_metadata) + `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_pact_metadata) Args: pact: @@ -5394,7 +5394,7 @@ def with_header_v2( r""" Configures a header for the Interaction. - [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_header_v2) + [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_header_v2) To setup a header with multiple values, you can either call this function multiple times with a different index value: @@ -5432,7 +5432,7 @@ def with_header_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -5454,7 +5454,7 @@ def with_header_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: If there was an error setting the header. @@ -5485,7 +5485,7 @@ def set_header( and generators can not be configured with it. [Rust - `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_set_header) + `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_set_header) If matching rules are required to be set, use `pactffi_with_header_v2`. @@ -5522,7 +5522,7 @@ def response_status(interaction: InteractionHandle, status: int) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_response_status) + `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_response_status) Args: interaction: @@ -5545,7 +5545,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_response_status_v2) + `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_response_status_v2) To include matching rules for the status (only statusCode or integer really makes sense to use), include the matching rule JSON format with the value as @@ -5564,7 +5564,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -5575,7 +5575,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: If the response status could not be set. @@ -5598,7 +5598,7 @@ def with_body( Adds the body for the interaction. [Rust - `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_body) + `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -5619,7 +5619,7 @@ def with_body( body: The body contents. For JSON payloads, matching rules can be embedded - in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). + in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: If the body could not be specified. @@ -5645,7 +5645,7 @@ def with_binary_body( Adds the body for the interaction. [Rust - `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_binary_body) + `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_binary_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -5688,7 +5688,7 @@ def with_binary_file( already started) [Rust - `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_binary_file) + `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_binary_file) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -5738,7 +5738,7 @@ def with_matching_rules( Add matching rules to the interaction. [Rust - `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_matching_rules) + `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_matching_rules) This function can be called multiple times, in which case the matching rules will be merged. @@ -5782,7 +5782,7 @@ def with_multipart_file_v2( # noqa: PLR0913 already started) or an error occurs. [Rust - `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_multipart_file_v2) + `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_multipart_file_v2) This function can be called multiple times. In that case, each subsequent call will be appended to the existing multipart body as a new part. @@ -5836,7 +5836,7 @@ def with_multipart_file( already started) or an error occurs. [Rust - `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_multipart_file) + `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_multipart_file) * `interaction` - Interaction handle to set the body for. * `part` - Request or response part. @@ -5873,7 +5873,7 @@ def set_key(interaction: InteractionHandle, key: str | None) -> None: Sets the key attribute for the interaction. [Rust - `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_set_key) + `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_set_key) Args: interaction: @@ -5897,7 +5897,7 @@ def set_pending(interaction: InteractionHandle, *, pending: bool) -> None: Mark the interaction as pending. [Rust - `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_set_pending) + `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_set_pending) Args: interaction: @@ -5917,7 +5917,7 @@ def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> Add a comment to the interaction. [Rust - `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_set_comment) + `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_set_comment) Args: interaction: @@ -5945,12 +5945,35 @@ def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> raise RuntimeError(msg) +def add_text_comment(interaction: InteractionHandle, comment: str) -> None: + """ + Add a text comment to the interaction. + + [Rust + `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_add_text_comment) + + Args: + interaction: + Interaction handle to set the comments for. + + comment: + Comment value. This is a regular string value. + """ + success: bool = lib.pactffi_add_text_comment( + interaction._ref, + comment.encode("utf-8"), + ) + if not success: + msg = f"Failed to add text comment for {interaction}." + raise RuntimeError(msg) + + def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: r""" Get an iterator over all the messages of the Pact. [Rust - `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_handle_get_message_iter) + `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_message_iter) # Safety @@ -5974,7 +5997,7 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6000,7 +6023,7 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: `pactffi_pact_sync_http_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) + `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) # Safety @@ -6021,7 +6044,7 @@ def new_message_pact(consumer_name: str, provider_name: str) -> MessagePactHandl Creates a new Pact Message model and returns a handle to it. [Rust - `pactffi_new_message_pact`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_new_message_pact) + `pactffi_new_message_pact`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_message_pact) * `consumer_name` - The name of the consumer for the pact. * `provider_name` - The name of the provider for the pact. @@ -6037,7 +6060,7 @@ def new_message(pact: MessagePactHandle, description: str) -> MessageHandle: Creates a new Message and returns a handle to it. [Rust - `pactffi_new_message`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_new_message) + `pactffi_new_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_message) * `description` - The message description. It needs to be unique for each Message. @@ -6052,7 +6075,7 @@ def message_expects_to_receive(message: MessageHandle, description: str) -> None Sets the description for the Message. [Rust - `pactffi_message_expects_to_receive`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_expects_to_receive) + `pactffi_message_expects_to_receive`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_expects_to_receive) * `description` - The message description. It needs to be unique for each message. @@ -6065,7 +6088,7 @@ def message_given(message: MessageHandle, description: str) -> None: Adds a provider state to the Interaction. [Rust - `pactffi_message_given`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_given) + `pactffi_message_given`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_given) * `description` - The provider state description. It needs to be unique for each message @@ -6083,7 +6106,7 @@ def message_given_with_param( Adds a provider state to the Message with a parameter key and value. [Rust - `pactffi_message_given_with_param`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_given_with_param) + `pactffi_message_given_with_param`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_given_with_param) * `description` - The provider state description. It needs to be unique. * `name` - Parameter name. @@ -6102,7 +6125,7 @@ def message_with_contents( Adds the contents of the Message. [Rust - `pactffi_message_with_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_with_contents) + `pactffi_message_with_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_with_contents) Accepts JSON, binary and other payload types. Binary data will be base64 encoded when serialised. @@ -6129,7 +6152,7 @@ def message_with_metadata(message_handle: MessageHandle, key: str, value: str) - Adds expected metadata to the Message. [Rust - `pactffi_message_with_metadata`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_with_metadata) + `pactffi_message_with_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_with_metadata) * `key` - metadata key * `value` - metadata value. @@ -6146,7 +6169,7 @@ def message_with_metadata_v2( Adds expected metadata to the Message. [Rust - `pactffi_message_with_metadata_v2`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_with_metadata_v2) + `pactffi_message_with_metadata_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_with_metadata_v2) Args: message_handle: @@ -6160,7 +6183,7 @@ def message_with_metadata_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). To include matching rules for the metadata, include the matching rule JSON format with the value as a single JSON document. I.e. @@ -6176,7 +6199,7 @@ def message_with_metadata_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). """ raise NotImplementedError @@ -6186,7 +6209,7 @@ def message_reify(message_handle: MessageHandle) -> OwnedString: Reifies the given message. [Rust - `pactffi_message_reify`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_message_reify) + `pactffi_message_reify`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_reify) Reification is the process of stripping away any matchers, and returning the original contents. @@ -6215,7 +6238,7 @@ def write_message_pact_file( pointer is passed, the current working directory is used. [Rust - `pactffi_write_message_pact_file`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_write_message_pact_file) + `pactffi_write_message_pact_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_write_message_pact_file) If overwrite is true, the file will be overwritten with the contents of the current pact. Otherwise, it will be merged with any existing pact file. @@ -6249,7 +6272,7 @@ def with_message_pact_metadata( version [Rust - `pactffi_with_message_pact_metadata`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_with_message_pact_metadata) + `pactffi_with_message_pact_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_message_pact_metadata) * `pact` - Handle to a Pact model * `namespace` - the top level metadat key to set any key values on @@ -6269,7 +6292,7 @@ def pact_handle_write_file( External interface to write out the pact file. [Rust - `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_pact_handle_write_file) + `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_write_file) This function should be called if all the consumer tests have passed. @@ -6309,7 +6332,7 @@ def free_pact_handle(pact: PactHandle) -> None: Delete a Pact handle and free the resources used by it. [Rust - `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_free_pact_handle) + `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_free_pact_handle) Raises: RuntimeError: If the handle could not be freed. @@ -6329,7 +6352,7 @@ def free_message_pact_handle(pact: MessagePactHandle) -> int: Delete a Pact handle and free the resources used by it. [Rust - `pactffi_free_message_pact_handle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_free_message_pact_handle) + `pactffi_free_message_pact_handle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_free_message_pact_handle) # Error Handling @@ -6346,7 +6369,7 @@ def verify(args: str) -> int: """ External interface to verifier a provider. - [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verify) + [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verify) * `args` - the same as the CLI interface, except newline delimited @@ -6373,7 +6396,7 @@ def verifier_new_for_application() -> VerifierHandle: Get a Handle to a newly created verifier. [Rust - `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_new_for_application) + `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_new_for_application) """ from pact import __version__ @@ -6388,7 +6411,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None: """ Shutdown the verifier and release all resources. - [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_shutdown) + [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_shutdown) """ lib.pactffi_verifier_shutdown(handle._ref) @@ -6405,7 +6428,7 @@ def verifier_set_provider_info( # noqa: PLR0913 Set the provider details for the Pact verifier. [Rust - `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_provider_info) + `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_provider_info) Args: handle: @@ -6451,7 +6474,7 @@ def verifier_add_provider_transport( Adds a new transport for the given provider. [Rust - `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_provider_transport) + `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_add_provider_transport) Args: handle: @@ -6492,7 +6515,7 @@ def verifier_set_filter_info( Set the filters for the Pact verifier. [Rust - `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_filter_info) + `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_filter_info) Set filters to narrow down the interactions to verify. @@ -6528,7 +6551,7 @@ def verifier_set_provider_state( Set the provider state URL for the Pact verifier. [Rust - `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_provider_state) + `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_provider_state) Args: handle: @@ -6563,7 +6586,7 @@ def verifier_set_verification_options( Set the options used by the verifier when calling the provider. [Rust - `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_verification_options) + `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_verification_options) Args: handle: @@ -6594,7 +6617,7 @@ def verifier_set_coloured_output( Enables or disables coloured output using ANSI escape codes. [Rust - `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_coloured_output) + `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_coloured_output) By default, coloured output is enabled. @@ -6619,7 +6642,7 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> Enables or disables if no pacts are found to verify results in an error. [Rust - `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) + `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) Args: handle: @@ -6648,7 +6671,7 @@ def verifier_set_publish_options( Set the options used when publishing verification results to the Broker. [Rust - `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_publish_options) + `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_publish_options) Args: handle: @@ -6687,7 +6710,7 @@ def verifier_set_consumer_filters( Set the consumer filters for the Pact verifier. [Rust - `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_consumer_filters) + `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_consumer_filters) """ lib.pactffi_verifier_set_consumer_filters( handle._ref, @@ -6705,7 +6728,7 @@ def verifier_add_custom_header( Adds a custom header to be added to the requests made to the provider. [Rust - `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_custom_header) + `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_add_custom_header) """ lib.pactffi_verifier_add_custom_header( handle._ref, @@ -6719,7 +6742,7 @@ def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: Adds a Pact file as a source to verify. [Rust - `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_file_source) + `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_add_file_source) """ lib.pactffi_verifier_add_file_source(handle._ref, file.encode("utf-8")) @@ -6731,7 +6754,7 @@ def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> Non All pacts from the directory that match the provider name will be verified. [Rust - `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_directory_source) + `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_add_directory_source) # Safety @@ -6753,7 +6776,7 @@ def verifier_url_source( Adds a URL as a source to verify. [Rust - `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_url_source) + `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_url_source) Args: handle: @@ -6793,7 +6816,7 @@ def verifier_broker_source( Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_broker_source) + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_broker_source) This will fetch all the pact files from the broker that match the provider name. @@ -6841,7 +6864,7 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) + `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) This will fetch all the pact files from the broker that match the provider name and the consumer version selectors (See [Consumer Version @@ -6906,7 +6929,7 @@ def verifier_execute(handle: VerifierHandle) -> None: """ Runs the verification. - (https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_execute) + (https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_execute) """ success: int = lib.pactffi_verifier_execute(handle._ref) if success != 0: @@ -6922,7 +6945,7 @@ def verifier_cli_args() -> str: string. [Rust - `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_cli_args) + `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_cli_args) The purpose is to then be able to use in other languages which wrap the FFI library, to implement the same CLI functionality automatically without @@ -6978,7 +7001,7 @@ def verifier_logs(handle: VerifierHandle) -> OwnedString: Extracts the logs for the verification run. [Rust - `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_logs) + `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_logs) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` @@ -6996,7 +7019,7 @@ def verifier_logs_for_provider(provider_name: str) -> OwnedString: Extracts the logs for the verification run for the provider name. [Rust - `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_logs_for_provider) + `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_logs_for_provider) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` @@ -7014,7 +7037,7 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: Extracts the standard output for the verification run. [Rust - `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_output) + `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_output) Args: handle: @@ -7037,7 +7060,7 @@ def verifier_json(handle: VerifierHandle) -> OwnedString: Extracts the verification result as a JSON document. [Rust - `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_json) + `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_json) """ ptr = lib.pactffi_verifier_json(handle._ref) if ptr == ffi.NULL: @@ -7061,7 +7084,7 @@ def using_plugin( otherwise you will have plugin processes left running. [Rust - `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_using_plugin) + `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_using_plugin) Args: pact: @@ -7100,7 +7123,7 @@ def cleanup_plugins(pact: PactHandle) -> None: zero). [Rust - `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_cleanup_plugins) + `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_cleanup_plugins) """ lib.pactffi_cleanup_plugins(pact._ref) @@ -7119,7 +7142,7 @@ def interaction_contents( format of the JSON contents. [Rust - `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_interaction_contents) + `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_interaction_contents) Args: interaction: @@ -7175,7 +7198,7 @@ def matches_string_value( function once it is no longer required. [Rust - `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_string_value) + `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_string_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string @@ -7206,7 +7229,7 @@ def matches_u64_value( function once it is no longer required. [Rust - `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_u64_value) + `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_u64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7236,7 +7259,7 @@ def matches_i64_value( function once it is no longer required. [Rust - `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_i64_value) + `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_i64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7266,7 +7289,7 @@ def matches_f64_value( function once it is no longer required. [Rust - `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_f64_value) + `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_f64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7296,7 +7319,7 @@ def matches_bool_value( function once it is no longer required. [Rust - `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_bool_value) + `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_bool_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get, 0 == false and 1 == true @@ -7328,7 +7351,7 @@ def matches_binary_value( # noqa: PLR0913 function once it is no longer required. [Rust - `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_binary_value) + `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_binary_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7363,7 +7386,7 @@ def matches_json_value( function once it is no longer required. [Rust - `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_matches_json_value) + `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_json_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string diff --git a/src/pact/v3/interaction/_base.py b/src/pact/v3/interaction/_base.py index baebef64d..1b375d29a 100644 --- a/src/pact/v3/interaction/_base.py +++ b/src/pact/v3/interaction/_base.py @@ -354,6 +354,11 @@ def set_comment(self, key: str, value: Any | None) -> Self: # noqa: ANN401 Value for the comment. This must be encodable using [`json.dumps(...)`][json.dumps], or an existing JSON string. The value of `None` will remove the comment with the given key. + + # Warning + + This function will overwrite any existing comment with the same key. In + particular, the `text` key is used by `add_text_comment`. """ if isinstance(value, str) or value is None: pact.v3.ffi.set_comment(self._handle, key, value) @@ -361,6 +366,27 @@ def set_comment(self, key: str, value: Any | None) -> Self: # noqa: ANN401 pact.v3.ffi.set_comment(self._handle, key, json.dumps(value)) return self + def add_text_comment(self, comment: str) -> Self: + """ + Add a text comment for the interaction. + + This is used by V4 interactions to set arbitrary text comments for the + interaction. + + Args: + comment: + Text of the comment. + + # Warning + + Internally, the comments are appended to an array under the `text` + comment key. Care should be taken to ensure that conflicts are not + introduced by + [`set_comment`][pact.v3.interaction.Interaction.set_comment]. + """ + pact.v3.ffi.add_text_comment(self._handle, comment) + return self + def test_name( self, name: str, diff --git a/src/pact/v3/interaction/_http_interaction.py b/src/pact/v3/interaction/_http_interaction.py index 9a2837cf0..335d6293c 100644 --- a/src/pact/v3/interaction/_http_interaction.py +++ b/src/pact/v3/interaction/_http_interaction.py @@ -149,7 +149,7 @@ def with_header( # JSON Matching Pact's matching rules are defined in the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md) + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) and support a wide range of matching rules. These can be specified using a JSON object as a strong using `json.dumps(...)`. For example, the above rule whereby the `X-Foo` header has multiple values can be @@ -391,7 +391,7 @@ def with_query_parameter(self, name: str, value: str) -> Self: ``` For more information on the format of the JSON object, see the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.18/rust/pact_ffi/IntegrationJson.md). + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). Args: name: From 5244da2ee21a4c0368c0b67b0b8a0a0573c27e76 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 11 Apr 2024 14:32:13 +1000 Subject: [PATCH 0331/1376] chore(test): disable failing tests on windows There is an issue tracking this, and until it is resolved, may as well disable the tests. Signed-off-by: JP-Ellis --- .../compatibility_suite/test_v1_provider.py | 105 ++++++++++++++++++ .../compatibility_suite/test_v2_provider.py | 18 +++ .../compatibility_suite/test_v3_provider.py | 10 ++ .../compatibility_suite/test_v4_provider.py | 10 ++ 4 files changed, 143 insertions(+) diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py index bdcca16fc..349ef3357 100644 --- a/tests/v3/compatibility_suite/test_v1_provider.py +++ b/tests/v3/compatibility_suite/test_v1_provider.py @@ -5,6 +5,7 @@ from __future__ import annotations import logging +import sys import pytest from pytest_bdd import given, parsers, scenario @@ -45,6 +46,10 @@ ################################################################################ +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Verifying a simple HTTP request", @@ -53,6 +58,10 @@ def test_verifying_a_simple_http_request() -> None: """Verifying a simple HTTP request.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Verifying multiple Pact files", @@ -61,6 +70,10 @@ def test_verifying_multiple_pact_files() -> None: """Verifying multiple Pact files.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Incorrect request is made to provider", @@ -69,6 +82,10 @@ def test_incorrect_request_is_made_to_provider() -> None: """Incorrect request is made to provider.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @pytest.mark.container() @scenario( "definition/features/V1/http_provider.feature", @@ -79,6 +96,10 @@ def test_verifying_a_simple_http_request_via_a_pact_broker() -> None: reset_broker_var.set(True) # noqa: FBT003 +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @pytest.mark.container() @scenario( "definition/features/V1/http_provider.feature", @@ -89,6 +110,10 @@ def test_verifying_a_simple_http_request_via_a_pact_broker_with_publishing() -> reset_broker_var.set(True) # noqa: FBT003 +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @pytest.mark.container() @scenario( "definition/features/V1/http_provider.feature", @@ -99,6 +124,10 @@ def test_verifying_multiple_pact_files_via_a_pact_broker() -> None: reset_broker_var.set(True) # noqa: FBT003 +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @pytest.mark.container() @scenario( "definition/features/V1/http_provider.feature", @@ -109,6 +138,10 @@ def test_incorrect_request_is_made_to_provider_via_a_pact_broker() -> None: reset_broker_var.set(True) # noqa: FBT003 +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Verifying an interaction with a defined provider state", @@ -117,6 +150,10 @@ def test_verifying_an_interaction_with_a_defined_provider_state() -> None: """Verifying an interaction with a defined provider state.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Verifying an interaction with no defined provider state", @@ -125,6 +162,10 @@ def test_verifying_an_interaction_with_no_defined_provider_state() -> None: """Verifying an interaction with no defined provider state.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Verifying an interaction where the provider state callback fails", @@ -133,6 +174,10 @@ def test_verifying_an_interaction_where_the_provider_state_callback_fails() -> N """Verifying an interaction where the provider state callback fails.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Verifying an interaction where a provider state callback is not configured", @@ -141,6 +186,10 @@ def test_verifying_an_interaction_where_no_provider_state_callback_configured() """Verifying an interaction where a provider state callback is not configured.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Verifying a HTTP request with a request filter configured", @@ -149,6 +198,10 @@ def test_verifying_a_http_request_with_a_request_filter_configured() -> None: """Verifying a HTTP request with a request filter configured.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Verifies the response status code", @@ -157,6 +210,10 @@ def test_verifies_the_response_status_code() -> None: """Verifies the response status code.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Verifies the response headers", @@ -165,6 +222,10 @@ def test_verifies_the_response_headers() -> None: """Verifies the response headers.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Response with plain text body (positive case)", @@ -173,6 +234,10 @@ def test_response_with_plain_text_body_positive_case() -> None: """Response with plain text body (positive case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Response with plain text body (negative case)", @@ -181,6 +246,10 @@ def test_response_with_plain_text_body_negative_case() -> None: """Response with plain text body (negative case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Response with JSON body (positive case)", @@ -189,6 +258,10 @@ def test_response_with_json_body_positive_case() -> None: """Response with JSON body (positive case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Response with JSON body (negative case)", @@ -197,6 +270,10 @@ def test_response_with_json_body_negative_case() -> None: """Response with JSON body (negative case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Response with XML body (positive case)", @@ -205,6 +282,10 @@ def test_response_with_xml_body_positive_case() -> None: """Response with XML body (positive case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Response with XML body (negative case)", @@ -213,6 +294,10 @@ def test_response_with_xml_body_negative_case() -> None: """Response with XML body (negative case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Response with binary body (positive case)", @@ -221,6 +306,10 @@ def test_response_with_binary_body_positive_case() -> None: """Response with binary body (positive case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Response with binary body (negative case)", @@ -229,6 +318,10 @@ def test_response_with_binary_body_negative_case() -> None: """Response with binary body (negative case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Response with form post body (positive case)", @@ -237,6 +330,10 @@ def test_response_with_form_post_body_positive_case() -> None: """Response with form post body (positive case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Response with form post body (negative case)", @@ -245,6 +342,10 @@ def test_response_with_form_post_body_negative_case() -> None: """Response with form post body (negative case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Response with multipart body (positive case)", @@ -253,6 +354,10 @@ def test_response_with_multipart_body_positive_case() -> None: """Response with multipart body (positive case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V1/http_provider.feature", "Response with multipart body (negative case)", diff --git a/tests/v3/compatibility_suite/test_v2_provider.py b/tests/v3/compatibility_suite/test_v2_provider.py index 303425aa5..94313bae2 100644 --- a/tests/v3/compatibility_suite/test_v2_provider.py +++ b/tests/v3/compatibility_suite/test_v2_provider.py @@ -5,7 +5,9 @@ from __future__ import annotations import logging +import sys +import pytest from pytest_bdd import given, parsers, scenario from tests.v3.compatibility_suite.util import ( @@ -28,6 +30,10 @@ ################################################################################ +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V2/http_provider.feature", "Supports matching rules for the response headers (positive case)", @@ -38,6 +44,10 @@ def test_supports_matching_rules_for_the_response_headers_positive_case() -> Non """ +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V2/http_provider.feature", "Supports matching rules for the response headers (negative case)", @@ -48,6 +58,10 @@ def test_supports_matching_rules_for_the_response_headers_negative_case() -> Non """ +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V2/http_provider.feature", "Verifies the response body (positive case)", @@ -58,6 +72,10 @@ def test_verifies_the_response_body_positive_case() -> None: """ +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V2/http_provider.feature", "Verifies the response body (negative case)", diff --git a/tests/v3/compatibility_suite/test_v3_provider.py b/tests/v3/compatibility_suite/test_v3_provider.py index c702d3e9c..eaa452a45 100644 --- a/tests/v3/compatibility_suite/test_v3_provider.py +++ b/tests/v3/compatibility_suite/test_v3_provider.py @@ -5,7 +5,9 @@ from __future__ import annotations import logging +import sys +import pytest from pytest_bdd import given, parsers, scenario from tests.v3.compatibility_suite.util import ( @@ -31,6 +33,10 @@ ################################################################################ +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/http_provider.feature", "Verifying an interaction with multiple defined provider states", @@ -41,6 +47,10 @@ def test_verifying_an_interaction_with_multiple_defined_provider_states() -> Non """ +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/http_provider.feature", "Verifying an interaction with a provider state with parameters", diff --git a/tests/v3/compatibility_suite/test_v4_provider.py b/tests/v3/compatibility_suite/test_v4_provider.py index 8d4463d81..bffc68f74 100644 --- a/tests/v3/compatibility_suite/test_v4_provider.py +++ b/tests/v3/compatibility_suite/test_v4_provider.py @@ -5,7 +5,9 @@ from __future__ import annotations import logging +import sys +import pytest from pytest_bdd import given, parsers, scenario from tests.v3.compatibility_suite.util import ( @@ -33,6 +35,10 @@ ################################################################################ +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V4/http_provider.feature", "Verifying a pending HTTP interaction", @@ -43,6 +49,10 @@ def test_verifying_a_pending_http_interaction() -> None: """ +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V4/http_provider.feature", "Verifying a HTTP interaction with comments", From a54461f4fb03f9d9ea0c476406aa04d91b6721b3 Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Thu, 11 Apr 2024 06:35:39 +0000 Subject: [PATCH 0332/1376] chore: update changelog v2.2.0 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c316a168..d265bf15b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## v2.2.0 (2024-04-11) + +### Feat + +- upgrade FFI to 0.4.19 +- **v3**: add verbose mismatches +- **v3**: add verifier class + +### Fix + +- **v3**: strip embedded user/password from urls +- **v3**: allow optional publish options +- delay pytest 8.1 + +### Refactor + +- remove relative imports +- **tests**: move parse_headers/matching_rules out of class + ## v2.1.3 (2024-03-07) ### Fix From 91a1e9200be7fd2475d3c124bb0c7cc101b2315c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 21:18:23 +0000 Subject: [PATCH 0333/1376] chore(deps): update dependency devel/ruff to v0.3.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2376c5fac..069e478ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.3.5"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.3.6"] ################################################################################ ## Hatch Build Configuration From 1fd6cbd1587877b7ad01a2162e01b5549e63ce0a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 21:18:27 +0000 Subject: [PATCH 0334/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.3.6 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1bc7b104..d3857e932 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.5 + rev: v0.3.6 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 9e394da3ac77a69766b910921aacaf22cb8f78bb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 21:18:31 +0000 Subject: [PATCH 0335/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.22.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d3857e932..b0ff970bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,7 +53,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.21.3 + rev: v3.22.0 hooks: - id: commitizen stages: [commit-msg] From a6e3aac7264aa8ec760b3552f95df537d6e7425a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:24:09 +0000 Subject: [PATCH 0336/1376] chore(deps): update peter-evans/create-pull-request digest to c55203c --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7f5313c6..941fda63d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -246,7 +246,7 @@ jobs: packages-dir: wheels - name: Create PR for changelog update - uses: peter-evans/create-pull-request@70a41aba780001da0a30141984ae2a0c95d8704e # v6 + uses: peter-evans/create-pull-request@c55203cfde3e5c11a452d352b4393e68b85b4533 # v6 with: token: ${{ secrets.GH_TOKEN }} commit-message: "chore: update changelog ${{ github.ref_name }}" From 6a6cde44c2eab2a8d26900ff0759b3549795ab6f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 04:40:58 +0000 Subject: [PATCH 0337/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.3.7 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0ff970bd..6a0dd1364 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.6 + rev: v0.3.7 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From a9bd5229e6172e9ccaa5d382e6eeaa11ab7ea3d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 04:40:55 +0000 Subject: [PATCH 0338/1376] chore(deps): update dependency devel/ruff to v0.3.7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 069e478ac..682571966 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.3.6"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.3.7"] ################################################################################ ## Hatch Build Configuration From 1f4da315c4e3cdc7ac2fb0cda141696132982bbb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:49:42 +0000 Subject: [PATCH 0339/1376] chore(deps): update ubuntu:22.04 docker digest to 1b8d8ff --- Dockerfile.ubuntu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 6c89305ab..3df37911b 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ubuntu:22.04@sha256:77906da86b60585ce12215807090eb327e7386c8fafb5402369e421f44eff17e +FROM ubuntu:22.04@sha256:1b8d8ff4777f36f19bfe73ee4df61e3a0b789caeff29caa019539ec7c9a57f95 ENV DEBIAN_FRONTEND=noninteractive ARG PYTHON_VERSION 3.9 From dfdf9ddf7c54f16c1efad6f6dcc4533056377030 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:56:38 +0000 Subject: [PATCH 0340/1376] chore(deps): update peter-evans/create-pull-request digest to 9153d83 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 941fda63d..cddb89a4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -246,7 +246,7 @@ jobs: packages-dir: wheels - name: Create PR for changelog update - uses: peter-evans/create-pull-request@c55203cfde3e5c11a452d352b4393e68b85b4533 # v6 + uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83 # v6 with: token: ${{ secrets.GH_TOKEN }} commit-message: "chore: update changelog ${{ github.ref_name }}" From 80f25ec21799fd140105e0a9a4a03d623c1072f2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:42:19 +0000 Subject: [PATCH 0341/1376] chore(deps): update actions/upload-artifact digest to 1746f4a --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cddb89a4f..06688923c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4 + uses: actions/upload-artifact@1746f4ab65b179e0ea60a494b83293b640dd5bba # v4 with: name: wheels-sdist path: ./dist/*.tar.* @@ -110,7 +110,7 @@ jobs: CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} - name: Upload wheels - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4 + uses: actions/upload-artifact@1746f4ab65b179e0ea60a494b83293b640dd5bba # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl @@ -168,7 +168,7 @@ jobs: CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} - name: Upload wheels - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4 + uses: actions/upload-artifact@1746f4ab65b179e0ea60a494b83293b640dd5bba # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }}-${{ matrix.build }} path: ./wheelhouse/*.whl From ac9dae3b3b0b5b9bee8e2318fe45fe802279e5ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:42:15 +0000 Subject: [PATCH 0342/1376] chore(deps): update actions/download-artifact digest to 8caf195 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06688923c..3b928c41c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -201,7 +201,7 @@ jobs: fetch-depth: 0 - name: Download wheels and sdist - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4 + uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # v4 with: path: wheels merge-multiple: true From 35af8af21a0681e733ae3deafe86ee7abd11b122 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:20:22 +0000 Subject: [PATCH 0343/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.24.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a0dd1364..94b174c41 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,7 +53,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.22.0 + rev: v3.24.0 hooks: - id: commitizen stages: [commit-msg] From 794bea41b25d8707626a92bd0816643d08848993 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Apr 2024 22:20:49 +0000 Subject: [PATCH 0344/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 94b174c41..382744f98 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.7 + rev: v0.4.0 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From 1ee19a97bdefe4f449967539468a86b3598b0e09 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Apr 2024 22:20:45 +0000 Subject: [PATCH 0345/1376] chore(deps): update dependency devel/ruff to v0.4.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 682571966..1b91a6ea9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.3.7"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.4.0"] ################################################################################ ## Hatch Build Configuration From 423e7aa1d117f5ea769a840e88b9e8fd9b1c6af1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:34:11 +0000 Subject: [PATCH 0346/1376] chore(deps): update actions/download-artifact digest to 9c19ed7 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3b928c41c..471785b53 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -201,7 +201,7 @@ jobs: fetch-depth: 0 - name: Download wheels and sdist - uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # v4 + uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4 with: path: wheels merge-multiple: true From dd9ffb4f083a102e167111fa3a470f81971fadde Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:43:43 +0000 Subject: [PATCH 0347/1376] chore(deps): update actions/checkout digest to 1d96c77 --- .github/workflows/build.yml | 8 ++++---- .github/workflows/docs.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/test.yml | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 471785b53..655dae0db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 with: # Fetch all tags fetch-depth: 0 @@ -71,7 +71,7 @@ jobs: archs: AMD64 steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 with: # Fetch all tags fetch-depth: 0 @@ -140,7 +140,7 @@ jobs: build: "" steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 with: # Fetch all tags fetch-depth: 0 @@ -195,7 +195,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 with: # Fetch all tags fetch-depth: 0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 00e6e567b..663e10c43 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 with: fetch-depth: 0 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index f336eab06..8071813da 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 - name: Synchronize labels uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e28f75d49..bd846e687 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,7 +52,7 @@ jobs: experimental: true steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 with: submodules: true @@ -108,7 +108,7 @@ jobs: experimental: [false] steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 with: submodules: true @@ -161,7 +161,7 @@ jobs: PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 - name: Set up Python 3 uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 @@ -194,7 +194,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 - name: Set up Python uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 From 031af832a18aa1fbf8714a35749cf711fa63cdef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:34:15 +0000 Subject: [PATCH 0348/1376] chore(deps): update actions/upload-artifact digest to 6546280 --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 655dae0db..78caf928b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@1746f4ab65b179e0ea60a494b83293b640dd5bba # v4 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 with: name: wheels-sdist path: ./dist/*.tar.* @@ -110,7 +110,7 @@ jobs: CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} - name: Upload wheels - uses: actions/upload-artifact@1746f4ab65b179e0ea60a494b83293b640dd5bba # v4 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl @@ -168,7 +168,7 @@ jobs: CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} - name: Upload wheels - uses: actions/upload-artifact@1746f4ab65b179e0ea60a494b83293b640dd5bba # v4 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }}-${{ matrix.build }} path: ./wheelhouse/*.whl From c3b23dc73b05904f0f34ca054542f2557444f48d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 13:11:11 +0000 Subject: [PATCH 0349/1376] chore(deps): update dependency devel/ruff to v0.4.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1b91a6ea9..5729688a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.4.0"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.4.1"] ################################################################################ ## Hatch Build Configuration From c9ad32464f60d3f0384ddd72c5a99f7f8ea5f559 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 20:08:51 +0000 Subject: [PATCH 0350/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.1 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 382744f98..92191e8de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.0 + rev: v0.4.1 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From c7520c6de71902b7f1ed87d84925630279380226 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:33:32 +0000 Subject: [PATCH 0351/1376] chore(deps): update actions/download-artifact digest to 65a9edc --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 78caf928b..083cc864a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -201,7 +201,7 @@ jobs: fetch-depth: 0 - name: Download wheels and sdist - uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4 with: path: wheels merge-multiple: true From 875a6276b7fc993d118f7c2d934dc469c5aa6033 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:33:36 +0000 Subject: [PATCH 0352/1376] chore(deps): update dependency devel-types/mypy to v1.10.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5729688a6..527ab8e4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ pact-verifier = "pact.cli.verify:main" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. devel-types = [ - "mypy ==1.9.0", + "mypy ==1.10.0", "types-cffi ~=1.0", "types-requests ~=2.0", ] From d9666e9c63a5eb29bd0a4c5cbe966768e42aafd3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 25 Apr 2024 10:40:48 +1000 Subject: [PATCH 0353/1376] ci: fix macos-latest The GitHub runners behind `macos-latest` were recently upgraded to `macos-14` running on ARM / M1 architecture. Unfortunately, Python 3.8 and 3.9 aren't supported on ARM. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bd846e687..41cbcf382 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -98,14 +98,23 @@ jobs: Tests py${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.experimental }} strategy: fail-fast: false matrix: os: [windows-latest, macos-latest] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - experimental: [false] + # Python 3.8 and 3.9 aren't supported on macos-latest (ARM) + exclude: + - os: macos-latest + python-version: "3.8" + - os: macos-latest + python-version: "3.9" + include: + - os: macos-13 + python-version: "3.8" + - os: macos-13 + python-version: "3.9" steps: - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 From 6d0a5079eaa6738206c3dae6512871d2435e757d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:23:15 +0000 Subject: [PATCH 0354/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.25.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 92191e8de..9d30176db 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,7 +53,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.24.0 + rev: v3.25.0 hooks: - id: commitizen stages: [commit-msg] From 3a2f6ce3bbe63f2a365db71af483b79637903822 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 04:16:22 +0000 Subject: [PATCH 0355/1376] chore(deps): update pre-commit hook igorshubovych/markdownlint-cli to v0.40.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9d30176db..ce32d5c5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -71,7 +71,7 @@ repos: stages: [pre-push] - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.39.0 + rev: v0.40.0 hooks: - id: markdownlint exclude: | From cd49ddb19df3fd4aed0bebad476c39f240b94c78 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 22:05:25 +0000 Subject: [PATCH 0356/1376] chore(deps): update ubuntu:22.04 docker digest to 6d7b5d3 --- Dockerfile.ubuntu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 3df37911b..79ebe70bc 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ubuntu:22.04@sha256:1b8d8ff4777f36f19bfe73ee4df61e3a0b789caeff29caa019539ec7c9a57f95 +FROM ubuntu:22.04@sha256:6d7b5d3317a71adb5e175640150e44b8b9a9401a7dd394f44840626aff9fa94d ENV DEBIAN_FRONTEND=noninteractive ARG PYTHON_VERSION 3.9 From d7366f3009b7db61d7ef8416c7e1e5a05624286c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 20:08:51 +0000 Subject: [PATCH 0357/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.2 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce32d5c5a..00210c562 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.1 + rev: v0.4.2 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From fa4b11e81bfbb8c5fd5b7582be92888a8c9d1eb2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:56:36 +0000 Subject: [PATCH 0358/1376] chore(deps): update actions/checkout digest to 0ad4b8f --- .github/workflows/build.yml | 8 ++++---- .github/workflows/docs.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/test.yml | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 083cc864a..cd81a2ad4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 with: # Fetch all tags fetch-depth: 0 @@ -71,7 +71,7 @@ jobs: archs: AMD64 steps: - - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 with: # Fetch all tags fetch-depth: 0 @@ -140,7 +140,7 @@ jobs: build: "" steps: - - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 with: # Fetch all tags fetch-depth: 0 @@ -195,7 +195,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 with: # Fetch all tags fetch-depth: 0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 663e10c43..c8214fa68 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 with: fetch-depth: 0 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 8071813da..ba5490dd0 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 - name: Synchronize labels uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41cbcf382..4f0900c6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,7 +52,7 @@ jobs: experimental: true steps: - - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 with: submodules: true @@ -117,7 +117,7 @@ jobs: python-version: "3.9" steps: - - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 with: submodules: true @@ -170,7 +170,7 @@ jobs: PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite steps: - - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 - name: Set up Python 3 uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 @@ -203,7 +203,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 - name: Set up Python uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 From ae82bfcecbdf39a67de5ff73e31c8fbf5759a9f2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:03:30 +0000 Subject: [PATCH 0359/1376] chore(deps): update peter-evans/create-pull-request digest to 6d6857d --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd81a2ad4..a3c15ea6f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -246,7 +246,7 @@ jobs: packages-dir: wheels - name: Create PR for changelog update - uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83 # v6 + uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6 with: token: ${{ secrets.GH_TOKEN }} commit-message: "chore: update changelog ${{ github.ref_name }}" From 0a69c0c603612edd1c14e3132020a84c23e6a2b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 23:56:52 +0000 Subject: [PATCH 0360/1376] chore(deps): update ubuntu docker tag to v24 --- Dockerfile.ubuntu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 79ebe70bc..67e41f127 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ubuntu:22.04@sha256:6d7b5d3317a71adb5e175640150e44b8b9a9401a7dd394f44840626aff9fa94d +FROM ubuntu:24.04@sha256:562456a05a0dbd62a671c1854868862a4687bf979a96d48ae8e766642cd911e8 ENV DEBIAN_FRONTEND=noninteractive ARG PYTHON_VERSION 3.9 From d6869797b52429252b5d0da4d0fc0079f9d3671c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 20:08:47 +0000 Subject: [PATCH 0361/1376] chore(deps): update dependency devel/ruff to v0.4.2 Signed-off-by: JP-Ellis --- pyproject.toml | 2 +- tests/v3/compatibility_suite/util/provider.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 527ab8e4b..a8ef954c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.4.1"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.4.2"] ################################################################################ ## Hatch Build Configuration diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index d060b72f7..4c9d0d5dd 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -291,7 +291,7 @@ def run(self) -> None: Start the provider. """ url = URL(f"http://localhost:{_find_free_port()}") - sys.stderr.write("Starting provider on %s\n" % url) + sys.stderr.write(f"Starting provider on {url}\n") for endpoint in self.app.url_map.iter_rules(): sys.stderr.write(f" * {endpoint}\n") From 32b5cfe9fac476e70d5902395ce929adf08c6ad8 Mon Sep 17 00:00:00 2001 From: David Rettie Date: Thu, 2 May 2024 22:24:47 +0100 Subject: [PATCH 0362/1376] docs(CONTRIBUTING.md): update installation steps adds step to patch the compatibility suite in order for tests to pass --- CONTRIBUTING.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b49f5a069..4fc271cb0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,9 +101,11 @@ You can also try using the new [github.dev](https://github.dev/pact-foundation/p 3. After cloning the repository, run `hatch shell` in the root of the repository. This will install all dependencies in a Python virtual environment and then ensure that the virtual environment is activated. You will also need to run `git submodule init` if you want to run tests, as Pact Python makes use of the Pact Compability Suite. -4. To run tests, run `hatch run test` to make sure the test suite is working. You should also make sure the example works by running `hatch run example`. For the examples, you will have to make sure that you have Docker (or a suitable alternative) installed and running. +4. Patch the compatibility suite by running `cd tests/v3/compatibility_suite && patch -p1 -d definition < definition-update.diff && cd -` in the root of the repository. -5. If you want to run the tests against all supported Python versions, you can run `hatch run test:all`. +5. To run tests, run `hatch run test` to make sure the test suite is working. You should also make sure the example works by running `hatch run example`. For the examples, you will have to make sure that you have Docker (or a suitable alternative) installed and running. + +6. If you want to run the tests against all supported Python versions, you can run `hatch run test:all`. ### Code Conventions From 734a2e45e6ffc4e3fdfcee50d93251938984e578 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 2 May 2024 11:10:33 +1000 Subject: [PATCH 0363/1376] docs: add additional code capabilities When displaying code snippets, allow for annotations to be inserted within the code block, and add a `copy` button. Signed-off-by: JP-Ellis --- mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index e0319c477..d2f50da06 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -114,6 +114,8 @@ theme: repo: fontawesome/brands/github features: + - content.code.annotate + - content.code.copy - content.tooltips - navigation.indexes - navigation.instant From ba665f529401c64a671a5309bcc62584e95f3816 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 2 May 2024 11:13:25 +1000 Subject: [PATCH 0364/1376] docs: add blog post about rust ffi Signed-off-by: JP-Ellis --- ...2 integrating rust ffi with pact python.md | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md diff --git a/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md b/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md new file mode 100644 index 000000000..e48cd0f5e --- /dev/null +++ b/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md @@ -0,0 +1,187 @@ +--- +authors: + - JP-Ellis +date: + created: 2024-05-02 +--- + +# Integrating Rust FFI with Pact Python + +In the [forthcoming release of Pact Python version 3](./04-11 a sneak peek into the pact python future.md), we're excited to be integrating our library with the ['Rust core'](https://github.com/pact-foundation/pact-reference), a Rust-based library that encapsulates Pact's fundamental operations for both consumers and providers. Known for its high performance and safety guarantees, [Rust](https://rust-lang.org) enables us to enhance the robustness and efficiency of our implementation. This move also promises simplified maintenance and scalability for future iterations of both the Pact Python library, and the [broader Pact ecosystem](https://docs.pact.io/diagrams/ecosystem). + +At its essence, this Rust-powered engine handles critical tasks such as parsing and serializing Pact files, matching requests with responses, and generating new Pact contracts. It provides mocking capabilities to simulate a provider when verifying a consumer, and equally acts in reverse when replaying consumer requests against a provider. By adopting this shared core logic from Rust, we will achieve uniformity across all languages implementing Pact while streamlining the integration of enhancements or bug fixes-benefits across our diverse ecosystem. + +In this blog post, I will delve into how this is all achieved. From explaining how [Hatch](https://hatch.pypa.io) is used to compile a binary extension and generate wheels for all supported platforms, to the intricacies of interfacing with the binary library. This information is not required to use Pact Python, but hopes to provide a deeper understanding of the inner workings of the library. + + + +## Briding Python and Binary Libraries + +Python, known for its dynamic typing and automated memory management, is fundamentally an interpreted language. Despite not having innate capabilities to directly interact with binary libraries, most Python interpreters bridge this gap efficiently. For instance, CPython—the principal interpreter—enables the creation of binary extensions[^1] and similarly, PyPy—a widely-used alternative—offers comparable functionalities[^2]. + +[^1]: You can find extensive documentation on building extensions for CPython [here](https://docs.python.org/3/extending/extending.html). +[^2]: PyPy extension-building documentation is available [here](https://doc.pypy.org/en/latest/extending.html). + +However, each interpreter has a distinct API tailored for crafting these binary extensions, which unfortunately leads to a lack of universal solutions across different environments. Furthermore, interpreters like [Jython](https://jython.org) and [Pyodide](https://pyodide.org/en/stable/), which are based on Java and WebAssembly respectively, present unique challenges that often preclude the straightforward use of such extensions due to their distinct runtime architectures.[^3] + +[^3]: It would appear that Pyodide can support C extensions as explained [here](https://pyodide.org/en/stable/development/new-packages.html), though by and large Pyodide appears to be intended for pure Python packages. + +While it is possible for the extension to contain all the logic, our specific requirement is merely to provide a bridge between Python and the Rust core library. This is the niche that [Python C Foreign Function Interface (CFFI)](https://cffi.readthedocs.io/en/stable/) fills. By parsing a C header file, CFFI automates the generation of extension code needed for Python to interface with the binary library. Consequently, this library can be imported into Python as if it were any standard module—streamlining development and potentially improving performance by leveraging optimized native code. + +Moreover, CFFI offers a simpler and more maintainable approach compared to other methods requiring manual boilerplate code. It abstracts away many of the complexities associated with linking Python to C libraries, making it an attractive choice for developers looking for efficiency and ease of integration. + +## Building the Python Extension + +Pact Python uses the fantastic [Hatch](https://hatch.pypa.io) project management and build system for handling dependencies, project metadata, and generate wheels across all supported platforms. Hatch can be extensively customised to suit the needs of each project through its configuration, plugin system, and ability to define custom interfaces. + +In the case of Pact Python, a [`BuildHookInterface`](https://hatch.pypa.io/1.9/plugins/build-hook/reference/) is defined in [`hatch_build.py`](https://github.com/pact-foundation/pact-python/blob/d6869797b52429252b5d0da4d0fc0079f9d3671c/hatch_build.py) which executes several crucial tasks: + +1. Downloads a specified version of the Rust core library from a designated release on the Pact Foundation's GitHub repository, including the accompanying `pact.h` header file. +2. Utilizes CFFI to create a Python extension module that encapsulates the Rust core library: + + ```python + ffibuilder = cffi.FFI() + with (self.tmpdir / "pact.h").open("r", encoding="utf-8") as f: + ffibuilder.cdef(f.read()) # (1) + ffibuilder.set_source( + "_ffi", # (2) + "\n".join([*includes, '#include "pact.h"']), + libraries=["pact_ffi", *extra_libs], # (3) + library_dirs=[str(self.tmpdir)], # (4) + ) + output = Path(ffibuilder.compile(verbose=True, tmpdir=str(self.tmpdir))) # (5) + shutil.copy(output, PACT_ROOT_DIR / "v3") + ``` + + 1. The `cdef` method processes the contents of `pact.h`, creating necessary declarations for the Python extension. + 2. Names the extension module `_ffi`, which is subsequently importable in Python via `import _ffi`. + 3. Details libraries to be linked, including `pact_ffi` and platform-specific additional libraries (`extra_libs`) as needed. + 4. Defines the directory that holds the Rust code library. + 5. Compiles the extension module and then relocates it to the Pact Python project directory. + +Upon completion of these steps, Hatch produces a Python extension module that interfaces seamlessly with the Rust core library. It will have a filename like `src/pact/v3/_ffi.cpython-312-darwin.so` (for CPython 3.12 on macOS) which can be used just as any other Python module. That is, the binary `_ffi` file can be imported in the same way as one would import a regular `.py` file. + +## Using the CFFI Extension + +With the Python extension module built, developers have direct access to interact with the Rust core library from their Python code. This is made possible through two main components generated by CFFI: + +1. `lib`, which provides access to functions and data structures from the Rust core library; +2. `ffi`, which offers additional utilities on the Python side for interfacing with these Rust components. + +Let's look at a simple example of using the CFFI extension to invoke the `pactffi_version` function from the Rust core library: + +```python +from _ffi import lib, ffi + +version = lib.pactffi_version() # (1) +version = ffi.string(version) # (2) +if isinstance(version, bytes): # (3) + version = version.decode("utf-8") +``` + +1. Call the `pactffi_version` function from Rust, which returns a pointer to a null-terminated string. This is represented in Python as a `cdata 'char *'` object. +2. Convert the pointer to a Python string, or bytes if necessary, using the `ffi.string` method. +3. Decode the bytes to a string if needed. + +While the process is reasonably straightforward, it does require some boilerplate code to handle the type conversions. To simplify this, we've wrapped each function from the Rust core library in a simple Python function that performs these conversion autoamtically. You can find these wrapper functions in the [`ffi` module](https://github.com/pact-foundation/pact-python/blob/d6869797b52429252b5d0da4d0fc0079f9d3671c/src/pact/v3/ffi.py). For example, the `version` function is implemented as follows: + +```python +def version() -> str: + """ + Return the version of the pact_ffi library. + + [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_version) + + Returns: + The version of the pact_ffi library as a string, in the form of `x.y.z`. + """ + v = ffi.string(lib.pactffi_version()) + if isinstance(v, bytes): + return v.decode("utf-8") + return v +``` + +The majority of the Rust core library functions return some trivial data types (booleans and integers) which are transparently handled by CFFI without the need for additional conversions. However, there typically is still a need to appropriately manage conversion of arguments into the expected types. A typical pattern will be converting an `str | None` into a `cdata 'char *'`, where `None` is represented as a null pointer: + +```python +def foobar(value: str | None) -> bool: + return lib.foobar(value.encode("utf-8") if value else ffi.NULL) # (1) +``` + +1. The encoding of the string to UTF-8 ensures that the string is correctly represented in the Rust core library. + +### Error Handling + +Handling errors across programming languages can be challenging due to differences in error handling mechanisms. The Rust programming language has two methods of handling unexpected errors: + +1. **Panicking**: This typically occurs when a function encounters an unrecoverable error and terminates the program. The Rust core library handles panics by catching them before they propagate to the Python interpreter, and therefore they can be safely ignored. + +2. **Result**: This is a more structured approach whereby a function can return either `Ok(value)` or `Err(error)` to indicate success or failure. + +It is unfortunately difficult for the C foreign function interface to handle Rust's `Result` type directly. Instead, we've opted to using return codes, either in the form of a boolean or an integer, to indicate success or failure. This is a common pattern in C libraries and is easily translated into Python: + +```python +def write_pact_file( + mock_server_handle: PactServerHandle, + directory: str | Path, + *, + overwrite: bool, +) -> None: + ret: int = lib.pactffi_write_pact_file( + mock_server_handle._ref, + str(directory).encode("utf-8"), + overwrite, + ) + if ret == 0: + return # (1) + if ret == 1: + msg = ( + f"The function panicked while writing the Pact for {mock_server_handle} in" + f" {directory}." + ) + elif ret == 2: + msg = ( + f"The Pact file for {mock_server_handle} could not be written in" + f" {directory}." + ) + else: + msg = ( + "An unknown error occurred while writing the Pact for" + f" {mock_server_handle} in {directory}." + ) + raise RuntimeError(msg) +``` + +1. A return code of `0` indicates success, and the function returns without raising an exception. Other return codes indicate different types of errors, which are then translated into Python exceptions. + +By ensuring that the return codes are correctly handled, we can ensure that end-users are aware of any issues that arise during the execution of the Rust core library functions in a Pythonic manner. + +### Memory Management + +Memory management is another critical aspect to consider when interfacing with binary libraries. Rust's memory model is based on ownership and borrowing, which ensures memory safety and eliminates the need for manual memory management. When interfacing with other languages though, Rust cannot guarantee memory safety, and additional care must be taken to prevent memory leaks. Python, on the other hand, relies on garbage collection to manage memory automatically, which works by checking whether an object is still reachable and deallocating it if not. + +In the case of the Rust core library, the ability to deallocate memory is provided by specific functions such as `pactffi_string_delete`. Python also offers a mechanism to hook into the garbage collection process using the `__del__` method. A good example of this is the `OwnedString` class from the `ffi` module, which automatically deallocates memory when the object is no longer reachable: + +```python +class OwnedString(str): + def __new__(cls, ptr: cffi.FFI.CData) -> Self: + s = ffi.string(ptr) + return super().__new__( + cls, + s if isinstance(s, str) else s.decode("utf-8"), + ) + + def __del__(self) -> None: + lib.pactffi_string_delete(self._ptr) +``` + +The `__del__` method is called[^4] when the object is about to be deallocated[^5], allowing us to free the memory associated with the string. This ensures that memory is managed correctly and prevents potential memory leaks. + +[^4]: There are some unique circumstances where `__del__` may not be called, such as when the Python interpreter is shutting down. +[^5]: Python does not provide guarantees on when `__del__` will be called, so it is not recommended to rely on it for critical cleanup tasks. Instead, the `__enter__` and `__exit__` methods should be used to guarantee timely cleanup. + +## Conclusion + +Integrating Pact Python with the Rust FFI represents a significant step towards enhancing the robustness and efficiency of our library. With the release of version 3 of Pact Python, it is our hope that the community will greatly benefit from the improved performance provided by the Rust core library. + +It is our hope that this blog post also helps to shed some light on the inner workings of the library, whether you are a Pact user who is curious about how the library functions, or a developer looking to contribute to the project. From 2d644c7f634338d5e73d0ab9b8010ffcc57ff880 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 26 May 2024 01:04:11 +0000 Subject: [PATCH 0365/1376] chore(deps): update pre-commit hook igorshubovych/markdownlint-cli to v0.41.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 00210c562..708b5c2bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -71,7 +71,7 @@ repos: stages: [pre-push] - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.40.0 + rev: v0.41.0 hooks: - id: markdownlint exclude: | From 56ee8cd530fb84ee607f0a34283c5a517966a803 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 20:22:53 +0000 Subject: [PATCH 0366/1376] chore(deps): update actions/checkout digest to a5ac7e5 --- .github/workflows/build.yml | 8 ++++---- .github/workflows/docs.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/test.yml | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a3c15ea6f..3f22b86cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 with: # Fetch all tags fetch-depth: 0 @@ -71,7 +71,7 @@ jobs: archs: AMD64 steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 with: # Fetch all tags fetch-depth: 0 @@ -140,7 +140,7 @@ jobs: build: "" steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 with: # Fetch all tags fetch-depth: 0 @@ -195,7 +195,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 with: # Fetch all tags fetch-depth: 0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c8214fa68..7a1f90994 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 with: fetch-depth: 0 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index ba5490dd0..bc031c1d5 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 - name: Synchronize labels uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4f0900c6d..bade09d59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,7 +52,7 @@ jobs: experimental: true steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 with: submodules: true @@ -117,7 +117,7 @@ jobs: python-version: "3.9" steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 with: submodules: true @@ -170,7 +170,7 @@ jobs: PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 - name: Set up Python 3 uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 @@ -203,7 +203,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 - name: Set up Python uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 From dc68ac31a593d3405455b6103243503a82f336fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 20:22:57 +0000 Subject: [PATCH 0367/1376] chore(deps): update codecov/codecov-action digest to 125fc84 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bade09d59..e36a14a00 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,7 +89,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # v4 + uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # v4 with: token: ${{ secrets.CODECOV_TOKEN }} From ba851e1aaeda794cafbcfe4ff9b433dff5095c1b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 20:23:06 +0000 Subject: [PATCH 0368/1376] chore(deps): update pypa/cibuildwheel action to v2.18.1 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f22b86cc..a2c9d6357 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,7 +104,7 @@ jobs: echo "MACOSX_DEPLOYMENT_TARGET=10.12" >> "$GITHUB_ENV" - name: Create wheels - uses: pypa/cibuildwheel@8d945475ac4b1aac4ae08b2fd27db9917158b6ce # v2.17.0 + uses: pypa/cibuildwheel@ba8be0d98853f5744f24e7f902c8adef7ae2e7f3 # v2.18.1 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} @@ -162,7 +162,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@8d945475ac4b1aac4ae08b2fd27db9917158b6ce # v2.17.0 + uses: pypa/cibuildwheel@ba8be0d98853f5744f24e7f902c8adef7ae2e7f3 # v2.18.1 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} From 6c6065868cdf09171fb55a31f89e035c72c176d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 16:37:38 +0000 Subject: [PATCH 0369/1376] chore(deps): update softprops/action-gh-release digest to 69320db --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2c9d6357..948ce038a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -230,7 +230,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 # v2 + uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87 # v2 with: files: wheels/* body_path: ${{ runner.temp }}/changelog From 96c9e1854f685cac7097d17e2de300e26c84c206 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 07:36:23 +0000 Subject: [PATCH 0370/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.27.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 708b5c2bf..15567089b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,7 +53,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.25.0 + rev: v3.27.0 hooks: - id: commitizen stages: [commit-msg] From bed8b67fbcb77b5b20f49b2ad1e01c16349a93e1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 20:23:01 +0000 Subject: [PATCH 0371/1376] chore(deps): update pactfoundation/pact-broker:latest docker digest to fc44f0a --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e36a14a00..693b00b1f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:8f10947f230f661ef21f270a4abcf53214ba27cd68063db81de555fcd93e07dd + image: pactfoundation/pact-broker:latest@sha256:fc44f0a6e731c7de78bf44923b9ab41e5d3351388bfcf2f648e72844041cf74d ports: - "9292:9292" env: @@ -158,7 +158,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:8f10947f230f661ef21f270a4abcf53214ba27cd68063db81de555fcd93e07dd + image: pactfoundation/pact-broker:latest@sha256:fc44f0a6e731c7de78bf44923b9ab41e5d3351388bfcf2f648e72844041cf74d ports: - "9292:9292" env: From c9937020ea785a8273cec6789a0bd664cf64e5db Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 5 Jun 2024 15:25:24 +1000 Subject: [PATCH 0372/1376] chore: group renovate updates If an update exists both in pre-commit and as an explicit dependency, make sure that they are updated in tandem. Signed-off-by: JP-Ellis --- .github/renovate.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 145fc10b7..bde45ac7f 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -8,5 +8,11 @@ "enabled": true }, "prHourlyLimit": 0, - "prConcurrentLimit": 0 + "prConcurrentLimit": 0, + "packageRules": [ + { + "groupName": "Ruff", + "matchPackageNames": ["astral-sh/ruff-pre-commit", "devel/ruff"] + } + ] } From 4b244f98e68df08ca4bad9f79300b8d6c6104edb Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 5 Jun 2024 16:24:58 +1000 Subject: [PATCH 0373/1376] ci: narrow when docs are built and published With this change: - Docs are built on pushes to `master`, PRs targetting `master`, and when new `v*` tags are pushed. - Docs are published only when new `v*` tags are pushed (in line with new releases). Signed-off-by: JP-Ellis --- .github/workflows/docs.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7a1f90994..42854dff4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,6 +2,13 @@ name: docs on: push: + tags: + - v* + branches: + - master + pull_request: + branches: + - master env: STABLE_PYTHON_VERSION: "3.12" @@ -41,7 +48,7 @@ jobs: publish: name: Publish docs - if: github.ref == 'refs/heads/master' + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') needs: build runs-on: ubuntu-latest From 1edb3183b924501c2b855476f1bc94fb8cf90a0a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:46:36 +0000 Subject: [PATCH 0374/1376] chore(deps): update pypa/cibuildwheel action to v2.19.0 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 948ce038a..37c6705a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,7 +104,7 @@ jobs: echo "MACOSX_DEPLOYMENT_TARGET=10.12" >> "$GITHUB_ENV" - name: Create wheels - uses: pypa/cibuildwheel@ba8be0d98853f5744f24e7f902c8adef7ae2e7f3 # v2.18.1 + uses: pypa/cibuildwheel@a8d190a111314a07eb5116036c4b3fb26a4e3162 # v2.19.0 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} @@ -162,7 +162,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@ba8be0d98853f5744f24e7f902c8adef7ae2e7f3 # v2.18.1 + uses: pypa/cibuildwheel@a8d190a111314a07eb5116036c4b3fb26a4e3162 # v2.19.0 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} From f98a927f4cfa062eaf2a8751a9921193ba4babe8 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 15:18:28 +1000 Subject: [PATCH 0375/1376] chore: use uv to install packages [uv](https://github.com/astral-sh/uv) provides similar capabilities to `pip` and `venv`, but is _significantly_ faster. Since `hatch` now supports using `uv` under the hood when managing virtual environments, we can get the speed ups for free. Signed-off-by: JP-Ellis --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index a8ef954c2..29dc4fb89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,6 +147,7 @@ artifacts = [ # Install dev dependencies in the default environment to simplify the developer # workflow. [tool.hatch.envs.default] +installer = "uv" features = ["devel"] extra-dependencies = [ "hatchling", @@ -169,6 +170,7 @@ docs-build = "mkdocs build {args}" # Test environment for running unit tests. This automatically tests against all # supported Python versions. [tool.hatch.envs.test] +installer = "uv" features = ["devel-test"] [[tool.hatch.envs.test.matrix]] From 40088095e1a3478d0f12922840d825917a845f14 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 21 Jun 2024 13:44:01 +1000 Subject: [PATCH 0376/1376] chore(v3): re-export Pact and Verifier at root Use the `__all__` to mark the `Pact` and `Verifier` imports as public re-exports, thereby removing the need to silence F401. Signed-off-by: JP-Ellis --- src/pact/v3/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pact/v3/__init__.py b/src/pact/v3/__init__.py index 91329f2dc..15a207faa 100644 --- a/src/pact/v3/__init__.py +++ b/src/pact/v3/__init__.py @@ -70,8 +70,8 @@ import warnings -from pact.v3.pact import Pact # noqa: F401 -from pact.v3.verifier import Verifier # noqa: F401 +from pact.v3.pact import Pact +from pact.v3.verifier import Verifier warnings.warn( "The `pact.v3` module is not yet stable. Use at your own risk, and expect " @@ -79,3 +79,5 @@ stacklevel=2, category=ImportWarning, ) + +__all__ = ["Pact", "Verifier"] From c6616a4f23be71ab544c5fbe18213e2fce6877f8 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 21 Jun 2024 13:42:50 +1000 Subject: [PATCH 0377/1376] feat(ffi): upgrade ffi 0.4.21 Main changes include: - Addition of a `PactAsyncMessageIterator` (and related functions) - Addition of `*_generate_contents` functions which are analogous to the `*_get_contents` functions, but replace any matchers/generators with the relevant substitution in order to generate a 'real' payload. - Addition of `with_metadata` to add metadat to an interaction - Addition of `with_generators` to add generators to an interaction - Deprecation of the `MessagePactHandle` and `MessageHandle` Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 5 +- hatch_build.py | 6 +- src/pact/v3/ffi.py | 798 ++++++++++++------------------- src/pact/v3/interaction/_base.py | 100 ++++ 4 files changed, 413 insertions(+), 496 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 37c6705a4..987bc3ee7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -135,9 +135,12 @@ jobs: - os: ubuntu-20.04 archs: aarch64 build: musllinux - - os: macos-12 + - os: macos-14 archs: arm64 build: "" + - os: windows-2019 + archs: ARM64 + build: "" steps: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 diff --git a/hatch_build.py b/hatch_build.py index 9842fb6a4..ab5e84e0f 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -36,7 +36,7 @@ # Latest version available at: # https://github.com/pact-foundation/pact-reference/releases -PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.19") +PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.21") PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{machine}.{ext}" @@ -256,7 +256,7 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 if platform.startswith("macosx"): os = "macos" if platform.endswith("arm64"): - machine = "aarch64-apple-darwin" + machine = "aarch64" elif platform.endswith("x86_64"): machine = "x86_64" else: @@ -274,6 +274,8 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 if platform.endswith("amd64"): machine = "x86_64" + elif platform.endswith(("arm64", "aarch64")): + machine = "aarch64" else: raise UnsupportedPlatformError(platform) return PACT_LIB_URL.format( diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index e23d94b86..df18845af 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -193,31 +193,68 @@ class MessageMetadataIterator: ... class MessageMetadataPair: ... -class MessagePact: ... - +class Mismatch: ... -class MessagePactHandle: ... +class Mismatches: ... -class MessagePactMessageIterator: ... +class MismatchesIterator: ... -class MessagePactMetadataIterator: ... +class Pact: ... -class MessagePactMetadataTriple: ... +class PactAsyncMessageIterator: + """ + Iterator over a Pact's asynchronous messages. + """ -class Mismatch: ... + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Pact Asynchronous Message Iterator. + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct PactAsyncMessageIterator *": + msg = ( + "ptr must be a struct PactAsyncMessageIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr -class Mismatches: ... + def __str__(self) -> str: + """ + Nice string representation. + """ + return "PactAsyncMessageIterator" + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"PactAsyncMessageIterator({self._ptr!r})" -class MismatchesIterator: ... + def __del__(self) -> None: + """ + Destructor for the Pact Synchronous Message Iterator. + """ + pact_async_message_iter_delete(self) + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self -class Pact: ... + def __next__(self) -> AsynchronousMessage: + """ + Get the next message from the iterator. + """ + return pact_async_message_iter_next(self) class PactHandle: @@ -1440,6 +1477,28 @@ def async_message_get_contents(message: AsynchronousMessage) -> MessageContents: raise NotImplementedError +def async_message_generate_contents( + message: AsynchronousMessage, +) -> MessageContents | None: + """ + Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. + + This function differs from `async_message_get_contents` in + that it will process the message contents for any generators or matchers + that are present in the message in order to generate the actual message + contents as would be received by the consumer. + + [Rust + `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_generate_contents) + + If the message contents are missing, this function will return `None`. + """ + return MessageContents( + lib.pactffi_async_message_generate_contents(message._ptr), + owned=False, + ) + + def async_message_get_contents_str(message: AsynchronousMessage) -> str: """ Get the message contents of an `AsynchronousMessage` in string form. @@ -3037,6 +3096,29 @@ def pact_message_iter_next(iter: PactMessageIterator) -> Message: return Message(ptr) +def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> AsynchronousMessage: + """ + Get the next asynchronous message from the iterator. + + [Rust + `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_async_message_iter_next) + """ + ptr = lib.pactffi_pact_async_message_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return AsynchronousMessage(ptr, owned=True) + + +def pact_async_message_iter_delete(iter: PactAsyncMessageIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_async_message_iter_delete) + """ + lib.pactffi_pact_async_message_iter_delete(iter._ptr) + + def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMessage: """ Get the next synchronous request/response message from the V4 pact. @@ -3584,229 +3666,6 @@ def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: raise NotImplementedError -def message_pact_new_from_json(file_name: str, json_str: str) -> MessagePact: - """ - Construct a new `MessagePact` from the JSON string. - - The provided file name is used when generating error messages. - - [Rust - `pactffi_message_pact_new_from_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_new_from_json) - - # Safety - - The `file_name` and `json_str` parameters must both be valid UTF-8 encoded - strings. - - # Error Handling - - On error, this function will return a null pointer. - """ - raise NotImplementedError - - -def message_pact_delete(message_pact: MessagePact) -> None: - """ - Delete the `MessagePact` being pointed to. - - [Rust - `pactffi_message_pact_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_delete) - """ - raise NotImplementedError - - -def message_pact_get_consumer(message_pact: MessagePact) -> Consumer: - """ - Get a pointer to the Consumer struct inside the MessagePact. - - This is a mutable borrow: The caller may mutate the Consumer through this - pointer. - - [Rust - `pactffi_message_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_get_consumer) - - # Safety - - This function is safe. - - # Error Handling - - This function will only fail if it is passed a NULL pointer. In the case of - error, a NULL pointer will be returned. - """ - raise NotImplementedError - - -def message_pact_get_provider(message_pact: MessagePact) -> Provider: - """ - Get a pointer to the Provider struct inside the MessagePact. - - This is a mutable borrow: The caller may mutate the Provider through this - pointer. - - [Rust - `pactffi_message_pact_get_provider`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_get_provider) - - # Safety - - This function is safe. - - # Error Handling - - This function will only fail if it is passed a NULL pointer. In the case of - error, a NULL pointer will be returned. - """ - raise NotImplementedError - - -def message_pact_get_message_iter( - message_pact: MessagePact, -) -> MessagePactMessageIterator: - r""" - Get an iterator over the messages of a message pact. - - [Rust - `pactffi_message_pact_get_message_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_get_message_iter) - - # Safety - - This iterator carries a pointer to the message pact, and must not outlive - the message pact. - - The message pact messages also must not be modified during iteration. If - they are, the old iterator must be deleted and a new iterator created. - - # Error Handling - - On failure, this function will return a NULL pointer. - - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. - """ - raise NotImplementedError - - -def message_pact_message_iter_next(iter: MessagePactMessageIterator) -> Message: - """ - Get the next message from the message pact. - - [Rust - `pactffi_message_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_message_iter_next) - - # Safety - - This function is safe. - - # Error Handling - - This function will return a NULL pointer if passed a NULL pointer or if an - error occurs. - """ - raise NotImplementedError - - -def message_pact_message_iter_delete(iter: MessagePactMessageIterator) -> None: - """ - Delete the iterator. - - [Rust - `pactffi_message_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_message_iter_delete) - """ - raise NotImplementedError - - -def message_pact_find_metadata(message_pact: MessagePact, key1: str, key2: str) -> str: - r""" - Get a copy of the metadata value indexed by `key1` and `key2`. - - [Rust - `pactffi_message_pact_find_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_find_metadata) - - # Safety - - Since it is a copy, the returned string may safely outlive the `Message`. - - The returned string must be deleted with `pactffi_string_delete`. - - The returned pointer will be NULL if the metadata does not contain the given - key, or if an error occurred. - - # Error Handling - - On failure, this function will return a NULL pointer. - - This function may fail if the provided `key1` or `key2` strings contains - invalid UTF-8, or if the Rust string contains embedded null ('\0') bytes. - """ - raise NotImplementedError - - -def message_pact_get_metadata_iter( - message_pact: MessagePact, -) -> MessagePactMetadataIterator: - r""" - Get an iterator over the metadata of a message pact. - - [Rust - `pactffi_message_pact_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_get_metadata_iter) - - # Safety - - This iterator carries a pointer to the message pact, and must not outlive - the message pact. - - The message pact metadata also must not be modified during iteration. If it - is, the old iterator must be deleted and a new iterator created. - - # Error Handling - - On failure, this function will return a NULL pointer. - - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. - """ - raise NotImplementedError - - -def message_pact_metadata_iter_next( - iter: MessagePactMetadataIterator, -) -> MessagePactMetadataTriple: - """ - Get the next triple out of the iterator, if possible. - - [Rust - `pactffi_message_pact_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_metadata_iter_next) - - # Safety - - This operation is invalid if the underlying data has been changed during - iteration. - - # Error Handling - - Returns null if no next element is present. - """ - raise NotImplementedError - - -def message_pact_metadata_iter_delete(iter: MessagePactMetadataIterator) -> None: - """ - Free the metadata iterator when you're done using it. - - [Rust `pactffi_message_pact_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_metadata_iter_delete) - """ - raise NotImplementedError - - -def message_pact_metadata_triple_delete(triple: MessagePactMetadataTriple) -> None: - """ - Free a triple returned from `pactffi_message_pact_metadata_iter_next`. - - [Rust `pactffi_message_pact_metadata_triple_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_metadata_triple_delete) - """ - raise NotImplementedError - - def provider_get_name(provider: Provider) -> str: r""" Get a copy of this provider's name. @@ -4156,6 +4015,36 @@ def sync_message_get_request_contents(message: SynchronousMessage) -> MessageCon raise NotImplementedError +def sync_message_generate_request_contents( + message: SynchronousMessage, +) -> MessageContents: + """ + Get the request contents of an `SynchronousMessage` as a `MessageContents`. + + This function differs from `pactffi_sync_message_get_request_contents` in + that it will process the message contents for any generators or matchers + that are present in the message in order to generate the actual message + contents as would be received by the consumer. + + [Rust + `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_generate_request_contents) + + # Safety + + The data pointed to by the pointer this function returns will be deleted + when the message is deleted. Trying to use if after the message is deleted + will result in undefined behaviour. + + # Error Handling + + If the message is NULL, returns NULL. + """ + return MessageContents( + lib.pactffi_sync_message_generate_request_contents(message._ptr), + owned=False, + ) + + def sync_message_get_number_responses(message: SynchronousMessage) -> int: """ Get the number of response messages in the `SynchronousMessage`. @@ -4346,6 +4235,38 @@ def sync_message_get_response_contents( raise NotImplementedError +def sync_message_generate_response_contents( + message: SynchronousMessage, + index: int, +) -> MessageContents: + """ + Get the response contents of an `SynchronousMessage` as a `MessageContents`. + + This function differs from + `sync_message_get_response_contents` in that it will process + the message contents for any generators or matchers that are present in + the message in order to generate the actual message contents as would be + received by the consumer. + + [Rust + `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_generate_response_contents) + + # Safety + + The data pointed to by the pointer this function returns will be deleted + when the message is deleted. Trying to use if after the message is deleted + will result in undefined behaviour. + + # Error Handling + + If the message is NULL or the index is not valid, returns NULL. + """ + return MessageContents( + lib.pactffi_sync_message_generate_response_contents(message._ptr, index), + owned=False, + ) + + def sync_message_get_description(message: SynchronousMessage) -> str: r""" Get a copy of the description. @@ -5275,6 +5196,24 @@ def with_query_parameter_v2( ) ``` + For query parameters with no value, two distinct formats are provided: + + 1. Parameters with blank values, as specified by `?foo=&bar=`, require an + empty string: + + ```python + with_query_parameter_v2(handle, "foo", 0, "") + with_query_parameter_v2(handle, "bar", 0, "") + ``` + + 2. Parameters with no associated value, as specified by `?foo&bar`, require + a NULL pointer: + + ```python + with_query_parameter_v2(handle, "foo", 0, None) + with_query_parameter_v2(handle, "bar", 0, None) + ``` + Args: interaction: Handle to the Interaction. @@ -5384,6 +5323,74 @@ def with_pact_metadata( raise RuntimeError(msg) +def with_metadata( + interaction: InteractionHandle, + key: str, + value: str, + part: InteractionPart, +) -> None: + r""" + Adds metadata to the interaction. + + Metadata is only relevant for message interactions to provide additional + information about the message, such as the queue name, message type, tags, + timestamps, etc. + + * `key` - metadata key + * `value` - metadata value, supports JSON structures with matchers and + generators. Passing a `NULL` point will remove the metadata key instead. + * `part` - the part of the interaction to add the metadata to (only + relevant for synchronous message interactions). + + Returns `true` if the metadata was added successfully, `false` otherwise. + + To include matching rules for the value, include the matching rule JSON + format with the value as a single JSON document. I.e. + + ```python with_metadata( + handle, "TagData", json.dumps({ + "value": {"ID": "sjhdjkshsdjh", "weight": 100.5}, + "pact:matcher:type": "type", + }), + ) + ``` + + See + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) + + # Note + + For HTTP interactions, use [`with_header_v2`][pact.v3.ffi.with_header_v2] + instead. This function will not have any effect on HTTP interactions and + returns `false`. + + For synchronous message interactions, the `part` parameter is required to + specify whether the metadata should be added to the request or response + part. For responses which can have multiple messages, the metadata will be + set on all response messages. This also requires for responses to have been + defined in the interaction. + + The [`with_body`][pact.v3.ffi.with_body] will also contribute to the + metadata of the message (both sync and async) by setting the key + `contentType` with the content type of the message. + + # Safety + + The key and value parameters must be valid pointers to NULL terminated + strings, or `NULL` for the value parameter if the metadata key should be + removed. + """ + success: bool = lib.pactffi_with_metadata( + interaction._ref, + key.encode("utf-8"), + value.encode("utf-8"), + part.value, + ) + if not success: + msg = f"Failed to set metadata for {interaction} with {key}={value}" + raise RuntimeError(msg) + + def with_header_v2( interaction: InteractionHandle, part: InteractionPart, @@ -5766,6 +5773,44 @@ def with_matching_rules( raise RuntimeError(msg) +def with_generators( + interaction: InteractionHandle, + part: InteractionPart, + generators: str, +) -> None: + """ + Add generators to the interaction. + + [Rust + `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_generators) + + This function can be called multiple times, in which case the generators + will be combined (provide they don't clash). + + For synchronous messages which allow multiple responses, the generators will + be added to all the responses. + + Args: + interaction: + Handle to the Interaction. + + part: + Request or response part (if applicable). + + generators: + JSON string of the generators to add to the interaction. + + """ + success: bool = lib.pactffi_with_generators( + interaction._ref, + part.value, + generators.encode("utf-8"), + ) + if not success: + msg = f"Unable to set generators for {interaction}." + raise RuntimeError(msg) + + def with_multipart_file_v2( # noqa: PLR0913 interaction: InteractionHandle, part: InteractionPart, @@ -5989,9 +6034,9 @@ def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: return PactMessageIterator(lib.pactffi_pact_handle_get_message_iter(pact._ref)) -def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterator: +def pact_handle_get_async_message_iter(pact: PactHandle) -> PactAsyncMessageIterator: r""" - Get an iterator over all the synchronous messages of the Pact. + Get an iterator over all the asynchronous messages of the Pact. The returned iterator needs to be freed with `pactffi_pact_sync_message_iter_delete`. @@ -6010,20 +6055,20 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat This function may fail if any of the Rust strings contain embedded null ('\0') bytes. """ - return PactSyncMessageIterator( - lib.pactffi_pact_handle_get_sync_message_iter(pact._ref), + return PactAsyncMessageIterator( + lib.pactffi_pact_handle_get_async_message_iter(pact._ref), ) -def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: +def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterator: r""" - Get an iterator over all the synchronous HTTP request/response interactions. + Get an iterator over all the synchronous messages of the Pact. The returned iterator needs to be freed with - `pactffi_pact_sync_http_iter_delete`. + `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6036,250 +6081,33 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: This function may fail if any of the Rust strings contain embedded null ('\0') bytes. """ - return PactSyncHttpIterator(lib.pactffi_pact_handle_get_sync_http_iter(pact._ref)) - - -def new_message_pact(consumer_name: str, provider_name: str) -> MessagePactHandle: - """ - Creates a new Pact Message model and returns a handle to it. - - [Rust - `pactffi_new_message_pact`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_message_pact) - - * `consumer_name` - The name of the consumer for the pact. - * `provider_name` - The name of the provider for the pact. - - Returns a new `MessagePactHandle`. The handle will need to be freed with the - `pactffi_free_message_pact_handle` function to release its resources. - """ - raise NotImplementedError - - -def new_message(pact: MessagePactHandle, description: str) -> MessageHandle: - """ - Creates a new Message and returns a handle to it. - - [Rust - `pactffi_new_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_message) - - * `description` - The message description. It needs to be unique for each - Message. - - Returns a new `MessageHandle`. - """ - raise NotImplementedError - - -def message_expects_to_receive(message: MessageHandle, description: str) -> None: - """ - Sets the description for the Message. - - [Rust - `pactffi_message_expects_to_receive`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_expects_to_receive) - - * `description` - The message description. It needs to be unique for each - message. - """ - raise NotImplementedError - - -def message_given(message: MessageHandle, description: str) -> None: - """ - Adds a provider state to the Interaction. - - [Rust - `pactffi_message_given`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_given) - - * `description` - The provider state description. It needs to be unique for - each message - """ - raise NotImplementedError - - -def message_given_with_param( - message: MessageHandle, - description: str, - name: str, - value: str, -) -> None: - """ - Adds a provider state to the Message with a parameter key and value. - - [Rust - `pactffi_message_given_with_param`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_given_with_param) - - * `description` - The provider state description. It needs to be unique. - * `name` - Parameter name. - * `value` - Parameter value. - """ - raise NotImplementedError - - -def message_with_contents( - message_handle: MessageHandle, - content_type: str, - body: List[int], - size: int, -) -> None: - """ - Adds the contents of the Message. - - [Rust - `pactffi_message_with_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_with_contents) - - Accepts JSON, binary and other payload types. Binary data will be base64 - encoded when serialised. - - Note: For text bodies (plain text, JSON or XML), you can pass in a C string - (NULL terminated) and the size of the body is not required (it will be - ignored). For binary bodies, you need to specify the number of bytes in the - body. - - * `content_type` - The content type of the body. Defaults to `text/plain`, - supports JSON structures with matchers and binary data. - * `body` - The body contents as bytes. For text payloads (JSON, XML, etc.), - a C string can be used and matching rules can be embedded in the body. - * `content_type` - Expected content type (e.g. application/json, - application/octet-stream) - * `size` - number of bytes in the message body to read. This is not required - for text bodies (JSON, XML, etc.). - """ - raise NotImplementedError - - -def message_with_metadata(message_handle: MessageHandle, key: str, value: str) -> None: - """ - Adds expected metadata to the Message. - - [Rust - `pactffi_message_with_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_with_metadata) - - * `key` - metadata key - * `value` - metadata value. - """ - raise NotImplementedError - - -def message_with_metadata_v2( - message_handle: MessageHandle, - key: str, - value: str, -) -> None: - """ - Adds expected metadata to the Message. - - [Rust - `pactffi_message_with_metadata_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_with_metadata_v2) - - Args: - message_handle: - Handle to the Message. - - key: - Metadata key. - - value: - Metadata value. - - This may be a simple string in which case it will be used as-is, or - it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). - - To include matching rules for the metadata, include the matching rule JSON - format with the value as a single JSON document. I.e. - - ```python - message_with_metadata_v2( - handle, - "contentType", - json.dumps({ - "pact:matcher:type": "regex", - "regex": "text/.*", - }), + return PactSyncMessageIterator( + lib.pactffi_pact_handle_get_sync_message_iter(pact._ref), ) - ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). - """ - raise NotImplementedError +def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: + r""" + Get an iterator over all the synchronous HTTP request/response interactions. -def message_reify(message_handle: MessageHandle) -> OwnedString: - """ - Reifies the given message. + The returned iterator needs to be freed with + `pactffi_pact_sync_http_iter_delete`. [Rust - `pactffi_message_reify`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_reify) - - Reification is the process of stripping away any matchers, and returning the - original contents. + `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) # Safety - The returned string needs to be deallocated with the `free_string` function. - This function must only ever be called from a foreign language. Calling it - from a Rust function that has a Tokio runtime in its call stack can result - in a deadlock. - """ - raise NotImplementedError - - -def write_message_pact_file( - pact: MessagePactHandle, - directory: str, - *, - overwrite: bool, -) -> int: - """ - External interface to write out the message pact file. - - This function should be called if all the consumer tests have passed. The - directory to write the file to is passed as the second parameter. If a NULL - pointer is passed, the current working directory is used. - - [Rust - `pactffi_write_message_pact_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_write_message_pact_file) - - If overwrite is true, the file will be overwritten with the contents of the - current pact. Otherwise, it will be merged with any existing pact file. - - Returns 0 if the pact file was successfully written. Returns a positive code - if the file can not be written, or there is no mock server running on that - port or the function panics. - - # Errors - - Errors are returned as positive values. - - | Error | Description | - |-------|-------------| - | 1 | The pact file was not able to be written | - | 2 | The message pact for the given handle was not found | - """ - raise NotImplementedError - - -def with_message_pact_metadata( - pact: MessagePactHandle, - namespace_: str, - name: str, - value: str, -) -> None: - """ - Sets the additional metadata on the Pact file. + The iterator contains a copy of the Pact, so it is always safe to use. - Common uses are to add the client library details such as the name and - version + # Error Handling - [Rust - `pactffi_with_message_pact_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_message_pact_metadata) + On failure, this function will return a NULL pointer. - * `pact` - Handle to a Pact model - * `namespace` - the top level metadat key to set any key values on - * `name` - the key to set - * `value` - the value to set + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. """ - raise NotImplementedError + return PactSyncHttpIterator(lib.pactffi_pact_handle_get_sync_http_iter(pact._ref)) def pact_handle_write_file( @@ -6347,24 +6175,6 @@ def free_pact_handle(pact: PactHandle) -> None: raise RuntimeError(msg) -def free_message_pact_handle(pact: MessagePactHandle) -> int: - """ - Delete a Pact handle and free the resources used by it. - - [Rust - `pactffi_free_message_pact_handle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_free_message_pact_handle) - - # Error Handling - - On failure, this function will return a positive integer value. - - * `1` - The handle is not valid or does not refer to a valid Pact. Could be - that it was previously deleted. - - """ - raise NotImplementedError - - def verify(args: str) -> int: """ External interface to verifier a provider. @@ -6912,9 +6722,11 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 password.encode("utf-8") if password else ffi.NULL, token.encode("utf-8") if token else ffi.NULL, enable_pending, - include_wip_pacts_since.isoformat().encode("utf-8") - if include_wip_pacts_since - else ffi.NULL, + ( + include_wip_pacts_since.isoformat().encode("utf-8") + if include_wip_pacts_since + else ffi.NULL + ), [ffi.new("char[]", t.encode("utf-8")) for t in provider_tags], len(provider_tags), provider_branch.encode("utf-8") if provider_branch else ffi.NULL, diff --git a/src/pact/v3/interaction/_base.py b/src/pact/v3/interaction/_base.py index 1b375d29a..d840c4a58 100644 --- a/src/pact/v3/interaction/_base.py +++ b/src/pact/v3/interaction/_base.py @@ -295,6 +295,73 @@ def with_binary_body( ) return self + def with_metadata( + self, + __metadata: dict[str, str] | None = None, + __part: Literal["Request", "Response"] | None = None, + /, + **kwargs: str, + ) -> Self: + """ + Set metadata for the interaction. + + This function may either be called with a single dictionary of metadata, + or with keyword arguments that are the key-value pairs of the metadata + (or a combination therefore): + + ```python + interaction.with_metadata({"key": "value", "key two": "value two"}) + interaction.with_metadata(foo="bar", baz="qux") + ``` + + The value of `None` will remove the metadata key from the interaction. + This is distinct from using an empty string or a string containing the + JSON `null` value, which will set the metadata key to an empty string or + the JSON `null` value, respectively. + + !!! note + + There are two special keys which cannot be used as keyword + arguments: `__metadata` and `__part`. Should there ever be a need + to set metadata with one of these keys, they must be passed through + as a dictionary: + + ```python + interaction.with_metadata({"__metadata": "value", "__part": 1}) + ``` + + Args: + ___metadata: + Dictionary of metadata keys and associated values. + + __part: + Whether the metadata should be added to the request or the + response. If `None`, then the function intelligently determines + whether the body should be added to the request or the response. + + **kwargs: + Additional metadata key-value pairs. + + Returns: + The current instance of the interaction. + """ + part = self._parse_interaction_part(__part) + for k, v in (__metadata or {}).items(): + pact.v3.ffi.with_metadata( + self._handle, + k, + v, + part, + ) + for k, v in kwargs.items(): + pact.v3.ffi.with_metadata( + self._handle, + k, + v, + part, + ) + return self + def with_multipart_file( # noqa: PLR0913 self, part_name: str, @@ -471,3 +538,36 @@ def with_matching_rules( rules, ) return self + + def with_generators( + self, + generators: dict[str, Any] | str, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Add generators to the interaction. + + Generators are used to adjust how parts of the request or response are + generated when the Pact is being tested. This can be useful for fields + that vary each time the request is made, such as a timestamp. + + Args: + generators: + Generators to add to the interaction. This must be encodable using + [`json.dumps(...)`][json.dumps], or a string. + + part: + Whether the generators should be added to the request or the + response. If `None`, then the function intelligently determines + whether the generators should be added to the request or the + response. + """ + if isinstance(generators, dict): + generators = json.dumps(generators) + + pact.v3.ffi.with_generators( + self._handle, + self._parse_interaction_part(part), + generators, + ) + return self From 49239eadc3e67f736487c4da125801eed47a9510 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 21 Jun 2024 14:43:10 +1000 Subject: [PATCH 0378/1376] feat(v3): add enum type aliases Define some type aliases for some enums. As a result of this, functions which take a subclass of an `Enum` can be, fairly straightforwardly, adapted to allow strings of the enum values. Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index df18845af..6294124b3 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -89,7 +89,7 @@ import typing import warnings from enum import Enum -from typing import TYPE_CHECKING, Any, List +from typing import TYPE_CHECKING, Any, List, Literal from pact.v3._ffi import ffi, lib # type: ignore[import] @@ -103,6 +103,43 @@ logger = logging.getLogger(__name__) +################################################################################ +# Type aliases +################################################################################ +# The following type aliases provide a nicer interface for end-users of the +# library, especially when it comes to [`Enum`][Enum] classes which offers +# support for string literals as alternative values. + +GeneratorCategoryOptions = Literal[ + "METHOD", "method", + "PATH", "path", + "HEADER", "header", + "QUERY", "query", + "BODY", "body", + "STATUS", "status", + "METADATA", "metadata", +] # fmt: skip +""" +Generator Category Options. + +Type alias for the string literals which represent the Generator Category +Options. +""" + +MatchingRuleCategoryOptions = Literal[ + "METHOD", "method", + "PATH", "path", + "HEADER", "header", + "QUERY", "query", + "BODY", "body", + "STATUS", "status", + "CONTENTS", "contents", + "METADATA", "metadata", +] # fmt: skip + +################################################################################ +# Classes +################################################################################ # The follow types are classes defined in the Rust code. Ultimately, a Python # alternative should be implemented, but for now, the follow lines only serve # to inform the type checker of the existence of these types. From e47f33be7acf6ba8935eae4266fdc2b23d0f54f9 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Sat, 22 Jun 2024 20:00:48 +1000 Subject: [PATCH 0379/1376] chore(ffi): disable private usage lint Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 6294124b3..9b96b0866 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -80,6 +80,9 @@ # ruff: noqa: SLF001 # private-member-access, as we need access to other handles' internal # references, without exposing them to the user. +# pyright: reportPrivateUsage=false +# Ignore private member access, as we frequently need to use the +# object's underlying pointer stored in `_ptr`. from __future__ import annotations From 1c0d6a8687ee9fce297edfcbabee2b6e000e45ab Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 21 Jun 2024 15:19:02 +1000 Subject: [PATCH 0380/1376] chore(ffi): implement AsynchronousMessage Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 119 ++++++++++++++++++++++++++++++++------------- 1 file changed, 86 insertions(+), 33 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 9b96b0866..5b3e65bca 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -93,6 +93,7 @@ import warnings from enum import Enum from typing import TYPE_CHECKING, Any, List, Literal +from typing import Generator as GeneratorType from pact.v3._ffi import ffi, lib # type: ignore[import] @@ -148,7 +149,72 @@ # to inform the type checker of the existence of these types. -class AsynchronousMessage: ... +class AsynchronousMessage: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: + """ + Initialise a new Asynchronous Message. + + Args: + ptr: + CFFI data structure. + + owned: + Whether the message is owned by something else or not. This + determines whether the message should be freed when the Python + object is destroyed. + """ + if ffi.typeof(ptr).cname != "struct AsynchronousMessage *": + msg = ( + "ptr must be a struct AsynchronousMessage, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + self._owned = owned + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "AsynchronousMessage" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"AsynchronousMessage({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the AsynchronousMessage. + """ + if not self._owned: + async_message_delete(self) + + @property + def description(self) -> str: + """ + Description of this message interaction. + + This needs to be unique in the pact file. + """ + return async_message_get_description(self) + + def provider_states(self) -> GeneratorType[ProviderState, None, None]: + """ + Optional provider state for the interaction. + """ + yield from async_message_get_provider_state_iter(self) + return # Ensures that the parent object outlives the generator + + @property + def contents(self) -> MessageContents | None: + """ + The contents of the message. + + This may be `None` if the message has no contents. + """ + return async_message_generate_contents(self) class Consumer: ... @@ -1494,27 +1560,19 @@ def async_message_delete(message: AsynchronousMessage) -> None: [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_delete) """ - raise NotImplementedError + lib.pactffi_async_message_delete(message._ptr) -def async_message_get_contents(message: AsynchronousMessage) -> MessageContents: +def async_message_get_contents(message: AsynchronousMessage) -> MessageContents | None: """ Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. [Rust `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents) - # Safety - - The data pointed to by the pointer this function returns will be deleted - when the message is deleted. Trying to use if after the message is deleted - will result in undefined behaviour. - - # Error Handling - - If the message is NULL, returns NULL. + If the message contents are missing, this function will return `None`. """ - raise NotImplementedError + return MessageContents(lib.pactffi_async_message_get_contents(message._ptr)) def async_message_generate_contents( @@ -1673,21 +1731,14 @@ def async_message_get_description(message: AsynchronousMessage) -> str: [Rust `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_description) - # Safety - - The returned string must be deleted with `pactffi_string_delete`. - - Since it is a copy, the returned string may safely outlive the - `AsynchronousMessage`. - - # Errors - - On failure, this function will return a NULL pointer. - - This function may fail if the Rust string contains embedded null ('\0') - bytes. + Raises: + RuntimeError: If the description cannot be retrieved. """ - raise NotImplementedError + ptr = lib.pactffi_async_message_get_description(message._ptr) + if ptr == ffi.NULL: + msg = "Unable to get the description from the message." + raise RuntimeError(msg) + return OwnedString(ptr) def async_message_set_description( @@ -1738,7 +1789,11 @@ def async_message_get_provider_state( This function may fail if the index requested is out of bounds, or if any of the Rust strings contain embedded null ('\0') bytes. """ - raise NotImplementedError + ptr = lib.pactffi_async_message_get_provider_state(message._ptr, index) + if ptr == ffi.NULL: + msg = "Unable to get the provider state from the message." + raise RuntimeError(msg) + return ProviderState(ptr) def async_message_get_provider_state_iter( @@ -1752,12 +1807,10 @@ def async_message_get_provider_state_iter( # Safety The underlying data must not change during iteration. - - # Error Handling - - Returns NULL if an error occurs. """ - raise NotImplementedError + return ProviderStateIterator( + lib.pactffi_async_message_get_provider_state_iter(message._ptr) + ) def consumer_get_name(consumer: Consumer) -> str: From 32d75b33d4e316bba63561fb09b750cd73f2d46a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 21 Jun 2024 15:42:42 +1000 Subject: [PATCH 0381/1376] chore(ffi): implement Generator The `Generator` (and other closely related types) are returned by the `MessageContents` type. These will generally not be used, but may prove to be useful in a few select circumstances. Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 196 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 186 insertions(+), 10 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 5b3e65bca..8a31f3a74 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -220,13 +220,174 @@ def contents(self) -> MessageContents | None: class Consumer: ... -class Generator: ... +class Generator: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a generator value. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct Generator *": + msg = "ptr must be a struct Generator, got" f" {ffi.typeof(ptr).cname}" + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "Generator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"Generator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Generator. + """ + + @property + def json(self) -> dict[str, Any]: + """ + Dictionary representation of the generator. + """ + return json.loads(generator_to_json(self)) + + def generate_string(self, context: dict[str, Any] | None = None) -> str: + """ + Generate a string from the generator. + + Args: + context: + JSON payload containing any generator context. For example: + + - The context for a `MockServerURL` generator should contain + details about the running mock server. + - The context for a `ProviderStateGenerator` should contain + the values returned from the provider state callback + function. + """ + return generator_generate_string(self, json.dumps(context or {})) + + def generate_integer(self, context: dict[str, Any] | None = None) -> int: + """ + Generate an integer from the generator. + + Args: + context: + JSON payload containing any generator context. For example: + + - The context for a `ProviderStateGenerator` should contain + the values returned from the provider state callback + function. + """ + return generator_generate_integer(self, json.dumps(context or {})) + + +class GeneratorCategoryIterator: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new generator category iterator. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct GeneratorCategoryIterator *": + msg = ( + "ptr must be a struct GeneratorCategoryIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "GeneratorCategoryIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"GeneratorCategoryIterator({self._ptr!r})" + def __del__(self) -> None: + """ + Destructor for the GeneratorCategoryIterator. + """ + generators_iter_delete(self) -class GeneratorCategoryIterator: ... + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + def __next__(self) -> GeneratorKeyValuePair: + """ + Get the next generator category from the iterator. + """ + return generators_iter_next(self) -class GeneratorKeyValuePair: ... + +class GeneratorKeyValuePair: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new key-value generator pair. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct GeneratorKeyValuePair *": + msg = ( + "ptr must be a struct GeneratorKeyValuePair, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "GeneratorKeyValuePair" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"GeneratorKeyValuePair({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the GeneratorKeyValuePair. + """ + generators_iter_pair_delete(self) + + @property + def path(self) -> str: + """ + Generator path. + """ + s = ffi.string(self._ptr.path) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s + + @property + def generator(self) -> Generator: + """ + Generator value. + """ + return Generator(self._ptr.generator) # type: ignore[attr-defined] class HttpRequest: ... @@ -2175,7 +2336,9 @@ def message_contents_get_generators_iter( On failure, this function will return a NULL pointer. """ - raise NotImplementedError + return GeneratorCategoryIterator( + lib.pactffi_message_contents_get_generators_iter(contents, category) + ) def request_contents_get_generators_iter( @@ -2577,7 +2740,7 @@ def generator_to_json(generator: Generator) -> str: This function will fail if it is passed a NULL pointer, or the owner of the generator has been deleted. """ - raise NotImplementedError + return OwnedString(lib.pactffi_generator_to_json(generator._ptr)) def generator_generate_string(generator: Generator, context_json: str) -> str: @@ -2595,7 +2758,14 @@ def generator_generate_string(generator: Generator, context_json: str) -> str: If anything goes wrong, it will return a NULL pointer. """ - raise NotImplementedError + ptr = lib.pactffi_generator_generate_string( + generator._ptr, + context_json.encode("utf-8"), + ) + s = ffi.string(ptr) + if isinstance(s, bytes): + s = s.decode("utf-8") + return s def generator_generate_integer(generator: Generator, context_json: str) -> int: @@ -2612,7 +2782,10 @@ def generator_generate_integer(generator: Generator, context_json: str) -> int: If anything goes wrong or the generator is not a type that can generate an integer value, it will return a zero value. """ - raise NotImplementedError + return lib.pactffi_generator_generate_integer( + generator._ptr, + context_json.encode("utf-8"), + ) def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: @@ -2622,7 +2795,7 @@ def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: [Rust `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generators_iter_delete) """ - raise NotImplementedError + lib.pactffi_generators_iter_delete(iter._ptr) def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePair: @@ -2644,7 +2817,10 @@ def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePa If no further data is present, returns NULL. """ - raise NotImplementedError + ptr = lib.pactffi_generators_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return GeneratorKeyValuePair(ptr) def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: @@ -2654,7 +2830,7 @@ def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: [Rust `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generators_iter_pair_delete) """ - raise NotImplementedError + lib.pactffi_generators_iter_pair_delete(pair._ptr) def sync_http_new() -> SynchronousHttp: From 455f8261497ba0ece7723c52fec51b5d718bc5dc Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 21 Jun 2024 15:57:43 +1000 Subject: [PATCH 0382/1376] chore(ffi): implement MatchingRule Along with associated types Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 140 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 133 insertions(+), 7 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 8a31f3a74..17ff11888 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -427,10 +427,86 @@ def __repr__(self) -> str: return f"InteractionHandle({self._ref!r})" -class MatchingRule: ... +class MatchingRule: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new key-value generator pair. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct MatchingRule *": + msg = "ptr must be a struct MatchingRule, got" f" {ffi.typeof(ptr).cname}" + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MatchingRule" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MatchingRule({self._ptr!r})" + + @property + def json(self) -> dict[str, Any]: + """ + Dictionary representation of the matching rule. + """ + return json.loads(matching_rule_to_json(self)) + + +class MatchingRuleCategoryIterator: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new key-value generator pair. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct MatchingRuleCategoryIterator *": + msg = ( + "ptr must be a struct MatchingRuleCategoryIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MatchingRuleCategoryIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MatchingRuleCategoryIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the MatchingRuleCategoryIterator. + """ + matching_rules_iter_delete(self) + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self -class MatchingRuleCategoryIterator: ... + def __next__(self) -> MatchingRuleKeyValuePair: + """ + Get the next generator category from the iterator. + """ + return matching_rules_iter_next(self) class MatchingRuleDefinitionResult: ... @@ -439,7 +515,57 @@ class MatchingRuleDefinitionResult: ... class MatchingRuleIterator: ... -class MatchingRuleKeyValuePair: ... +class MatchingRuleKeyValuePair: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new key-value generator pair. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct MatchingRuleKeyValuePair *": + msg = ( + "ptr must be a struct MatchingRuleKeyValuePair, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MatchingRuleKeyValuePair" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MatchingRuleKeyValuePair({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the MatchingRuleKeyValuePair. + """ + matching_rules_iter_pair_delete(self) + + @property + def path(self) -> str: + """ + Matching Rule path. + """ + s = ffi.string(self._ptr.path) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s + + @property + def matching_rule(self) -> MatchingRule: + """ + Matching Rule value. + """ + return MatchingRule(self._ptr.matching_rule) # type: ignore[attr-defined] class MatchingRuleResult: ... @@ -3474,7 +3600,7 @@ def matching_rule_to_json(rule: MatchingRule) -> str: This function will fail if it is passed a NULL pointer, or the iterator that owns the value of the matching rule has been deleted. """ - raise NotImplementedError + return OwnedString(lib.pactffi_matching_rule_to_json(rule._ptr)) def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: @@ -3484,7 +3610,7 @@ def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: [Rust `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rules_iter_delete) """ - raise NotImplementedError + lib.pactffi_matching_rules_iter_delete(iter._ptr) def matching_rules_iter_next( @@ -3508,7 +3634,7 @@ def matching_rules_iter_next( If no further data is present, returns NULL. """ - raise NotImplementedError + return MatchingRuleKeyValuePair(lib.pactffi_matching_rules_iter_next(iter._ptr)) def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: @@ -3518,7 +3644,7 @@ def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: [Rust `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) """ - raise NotImplementedError + lib.pactffi_matching_rules_iter_pair_delete(pair._ptr) def message_new() -> Message: From e0ed30e7d7ebce00c485d15505e607e613f1e132 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Sat, 22 Jun 2024 21:58:09 +1000 Subject: [PATCH 0383/1376] chore(ffi): remove old message and message handle The `InteractionHandle` is the intended way to define interactions, with `Message` and `MessageHandle` being replaced by the former. Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 476 --------------------------------------------- 1 file changed, 476 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 17ff11888..dbb0e14a9 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -571,15 +571,9 @@ def matching_rule(self) -> MatchingRule: class MatchingRuleResult: ... -class Message: ... - - class MessageContents: ... -class MessageHandle: ... - - class MessageMetadataIterator: ... @@ -789,57 +783,6 @@ def __next__(self) -> PactInteraction: return pact_interaction_iter_next(self) -class PactMessageIterator: - """ - Iterator over a Pact's asynchronous messages. - """ - - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new Pact Message Iterator. - - Args: - ptr: - CFFI data structure. - """ - if ffi.typeof(ptr).cname != "struct PactMessageIterator *": - msg = ( - f"ptr must be a struct PactMessageIterator, got {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "PactMessageIterator" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"PactMessageIterator({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the Pact Message Iterator. - """ - pact_message_iter_delete(self) - - def __iter__(self) -> Self: - """ - Return the iterator itself. - """ - return self - - def __next__(self) -> Message: - """ - Get the next message from the iterator. - """ - return pact_message_iter_next(self) - - class PactSyncHttpIterator: """ Iterator over a Pact's synchronous HTTP interactions. @@ -1437,18 +1380,6 @@ def log_message( ) -def match_message(msg_1: Message, msg_2: Message) -> Mismatches: - """ - Match a pair of messages, producing a collection of mismatches. - - If the messages match, the returned collection will be empty. - - [Rust - `pactffi_match_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_match_message) - """ - raise NotImplementedError - - def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: """ Get an iterator over mismatches. @@ -3398,28 +3329,6 @@ def pact_interaction_as_synchronous_http( raise NotImplementedError -def pact_interaction_as_message(interaction: PactInteraction) -> Message: - """ - Casts this interaction to a `Message` interaction. - - Returns a NULL pointer if the interaction can not be casted to a `Message` - interaction (for instance, it is a http interaction). The returned pointer - must be freed with `pactffi_message_delete` when no longer required. - - [Rust - `pactffi_pact_interaction_as_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_as_message) - - Note that if the interaction is a V4 `AsynchronousMessage`, it will be - converted to a V3 `Message` before being returned. - - # Safety This function is safe as long as the interaction pointer is a valid - pointer. - - # Errors On any error, this function will return a NULL pointer. - """ - raise NotImplementedError - - def pact_interaction_as_asynchronous_message( interaction: PactInteraction, ) -> AsynchronousMessage: @@ -3467,30 +3376,6 @@ def pact_interaction_as_synchronous_message( raise NotImplementedError -def pact_message_iter_delete(iter: PactMessageIterator) -> None: - """ - Free the iterator when you're done using it. - - [Rust - `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_message_iter_delete) - """ - lib.pactffi_pact_message_iter_delete(iter._ptr) - - -def pact_message_iter_next(iter: PactMessageIterator) -> Message: - """ - Get the next message from the message pact. - - [Rust - `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_message_iter_next) - """ - ptr = lib.pactffi_pact_message_iter_next(iter._ptr) - if ptr == ffi.NULL: - raise StopIteration - raise NotImplementedError - return Message(ptr) - - def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> AsynchronousMessage: """ Get the next asynchronous message from the iterator. @@ -3647,275 +3532,6 @@ def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: lib.pactffi_matching_rules_iter_pair_delete(pair._ptr) -def message_new() -> Message: - """ - Get a mutable pointer to a newly-created default message on the heap. - - [Rust - `pactffi_message_new`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_new) - - # Safety - - This function is safe. - - # Error Handling - - Returns NULL on error. - """ - raise NotImplementedError - - -def message_new_from_json( - index: int, - json_str: str, - spec_version: PactSpecification, -) -> Message: - """ - Constructs a `Message` from the JSON string. - - [Rust - `pactffi_message_new_from_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_new_from_json) - - # Safety - - This function is safe. - - # Error Handling - - If the JSON string is invalid or not UTF-8 encoded, returns a NULL. - """ - raise NotImplementedError - - -def message_new_from_body(body: str, content_type: str) -> Message: - """ - Constructs a `Message` from a body with a given content-type. - - [Rust - `pactffi_message_new_from_body`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_new_from_body) - - # Safety - - This function is safe. - - # Error Handling - - If the body or content type are invalid or not UTF-8 encoded, returns NULL. - """ - raise NotImplementedError - - -def message_delete(message: Message) -> None: - """ - Destroy the `Message` being pointed to. - - [Rust - `pactffi_message_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_delete) - """ - raise NotImplementedError - - -def message_get_contents(message: Message) -> OwnedString | None: - """ - Get the contents of a `Message` in string form. - - [Rust - `pactffi_message_get_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_contents) - - # Safety - - The returned string must be deleted with `pactffi_string_delete` and can - outlive the message. This function must only ever be called from a foreign - language. Calling it from a Rust function that has a Tokio runtime in its - call stack can result in a deadlock. - - The returned string can outlive the message. - - # Error Handling - - If the message is NULL, returns NULL. If the body of the message is missing, - then this function also returns NULL. This means there's no mechanism to - differentiate with this function call alone between a NULL message and a - missing message body. - """ - raise NotImplementedError - - -def message_set_contents(message: Message, contents: str, content_type: str) -> None: - """ - Sets the contents of the message. - - [Rust - `pactffi_message_set_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_set_contents) - - # Safety - - The message contents and content type must either be NULL pointers, or point - to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is - undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. - """ - raise NotImplementedError - - -def message_get_contents_length(message: Message) -> int: - """ - Get the length of the contents of a `Message`. - - [Rust - `pactffi_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_contents_length) - - # Safety - - This function is safe. - - # Error Handling - - If the message is NULL, returns 0. If the body of the message is missing, - then this function also returns 0. - """ - raise NotImplementedError - - -def message_get_contents_bin(message: Message) -> str: - """ - Get the contents of a `Message` as a pointer to an array of bytes. - - [Rust - `pactffi_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_contents_bin) - - # Safety - - The number of bytes in the buffer will be returned by - `pactffi_message_get_contents_length`. It is safe to use the pointer while - the message is not deleted or changed. Using the pointer after the message - is mutated or deleted may lead to undefined behaviour. - - # Error Handling - - If the message is NULL, returns NULL. If the body of the message is missing, - then this function also returns NULL. - """ - raise NotImplementedError - - -def message_set_contents_bin( - message: Message, - contents: str, - len: int, - content_type: str, -) -> None: - """ - Sets the contents of the message as an array of bytes. - - [Rust - `pactffi_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_set_contents_bin) - - # Safety - - The contents pointer must be valid for reads of `len` bytes, and it must be - properly aligned and consecutive. Otherwise behaviour is undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. - """ - raise NotImplementedError - - -def message_get_description(message: Message) -> OwnedString: - r""" - Get a copy of the description. - - [Rust - `pactffi_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_description) - - # Safety - - The returned string must be deleted with `pactffi_string_delete`. - - Since it is a copy, the returned string may safely outlive the `Message`. - - # Errors - - On failure, this function will return a NULL pointer. - - This function may fail if the Rust string contains embedded null ('\0') - bytes. - """ - raise NotImplementedError - - -def message_set_description(message: Message, description: str) -> int: - """ - Write the `description` field on the `Message`. - - [Rust - `pactffi_message_set_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_set_description) - - # Safety - - `description` must contain valid UTF-8. Invalid UTF-8 will be replaced with - U+FFFD REPLACEMENT CHARACTER. - - This function will only reallocate if the new string does not fit in the - existing buffer. - - # Error Handling - - Errors will be reported with a non-zero return value. - """ - raise NotImplementedError - - -def message_get_provider_state(message: Message, index: int) -> ProviderState: - r""" - Get a copy of the provider state at the given index from this message. - - [Rust - `pactffi_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_provider_state) - - # Safety - - The returned structure must be deleted with `provider_state_delete`. - - Since it is a copy, the returned structure may safely outlive the `Message`. - - # Error Handling - - On failure, this function will return a variant other than Success. - - This function may fail if the index requested is out of bounds, or if any of - the Rust strings contain embedded null ('\0') bytes. - """ - raise NotImplementedError - - -def message_get_provider_state_iter(message: Message) -> ProviderStateIterator: - """ - Get an iterator over provider states. - - [Rust - `pactffi_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_provider_state_iter) - - # Safety - - The underlying data must not change during iteration. - - # Error Handling - - Returns NULL if an error occurs. - """ - raise NotImplementedError - - def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: """ Get the next value from the iterator. @@ -3947,52 +3563,6 @@ def provider_state_iter_delete(iter: ProviderStateIterator) -> None: raise NotImplementedError -def message_find_metadata(message: Message, key: str) -> str: - r""" - Get a copy of the metadata value indexed by `key`. - - [Rust - `pactffi_message_find_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_find_metadata) - - # Safety - - The returned string must be deleted with `pactffi_string_delete`. - - Since it is a copy, the returned string may safely outlive the `Message`. - - The returned pointer will be NULL if the metadata does not contain the given - key, or if an error occurred. - - # Error Handling - - On failure, this function will return a NULL pointer. - - This function may fail if the provided `key` string contains invalid UTF-8, - or if the Rust string contains embedded null ('\0') bytes. - """ - raise NotImplementedError - - -def message_insert_metadata(message: Message, key: str, value: str) -> int: - r""" - Insert the (`key`, `value`) pair into this Message's `metadata` HashMap. - - [Rust - `pactffi_message_insert_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_insert_metadata) - - # Safety - - This function returns an enum indicating the result; see the comments on - HashMapInsertStatus for details. - - # Error Handling - - This function may fail if the provided `key` or `value` strings contain - invalid UTF-8. - """ - raise NotImplementedError - - def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadataPair: """ Get the next key and value out of the iterator, if possible. @@ -4016,31 +3586,6 @@ def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadata raise NotImplementedError -def message_get_metadata_iter(message: Message) -> MessageMetadataIterator: - r""" - Get an iterator over the metadata of a message. - - [Rust - `pactffi_message_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_metadata_iter) - - # Safety - - This iterator carries a pointer to the message, and must not outlive the - message. - - The message metadata also must not be modified during iteration. If it is, - the old iterator must be deleted and a new iterator created. - - # Error Handling - - On failure, this function will return a NULL pointer. - - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. - """ - raise NotImplementedError - - def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: """ Free the metadata iterator when you're done using it. @@ -6408,27 +5953,6 @@ def add_text_comment(interaction: InteractionHandle, comment: str) -> None: raise RuntimeError(msg) -def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: - r""" - Get an iterator over all the messages of the Pact. - - [Rust - `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_message_iter) - - # Safety - - The iterator contains a copy of the Pact, so it is always safe to use. - - # Error Handling - - On failure, this function will return a NULL pointer. - - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. - """ - return PactMessageIterator(lib.pactffi_pact_handle_get_message_iter(pact._ref)) - - def pact_handle_get_async_message_iter(pact: PactHandle) -> PactAsyncMessageIterator: r""" Get an iterator over all the asynchronous messages of the Pact. From b0b93f2838bcad98b8d98b69603442358fad297a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 09:40:57 +1000 Subject: [PATCH 0384/1376] chore(ffi): implement MessageContents Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 186 +++++++++++++++++++++++++++++++-------------- 1 file changed, 131 insertions(+), 55 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index dbb0e14a9..4dc0024ea 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -571,7 +571,87 @@ def matching_rule(self) -> MatchingRule: class MatchingRuleResult: ... -class MessageContents: ... +class MessageContents: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = True) -> None: + """ + Initialise a Message Contents. + + Args: + ptr: + CFFI data structure. + + owned: + Whether the message is owned by something else or not. This + determines whether the message should be freed when the Python + object is destroyed. + """ + if ffi.typeof(ptr).cname != "struct MessageContents *": + msg = ( + "ptr must be a struct MessageContents, got" f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + self._owned = owned + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MessageContents" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MessageContents({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the MessageContents. + """ + if not self._owned: + message_contents_delete(self) + + @property + def contents(self) -> str | bytes | None: + """ + Get the contents of the message. + """ + return message_contents_get_contents_str( + self + ) or message_contents_get_contents_bin(self) + + @property + def metadata(self) -> GeneratorType[MessageMetadataPair, None, None]: + """ + Get the metadata for the message contents. + """ + yield from message_contents_get_metadata_iter(self) + return # Ensures that the parent object outlives the generator + + def matching_rules( + self, + category: MatchingRuleCategoryOptions | MatchingRuleCategory, + ) -> GeneratorType[MatchingRuleKeyValuePair, None, None]: + """ + Get the matching rules for the message contents. + """ + if isinstance(category, str): + category = MatchingRuleCategory(category.upper()) + yield from message_contents_get_matching_rule_iter(self, category) + return # Ensures that the parent object outlives the generator + + def generators( + self, + category: GeneratorCategoryOptions | GeneratorCategory, + ) -> GeneratorType[GeneratorKeyValuePair, None, None]: + """ + Get the generators for the message contents. + """ + if isinstance(category, str): + category = GeneratorCategory(category.upper()) + yield from message_contents_get_generators_iter(self, category) + return # Ensures that the parent object outlives the generator class MessageMetadataIterator: ... @@ -2100,26 +2180,36 @@ def pact_consumer_delete(consumer: Consumer) -> None: raise NotImplementedError -def message_contents_get_contents_str(contents: MessageContents) -> str: +def message_contents_delete(contents: MessageContents) -> None: """ - Get the message contents in string form. + Delete the message contents instance. - [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_str) + This should only be called on a message contents that require deletion. + The function creating the message contents should document whether it + requires deletion. - # Safety + Deleting a message content which is associated with an interaction + will result in undefined behaviour. - The returned string must be deleted with `pactffi_string_delete`. + [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_delete) + """ + lib.pactffi_message_contents_delete(contents._ptr) - The returned string can outlive the message. - # Error Handling +def message_contents_get_contents_str(contents: MessageContents) -> str | None: + """ + Get the message contents in string form. - If the message contents is NULL, returns NULL. If the body of the message - is missing, then this function also returns NULL. This means there's - no mechanism to differentiate with this function call alone between - a NULL message and a missing message body. + [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_str) + + If the message has no contents or contain invalid UTF-8 characters, this + function will return `None`. + # Safety """ - raise NotImplementedError + ptr = lib.pactffi_message_contents_get_contents_str(contents._ptr) + if ptr == ffi.NULL: + return None + return OwnedString(ptr) def message_contents_set_contents_str( @@ -2160,38 +2250,27 @@ def message_contents_get_contents_length(contents: MessageContents) -> int: [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_length) - # Safety - - This function is safe. - - # Error Handling - - If the message is NULL, returns 0. If the body of the message - is missing, then this function also returns 0. + If the message has not contents, this function will return 0. """ - raise NotImplementedError + return lib.pactffi_message_contents_get_contents_length(contents._ptr) -def message_contents_get_contents_bin(contents: MessageContents) -> str: +def message_contents_get_contents_bin(contents: MessageContents) -> bytes | None: """ Get the contents of a message as a pointer to an array of bytes. [Rust `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_bin) - # Safety - - The number of bytes in the buffer will be returned by - `pactffi_message_contents_get_contents_length`. It is safe to use the - pointer while the message is not deleted or changed. Using the pointer after - the message is mutated or deleted may lead to undefined behaviour. - - # Error Handling - - If the message is NULL, returns NULL. If the body of the message is missing, - then this function also returns NULL. + If the message has no contents, this function will return `None`. """ - raise NotImplementedError + ptr = lib.pactffi_message_contents_get_contents_bin(contents._ptr) + if ptr == ffi.NULL: + return None + return ffi.buffer( + ptr, + lib.pactffi_message_contents_get_contents_length(contents._ptr), + )[:] def message_contents_set_contents_bin( @@ -2235,9 +2314,6 @@ def message_contents_get_metadata_iter( [Rust `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) - The returned pointer must be deleted with - `pactffi_message_metadata_iter_delete` when done with it. - # Safety This iterator carries a pointer to the message contents, and must not @@ -2246,14 +2322,14 @@ def message_contents_get_metadata_iter( The message metadata also must not be modified during iteration. If it is, the old iterator must be deleted and a new iterator created. - # Error Handling - - On failure, this function will return a NULL pointer. - - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. + Raises: + RuntimeError: If the metadata iterator cannot be retrieved. """ - raise NotImplementedError + ptr = lib.pactffi_message_contents_get_metadata_iter(contents._ptr) + if ptr == ffi.NULL: + msg = "Unable to get the metadata iterator from the message contents." + raise RuntimeError(msg) + return MessageMetadataIterator(ptr) def message_contents_get_matching_rule_iter( @@ -2294,7 +2370,9 @@ def message_contents_get_matching_rule_iter( On failure, this function will return a NULL pointer. """ - raise NotImplementedError + return MatchingRuleCategoryIterator( + lib.pactffi_message_contents_get_matching_rule_iter(contents._ptr, category) + ) def request_contents_get_matching_rule_iter( @@ -2381,21 +2459,19 @@ def message_contents_get_generators_iter( [Rust `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_generators_iter) - The returned pointer must be deleted with `pactffi_generators_iter_delete` - when done with it. - # Safety The iterator contains a copy of the data, so is safe to use when the message or message contents has been deleted. - # Error Handling - - On failure, this function will return a NULL pointer. + Raises: + RuntimeError: If the generators iterator cannot be retrieved. """ - return GeneratorCategoryIterator( - lib.pactffi_message_contents_get_generators_iter(contents, category) - ) + ptr = lib.pactffi_message_contents_get_generators_iter(contents._ptr, category) + if ptr == ffi.NULL: + msg = "Unable to get the generators iterator from the message contents." + raise RuntimeError(msg) + return GeneratorCategoryIterator(ptr) def request_contents_get_generators_iter( From 746fcf3f3e0c354df97b2d3db06a9659aca89711 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 09:45:32 +1000 Subject: [PATCH 0385/1376] chore(ffi): implement MessageMetadataPair and Iterator Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 124 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 8 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 4dc0024ea..4f2861387 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -654,10 +654,116 @@ def generators( return # Ensures that the parent object outlives the generator -class MessageMetadataIterator: ... +class MessageMetadataIterator: + """ + Iterator over an interaction's metadata. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Message Metadata Iterator. + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct MessageMetadataIterator *": + msg = ( + "ptr must be a struct MessageMetadataIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr -class MessageMetadataPair: ... + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MessageMetadataIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MessageMetadataIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Interaction Iterator. + """ + message_metadata_iter_delete(self) + + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> MessageMetadataPair: + """ + Get the next interaction from the iterator. + """ + return message_metadata_iter_next(self) + + +class MessageMetadataPair: + """ + A metadata key-value pair. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Message Metadata Pair. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct MessageMetadataPair *": + msg = ( + "ptr must be a struct MessageMetadataPair, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MessageMetadataPair" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MessageMetadataPair({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Interaction Iterator. + """ + message_metadata_pair_delete(self) + + @property + def key(self) -> str: + """ + Metadata key. + """ + s = ffi.string(self._ptr.key) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s + + @property + def value(self) -> str: + """ + Metadata value. + """ + s = ffi.string(self._ptr.value) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s class Mismatch: ... @@ -3655,11 +3761,13 @@ def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadata only ever be called from a foreign language. Calling it from a Rust function that has a Tokio runtime in its call stack can result in a deadlock. - # Error Handling - - If no further data is present, returns NULL. + Raises: + StopIteration: If no further data is present. """ - raise NotImplementedError + ptr = lib.pactffi_message_metadata_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return MessageMetadataPair(ptr) def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: @@ -3669,7 +3777,7 @@ def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: [Rust `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_metadata_iter_delete) """ - raise NotImplementedError + lib.pactffi_message_metadata_iter_delete(iter._ptr) def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: @@ -3679,7 +3787,7 @@ def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: [Rust `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_metadata_pair_delete) """ - raise NotImplementedError + lib.pactffi_message_metadata_pair_delete(pair._ptr) def provider_get_name(provider: Provider) -> str: From efa5942675557cf602c81eb81f1b0f94d4c6820b Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 09:52:55 +1000 Subject: [PATCH 0386/1376] chore(ffi): implement ProviderState and related Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 271 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 229 insertions(+), 42 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 4f2861387..ec6d2b75b 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -92,7 +92,7 @@ import typing import warnings from enum import Enum -from typing import TYPE_CHECKING, Any, List, Literal +from typing import TYPE_CHECKING, Any, List, Literal, Tuple from typing import Generator as GeneratorType from pact.v3._ffi import ffi, lib # type: ignore[import] @@ -1076,16 +1076,208 @@ def __next__(self) -> SynchronousMessage: class Provider: ... -class ProviderState: ... +class ProviderState: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new ProviderState. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct ProviderState *": + msg = "ptr must be a struct ProviderState, got" f" {ffi.typeof(ptr).cname}" + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "ProviderState({self.name!r})" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"ProviderState({self._ptr!r})" + + @property + def name(self) -> str: + """ + Provider State name. + """ + return provider_state_get_name(self) or "" + + def parameters(self) -> GeneratorType[Tuple[str, str], None, None]: + """ + Provider State parameters. + + This is a generator that yields key-value pairs. + """ + for p in provider_state_get_param_iter(self): + yield p.key, p.value + return # Ensures that the parent object outlives the generator + + +class ProviderStateIterator: + """ + Iterator over an interactions ProviderStates. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Provider State Iterator. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct ProviderStateIterator *": + msg = ( + "ptr must be a struct ProviderStateIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "ProviderStateIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"ProviderStateIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Provider State Iterator. + """ + provider_state_iter_delete(self) + + def __iter__(self) -> ProviderStateIterator: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> ProviderState: + """ + Get the next message from the iterator. + """ + return provider_state_iter_next(self) + + +class ProviderStateParamIterator: + """ + Iterator over a Provider States Parameters. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Provider State Param Iterator. + + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct ProviderStateParamIterator *": + msg = ( + "ptr must be a struct ProviderStateParamIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "ProviderStateParamIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"ProviderStateParamIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Provider State Param Iterator. + """ + provider_state_param_iter_delete(self) + def __iter__(self) -> ProviderStateParamIterator: + """ + Return the iterator itself. + """ + return self -class ProviderStateIterator: ... + def __next__(self) -> ProviderStateParamPair: + """ + Get the next message from the iterator. + """ + return provider_state_param_iter_next(self) -class ProviderStateParamIterator: ... +class ProviderStateParamPair: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new ProviderStateParamPair. + Args: + ptr: + CFFI data structure. + """ + if ffi.typeof(ptr).cname != "struct ProviderStateParamPair *": + msg = ( + "ptr must be a struct ProviderStateParamPair, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr -class ProviderStateParamPair: ... + def __str__(self) -> str: + """ + Nice string representation. + """ + return "ProviderStateParamPair" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"ProviderStateParamPair({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Provider State Param Pair. + """ + provider_state_param_pair_delete(self) + + @property + def key(self) -> str: + """ + Provider State Param key. + """ + s = ffi.string(self._ptr.key) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s + + @property + def value(self) -> str: + """ + Provider State Param value. + """ + s = ffi.string(self._ptr.value) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s class SynchronousHttp: ... @@ -3725,14 +3917,14 @@ def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: The underlying data must not change during iteration. - If a previous call panicked, then the internal mutex will have been poisoned - and this function will return NULL. - - # Error Handling - - Returns NULL if an error occurs. + Raises: + StopIteration: If no further data is present, or if an internal error + occurs. """ - raise NotImplementedError + provider_state = lib.pactffi_provider_state_iter_next(iter._ptr) + if provider_state == ffi.NULL: + raise StopIteration + return ProviderState(provider_state) def provider_state_iter_delete(iter: ProviderStateIterator) -> None: @@ -3742,7 +3934,7 @@ def provider_state_iter_delete(iter: ProviderStateIterator) -> None: [Rust `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_iter_delete) """ - raise NotImplementedError + lib.pactffi_provider_state_iter_delete(iter._ptr) def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadataPair: @@ -3861,24 +4053,22 @@ def pact_provider_delete(provider: Provider) -> None: raise NotImplementedError -def provider_state_get_name(provider_state: ProviderState) -> str: +def provider_state_get_name(provider_state: ProviderState) -> str | None: """ Get the name of the provider state as a string. - This needs to be deleted with `pactffi_string_delete`. - [Rust `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_get_name) - # Safety - - This function is safe. - - # Error Handling - - If the provider_state param is NULL, this returns NULL. + Raises: + RuntimeError: + If the name could not be retrieved. """ - raise NotImplementedError + ptr = lib.pactffi_provider_state_get_name(provider_state._ptr) + if ptr == ffi.NULL: + msg = "Failed to get provider state name." + raise RuntimeError(msg) + return OwnedString(ptr) def provider_state_get_param_iter( @@ -3898,14 +4088,14 @@ def provider_state_get_param_iter( The provider state params also must not be modified during iteration. If it is, the old iterator must be deleted and a new iterator created. - # Errors - - On failure, this function will return a NULL pointer. - - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. + Raises: + RuntimeError: If the iterator could not be created. """ - raise NotImplementedError + ptr = lib.pactffi_provider_state_get_param_iter(provider_state._ptr) + if ptr == ffi.NULL: + msg = "Failed to get provider state param iterator." + raise RuntimeError(msg) + return ProviderStateParamIterator(ptr) def provider_state_param_iter_next( @@ -3917,20 +4107,17 @@ def provider_state_param_iter_next( [Rust `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_param_iter_next) - Returns a pointer to a heap allocated array of 2 elements, the pointer to - the key string on the heap, and the pointer to the value string on the heap. - # Safety The underlying data must not be modified during iteration. - The user needs to free both the contained strings and the array. - - # Error Handling - - Returns NULL if there's no further elements or the iterator is NULL. + Raises: + StopIteration: If no further data is present. """ - raise NotImplementedError + provider_state_param = lib.pactffi_provider_state_param_iter_next(iter._ptr) + if provider_state_param == ffi.NULL: + raise StopIteration + return ProviderStateParamPair(provider_state_param) def provider_state_delete(provider_state: ProviderState) -> None: @@ -3950,7 +4137,7 @@ def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: [Rust `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_param_iter_delete) """ - raise NotImplementedError + lib.pactffi_provider_state_param_iter_delete(iter._ptr) def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: @@ -3960,7 +4147,7 @@ def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: [Rust `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_param_pair_delete) """ - raise NotImplementedError + lib.pactffi_provider_state_param_pair_delete(pair._ptr) def sync_message_new() -> SynchronousMessage: From da9b2413322466f4ea31f0cd99c5bb620a24138d Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 10:25:27 +1000 Subject: [PATCH 0387/1376] chore(ffi): implement SynchronousHttp Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 223 ++++++++++++++++++++++++++------------------- 1 file changed, 128 insertions(+), 95 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index ec6d2b75b..0d780f7b8 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -1280,7 +1280,80 @@ def value(self) -> str: return s -class SynchronousHttp: ... +class SynchronousHttp: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: + """ + Initialise a new Synchronous HTTP Interaction. + + Args: + ptr: + CFFI data structure. + + owned: + Whether the message is owned by something else or not. This + determines whether the message should be freed when the Python + object is destroyed. + """ + if ffi.typeof(ptr).cname != "struct SynchronousHttp *": + msg = ( + "ptr must be a struct SynchronousHttp, got" f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + self._owned = owned + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "SynchronousHttp" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"SynchronousHttp({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the SynchronousHttp. + """ + if not self._owned: + sync_http_delete(self) + + @property + def description(self) -> str: + """ + Description of this message interaction. + + This needs to be unique in the pact file. + """ + return sync_http_get_description(self) + + def provider_states(self) -> GeneratorType[ProviderState, None, None]: + """ + Optional provider state for the interaction. + """ + yield from sync_http_get_provider_state_iter(self) + return # Ensures that the parent object outlives the generator + + @property + def request_contents(self) -> str | bytes | None: + """ + The contents of the request. + """ + return sync_http_get_request_contents( + self + ) or sync_http_get_request_contents_bin(self) + + @property + def response_contents(self) -> str | bytes | None: + """ + The contents of the response. + """ + return sync_http_get_response_contents( + self + ) or sync_http_get_response_contents_bin(self) class SynchronousMessage: ... @@ -3288,7 +3361,7 @@ def sync_http_delete(interaction: SynchronousHttp) -> None: [Rust `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_delete) """ - raise NotImplementedError + lib.pactffi_sync_http_delete(interaction) def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: @@ -3311,27 +3384,20 @@ def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: raise NotImplementedError -def sync_http_get_request_contents(interaction: SynchronousHttp) -> str: +def sync_http_get_request_contents(interaction: SynchronousHttp) -> str | None: """ Get the request contents of a `SynchronousHttp` interaction in string form. [Rust `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request_contents) - # Safety - - The returned string must be deleted with `pactffi_string_delete`. - - The returned string can outlive the interaction. - - # Error Handling - - If the interaction is NULL, returns NULL. If the body of the request is - missing, then this function also returns NULL. This means there's no - mechanism to differentiate with this function call alone between a NULL body - and a missing body. + Note that this function will return `None` if either the body is missing or + is `null`. """ - raise NotImplementedError + ptr = lib.pactffi_sync_http_get_request_contents(interaction._ptr) + if ptr == ffi.NULL: + return None + return OwnedString(ptr) def sync_http_set_request_contents( @@ -3373,38 +3439,28 @@ def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: [Rust `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) - # Safety - - This function is safe. - - # Error Handling - - If the interaction is NULL, returns 0. If the body of the request is - missing, then this function also returns 0. + This function will return 0 if the body is missing. """ - raise NotImplementedError + return lib.pactffi_sync_http_get_request_contents_length(interaction._ptr) -def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes: +def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes | None: """ Get the request contents of a `SynchronousHttp` interaction as bytes. [Rust `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) - # Safety - - The number of bytes in the buffer will be returned by - `pactffi_sync_http_get_request_contents_length`. It is safe to use the - pointer while the interaction is not deleted or changed. Using the pointer - after the interaction is mutated or deleted may lead to undefined behaviour. - - # Error Handling - - If the interaction is NULL, returns NULL. If the body of the request is - missing, then this function also returns NULL. + Note that this function will return `None` if either the body is missing or + is `null`. """ - raise NotImplementedError + ptr = lib.pactffi_sync_http_get_request_contents_bin(interaction._ptr) + if ptr == ffi.NULL: + return None + return ffi.buffer( + ptr, + sync_http_get_request_contents_length(interaction), + )[:] def sync_http_set_request_contents_bin( @@ -3459,28 +3515,20 @@ def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: raise NotImplementedError -def sync_http_get_response_contents(interaction: SynchronousHttp) -> str: +def sync_http_get_response_contents(interaction: SynchronousHttp) -> str | None: """ Get the response contents of a `SynchronousHttp` interaction in string form. [Rust `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response_contents) - # Safety - - The returned string must be deleted with `pactffi_string_delete`. - - The returned string can outlive the interaction. - - # Error Handling - - If the interaction is NULL, returns NULL. - - If the body of the response is missing, then this function also returns - NULL. This means there's no mechanism to differentiate with this function - call alone between a NULL body and a missing body. + Note that this function will return `None` if either the body is missing or + is `null`. """ - raise NotImplementedError + ptr = lib.pactffi_sync_http_get_response_contents(interaction._ptr) + if ptr == ffi.NULL: + return None + return OwnedString(ptr) def sync_http_set_response_contents( @@ -3522,38 +3570,28 @@ def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: [Rust `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) - # Safety - - This function is safe. - - # Error Handling - - If the interaction is NULL or the index is not valid, returns 0. If the body - of the response is missing, then this function also returns 0. + This function will return 0 if the body is missing. """ - raise NotImplementedError + return lib.pactffi_sync_http_get_response_contents_length(interaction._ptr) -def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes: +def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes | None: """ Get the response contents of a `SynchronousHttp` interaction as bytes. [Rust `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) - # Safety - - The number of bytes in the buffer will be returned by - `pactffi_sync_http_get_response_contents_length`. It is safe to use the - pointer while the interaction is not deleted or changed. Using the pointer - after the interaction is mutated or deleted may lead to undefined behaviour. - - # Error Handling - - If the interaction is NULL, returns NULL. If the body of the response is - missing, then this function also returns NULL. + Note that this function will return `None` if either the body is missing or + is `null`. """ - raise NotImplementedError + ptr = lib.pactffi_sync_http_get_response_contents_bin(interaction._ptr) + if ptr == ffi.NULL: + return None + return ffi.buffer( + ptr, + sync_http_get_response_contents_length(interaction), + )[:] def sync_http_set_response_contents_bin( @@ -3595,21 +3633,14 @@ def sync_http_get_description(interaction: SynchronousHttp) -> str: [Rust `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_description) - # Safety - - The returned string must be deleted with `pactffi_string_delete`. - - Since it is a copy, the returned string may safely outlive the - `SynchronousHttp` interaction. - - # Errors - - On failure, this function will return a NULL pointer. - - This function may fail if the Rust string contains embedded null ('\0') - bytes. + Raises: + RuntimeError: If the description cannot be retrieved """ - raise NotImplementedError + ptr = lib.pactffi_sync_http_get_description(interaction._ptr) + if ptr == ffi.NULL: + msg = "Failed to get description" + raise RuntimeError(msg) + return OwnedString(ptr) def sync_http_set_description(interaction: SynchronousHttp, description: str) -> int: @@ -3674,11 +3705,14 @@ def sync_http_get_provider_state_iter( The underlying data must not change during iteration. - # Error Handling - - Returns NULL if an error occurs. + Raises: + RuntimeError: If the iterator cannot be retrieved """ - raise NotImplementedError + ptr = lib.pactffi_sync_http_get_provider_state_iter(interaction._ptr) + if ptr == ffi.NULL: + msg = "Failed to get provider state iterator" + raise RuntimeError(msg) + return ProviderStateIterator(ptr) def pact_interaction_as_synchronous_http( @@ -3807,8 +3841,7 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: ptr = lib.pactffi_pact_sync_http_iter_next(iter._ptr) if ptr == ffi.NULL: raise StopIteration - raise NotImplementedError - return SynchronousHttp(ptr) + return SynchronousHttp(ptr, owned=True) def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: From 86992e0aa3e443cc113ecbac8f53b00da39030dc Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 11:18:23 +1000 Subject: [PATCH 0388/1376] chore(ffi): implement SynchronousMessage Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 162 +++++++++++++++++++++++++++++---------------- 1 file changed, 105 insertions(+), 57 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 0d780f7b8..2d91adc67 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -1356,7 +1356,81 @@ def response_contents(self) -> str | bytes | None: ) or sync_http_get_response_contents_bin(self) -class SynchronousMessage: ... +class SynchronousMessage: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: + """ + Initialise a new Synchronous Message. + + Args: + ptr: + CFFI data structure. + + owned: + Whether the message is owned by something else or not. This + determines whether the message should be freed when the Python + object is destroyed. + """ + if ffi.typeof(ptr).cname != "struct SynchronousMessage *": + msg = ( + "ptr must be a struct SynchronousMessage, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + self._owned = owned + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "SynchronousMessage" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"SynchronousMessage({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the SynchronousMessage. + """ + if not self._owned: + sync_message_delete(self) + + @property + def description(self) -> str: + """ + Description of this message interaction. + + This needs to be unique in the pact file. + """ + return sync_message_get_description(self) + + def provider_states(self) -> GeneratorType[ProviderState, None, None]: + """ + Optional provider state for the interaction. + """ + yield from sync_message_get_provider_state_iter(self) + return # Ensures that the parent object outlives the generator + + @property + def request_contents(self) -> MessageContents: + """ + The contents of the message. + """ + return sync_message_generate_request_contents(self) + + @property + def response_contents(self) -> GeneratorType[MessageContents, None, None]: + """ + The contents of the responses. + """ + yield from ( + sync_message_generate_response_contents(self, i) + for i in range(sync_message_get_number_responses(self)) + ) + return # Ensures that the parent object outlives the generator class VerifierHandle: @@ -3817,8 +3891,7 @@ def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMes ptr = lib.pactffi_pact_sync_message_iter_next(iter._ptr) if ptr == ffi.NULL: raise StopIteration - raise NotImplementedError - return SynchronousMessage(ptr) + return SynchronousMessage(ptr, owned=True) def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: @@ -4208,7 +4281,7 @@ def sync_message_delete(message: SynchronousMessage) -> None: [Rust `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_delete) """ - raise NotImplementedError + lib.pactffi_sync_message_delete(message._ptr) def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: @@ -4372,21 +4445,12 @@ def sync_message_generate_request_contents( [Rust `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_generate_request_contents) - - # Safety - - The data pointed to by the pointer this function returns will be deleted - when the message is deleted. Trying to use if after the message is deleted - will result in undefined behaviour. - - # Error Handling - - If the message is NULL, returns NULL. """ - return MessageContents( - lib.pactffi_sync_message_generate_request_contents(message._ptr), - owned=False, - ) + ptr = lib.pactffi_sync_message_generate_request_contents(message._ptr) + if ptr == ffi.NULL: + msg = "Failed to generate request contents" + raise RuntimeError(msg) + return MessageContents(ptr, owned=False) def sync_message_get_number_responses(message: SynchronousMessage) -> int: @@ -4396,15 +4460,9 @@ def sync_message_get_number_responses(message: SynchronousMessage) -> int: [Rust `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_number_responses) - # Safety - - The message pointer must point to a valid SynchronousMessage. - - # Error Handling - - If the message is NULL, returns 0. + If the message is null, this function will return 0. """ - raise NotImplementedError + return lib.pactffi_sync_message_get_number_responses(message._ptr) def sync_message_get_response_contents_str( @@ -4595,20 +4653,14 @@ def sync_message_generate_response_contents( [Rust `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_generate_response_contents) - # Safety - - The data pointed to by the pointer this function returns will be deleted - when the message is deleted. Trying to use if after the message is deleted - will result in undefined behaviour. - - # Error Handling - - If the message is NULL or the index is not valid, returns NULL. + Raises: + RuntimeError: If the response contents could not be generated. """ - return MessageContents( - lib.pactffi_sync_message_generate_response_contents(message._ptr, index), - owned=False, - ) + ptr = lib.pactffi_sync_message_generate_response_contents(message._ptr, index) + if ptr == ffi.NULL: + msg = "Failed to generate response contents." + raise RuntimeError(msg) + return MessageContents(ptr, owned=False) def sync_message_get_description(message: SynchronousMessage) -> str: @@ -4618,21 +4670,14 @@ def sync_message_get_description(message: SynchronousMessage) -> str: [Rust `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_description) - # Safety - - The returned string must be deleted with `pactffi_string_delete`. - - Since it is a copy, the returned string may safely outlive the - `SynchronousMessage`. - - # Errors - - On failure, this function will return a NULL pointer. - - This function may fail if the Rust string contains embedded null ('\0') - bytes. + Raises: + RuntimeError: If the description could not be retrieved """ - raise NotImplementedError + ptr = lib.pactffi_sync_message_get_description(message._ptr) + if ptr == ffi.NULL: + msg = "Failed to get description." + raise RuntimeError(msg) + return OwnedString(ptr) def sync_message_set_description(message: SynchronousMessage, description: str) -> int: @@ -4697,11 +4742,14 @@ def sync_message_get_provider_state_iter( The underlying data must not change during iteration. - # Error Handling - - Returns NULL if an error occurs. + Raises: + RuntimeError: If the iterator could not be created. """ - raise NotImplementedError + ptr = lib.pactffi_sync_message_get_provider_state_iter(message._ptr) + if ptr == ffi.NULL: + msg = "Failed to get provider state iterator." + raise RuntimeError(msg) + return ProviderStateIterator(ptr) def string_delete(string: OwnedString) -> None: From c44a83b9ebbe5a19362deebce0a013dcba723a86 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 11:46:02 +1000 Subject: [PATCH 0389/1376] docs(ffi): properly document exceptions Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 379 +++++++++++++++++++++++++++++++++------------ 1 file changed, 279 insertions(+), 100 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 2d91adc67..d0eeaee96 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -162,6 +162,10 @@ def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: Whether the message is owned by something else or not. This determines whether the message should be freed when the Python object is destroyed. + + Raises: + TypeError: + If the `ptr` is not a `struct AsynchronousMessage`. """ if ffi.typeof(ptr).cname != "struct AsynchronousMessage *": msg = ( @@ -228,6 +232,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct Generator`. """ if ffi.typeof(ptr).cname != "struct Generator *": msg = "ptr must be a struct Generator, got" f" {ffi.typeof(ptr).cname}" @@ -297,6 +305,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct GeneratorCategoryIterator`. """ if ffi.typeof(ptr).cname != "struct GeneratorCategoryIterator *": msg = ( @@ -345,6 +357,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct GeneratorKeyValuePair`. """ if ffi.typeof(ptr).cname != "struct GeneratorKeyValuePair *": msg = ( @@ -435,6 +451,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MatchingRule`. """ if ffi.typeof(ptr).cname != "struct MatchingRule *": msg = "ptr must be a struct MatchingRule, got" f" {ffi.typeof(ptr).cname}" @@ -469,6 +489,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MatchingRuleCategoryIterator`. """ if ffi.typeof(ptr).cname != "struct MatchingRuleCategoryIterator *": msg = ( @@ -523,6 +547,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MatchingRuleKeyValuePair`. """ if ffi.typeof(ptr).cname != "struct MatchingRuleKeyValuePair *": msg = ( @@ -584,6 +612,10 @@ def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = True) -> None: Whether the message is owned by something else or not. This determines whether the message should be freed when the Python object is destroyed. + + Raises: + TypeError: + If the `ptr` is not a `struct MessageContents`. """ if ffi.typeof(ptr).cname != "struct MessageContents *": msg = ( @@ -666,6 +698,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MessageMetadataIterator`. """ if ffi.typeof(ptr).cname != "struct MessageMetadataIterator *": msg = ( @@ -718,6 +754,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MessageMetadataPair`. """ if ffi.typeof(ptr).cname != "struct MessageMetadataPair *": msg = ( @@ -790,6 +830,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct PactAsyncMessageIterator`. """ if ffi.typeof(ptr).cname != "struct PactAsyncMessageIterator *": msg = ( @@ -935,6 +979,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct PactInteractionIterator`. """ if ffi.typeof(ptr).cname != "struct PactInteractionIterator *": msg = ( @@ -981,6 +1029,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct PactSyncHttpIterator`. """ if ffi.typeof(ptr).cname != "struct PactSyncHttpIterator *": msg = ( @@ -1033,6 +1085,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct PactSyncMessageIterator`. """ if ffi.typeof(ptr).cname != "struct PactSyncMessageIterator *": msg = ( @@ -1084,6 +1140,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct ProviderState`. """ if ffi.typeof(ptr).cname != "struct ProviderState *": msg = "ptr must be a struct ProviderState, got" f" {ffi.typeof(ptr).cname}" @@ -1132,6 +1192,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct ProviderStateIterator`. """ if ffi.typeof(ptr).cname != "struct ProviderStateIterator *": msg = ( @@ -1184,6 +1248,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct ProviderStateParamIterator`. """ if ffi.typeof(ptr).cname != "struct ProviderStateParamIterator *": msg = ( @@ -1232,6 +1300,10 @@ def __init__(self, ptr: cffi.FFI.CData) -> None: Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct ProviderStateParamPair`. """ if ffi.typeof(ptr).cname != "struct ProviderStateParamPair *": msg = ( @@ -1293,6 +1365,10 @@ def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: Whether the message is owned by something else or not. This determines whether the message should be freed when the Python object is destroyed. + + Raises: + TypeError: + If the `ptr` is not a `struct SynchronousHttp`. """ if ffi.typeof(ptr).cname != "struct SynchronousHttp *": msg = ( @@ -1369,6 +1445,10 @@ def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: Whether the message is owned by something else or not. This determines whether the message should be freed when the Python object is destroyed. + + Raises: + TypeError: + If the `ptr` is not a `struct SynchronousMessage`. """ if ffi.typeof(ptr).cname != "struct SynchronousMessage *": msg = ( @@ -1674,6 +1754,10 @@ def __init__(self, cdata: cffi.FFI.CData) -> None: Args: cdata: CFFI data structure. + + Raises: + TypeError: + If the `cdata` is not a `struct StringResult`. """ if ffi.typeof(cdata).cname != "struct StringResult": msg = f"cdata must be a struct StringResult, got {ffi.typeof(cdata).cname}" @@ -1722,7 +1806,12 @@ def raise_exception(self) -> None: Raise an exception with the text of the result. Raises: - RuntimeError: If the result is an error. + RuntimeError: + If the result is an error. + + Raises: + RuntimeError: + If the result is an error. """ if self.is_failed: raise RuntimeError(self.text) @@ -2015,7 +2104,8 @@ def get_error_message(length: int = 1024) -> str | None: message. Raises: - RuntimeError: If the error message could not be retrieved. + RuntimeError: + If the error message could not be retrieved. """ buffer = ffi.new("char[]", length) ret: int = lib.pactffi_get_error_message(buffer, length) @@ -2066,7 +2156,8 @@ def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: insensitive). Raises: - RuntimeError: If there was an error setting the logger. + RuntimeError: + If there was an error setting the logger. """ if isinstance(level_filter, str): level_filter = LevelFilter[level_filter.upper()] @@ -2096,6 +2187,10 @@ def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: Convenience function to direct all logging to a task local memory buffer. [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_buffer) + + Raises: + RuntimeError: + If there was an error setting the logger. """ if isinstance(level_filter, str): level_filter = LevelFilter[level_filter.upper()] @@ -2475,7 +2570,8 @@ def async_message_get_description(message: AsynchronousMessage) -> str: `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_description) Raises: - RuntimeError: If the description cannot be retrieved. + RuntimeError: + If the description cannot be retrieved. """ ptr = lib.pactffi_async_message_get_description(message._ptr) if ptr == ffi.NULL: @@ -2518,19 +2614,9 @@ def async_message_get_provider_state( [Rust `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_provider_state) - # Safety - - The returned structure must be deleted with `provider_state_delete`. - - Since it is a copy, the returned structure may safely outlive the - `AsynchronousMessage`. - - # Error Handling - - On failure, this function will return a variant other than Success. - - This function may fail if the index requested is out of bounds, or if any of - the Rust strings contain embedded null ('\0') bytes. + Raises: + RuntimeError: + If the provider state cannot be retrieved. """ ptr = lib.pactffi_async_message_get_provider_state(message._ptr, index) if ptr == ffi.NULL: @@ -2649,7 +2735,6 @@ def message_contents_get_contents_str(contents: MessageContents) -> str | None: If the message has no contents or contain invalid UTF-8 characters, this function will return `None`. - # Safety """ ptr = lib.pactffi_message_contents_get_contents_str(contents._ptr) if ptr == ffi.NULL: @@ -2768,7 +2853,8 @@ def message_contents_get_metadata_iter( the old iterator must be deleted and a new iterator created. Raises: - RuntimeError: If the metadata iterator cannot be retrieved. + RuntimeError: + If the metadata iterator cannot be retrieved. """ ptr = lib.pactffi_message_contents_get_metadata_iter(contents._ptr) if ptr == ffi.NULL: @@ -2910,7 +2996,8 @@ def message_contents_get_generators_iter( or message contents has been deleted. Raises: - RuntimeError: If the generators iterator cannot be retrieved. + RuntimeError: + If the generators iterator cannot be retrieved. """ ptr = lib.pactffi_message_contents_get_generators_iter(contents._ptr, category) if ptr == ffi.NULL: @@ -3273,23 +3360,15 @@ def validate_datetime(value: str, format: str) -> None: """ Validates the date/time value against the date/time format string. - Raises an error if the value is not a valid date/time for the format string. - - If the value is valid, this function will return a zero status code - (EXIT_SUCCESS). If the value is not valid, will return a value of 1 - (EXIT_FAILURE) and set the error message which can be retrieved with - `pactffi_get_error_message`. - [Rust `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_validate_datetime) - # Errors If the function receives a panic, it will return 2 and the message - associated with the panic can be retrieved with `pactffi_get_error_message`. - - # Safety + Raises: + ValueError: + If the value is not a valid date/time for the format string. - This function is safe as long as the value and format parameters point to - valid NULL-terminated strings. + RuntimeError: + For any other error. """ ret = lib.pactffi_validate_datetime(value.encode(), format.encode()) if ret == 0: @@ -3386,14 +3465,9 @@ def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePa The returned pointer must be deleted with `pactffi_generator_iter_pair_delete`. - # Safety - - The underlying data is owned by the `GeneratorKeyValuePair`, so is always - safe to use. - - # Error Handling - - If no further data is present, returns NULL. + Raises: + StopIteration: + If the iterator has reached the end. """ ptr = lib.pactffi_generators_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -3708,7 +3782,8 @@ def sync_http_get_description(interaction: SynchronousHttp) -> str: `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_description) Raises: - RuntimeError: If the description cannot be retrieved + RuntimeError: + If the description cannot be retrieved """ ptr = lib.pactffi_sync_http_get_description(interaction._ptr) if ptr == ffi.NULL: @@ -3780,7 +3855,8 @@ def sync_http_get_provider_state_iter( The underlying data must not change during iteration. Raises: - RuntimeError: If the iterator cannot be retrieved + RuntimeError: + If the iterator cannot be retrieved """ ptr = lib.pactffi_sync_http_get_provider_state_iter(interaction._ptr) if ptr == ffi.NULL: @@ -3864,6 +3940,10 @@ def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> Asynchronous [Rust `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_async_message_iter_next) + + Raises: + StopIteration: + If the iterator has reached the end. """ ptr = lib.pactffi_pact_async_message_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -3887,6 +3967,10 @@ def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMes [Rust `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_message_iter_next) + + Raises: + StopIteration: + If the iterator has reached the end. """ ptr = lib.pactffi_pact_sync_message_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -3910,6 +3994,10 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: [Rust `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_http_iter_next) + + Raises: + StopIteration: + If the iterator has reached the end. """ ptr = lib.pactffi_pact_sync_http_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -3933,6 +4021,10 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction [Rust `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_iter_next) + + Raises: + StopIteration: + If the iterator has reached the end. """ ptr = lib.pactffi_pact_interaction_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -4024,8 +4116,8 @@ def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: The underlying data must not change during iteration. Raises: - StopIteration: If no further data is present, or if an internal error - occurs. + StopIteration: + If no further data is present, or if an internal error occurs. """ provider_state = lib.pactffi_provider_state_iter_next(iter._ptr) if provider_state == ffi.NULL: @@ -4060,7 +4152,8 @@ def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadata that has a Tokio runtime in its call stack can result in a deadlock. Raises: - StopIteration: If no further data is present. + StopIteration: + If no further data is present. """ ptr = lib.pactffi_message_metadata_iter_next(iter._ptr) if ptr == ffi.NULL: @@ -4195,7 +4288,8 @@ def provider_state_get_param_iter( is, the old iterator must be deleted and a new iterator created. Raises: - RuntimeError: If the iterator could not be created. + RuntimeError: + If the iterator could not be created. """ ptr = lib.pactffi_provider_state_get_param_iter(provider_state._ptr) if ptr == ffi.NULL: @@ -4218,7 +4312,8 @@ def provider_state_param_iter_next( The underlying data must not be modified during iteration. Raises: - StopIteration: If no further data is present. + StopIteration: + If no further data is present. """ provider_state_param = lib.pactffi_provider_state_param_iter_next(iter._ptr) if provider_state_param == ffi.NULL: @@ -4445,6 +4540,10 @@ def sync_message_generate_request_contents( [Rust `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_generate_request_contents) + + Raises: + RuntimeError: + If the request contents cannot be generated """ ptr = lib.pactffi_sync_message_generate_request_contents(message._ptr) if ptr == ffi.NULL: @@ -4654,7 +4753,8 @@ def sync_message_generate_response_contents( `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_generate_response_contents) Raises: - RuntimeError: If the response contents could not be generated. + RuntimeError: + If the response contents could not be generated. """ ptr = lib.pactffi_sync_message_generate_response_contents(message._ptr, index) if ptr == ffi.NULL: @@ -4671,7 +4771,8 @@ def sync_message_get_description(message: SynchronousMessage) -> str: `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_description) Raises: - RuntimeError: If the description could not be retrieved + RuntimeError: + If the description could not be retrieved """ ptr = lib.pactffi_sync_message_get_description(message._ptr) if ptr == ffi.NULL: @@ -4743,7 +4844,8 @@ def sync_message_get_provider_state_iter( The underlying data must not change during iteration. Raises: - RuntimeError: If the iterator could not be created. + RuntimeError: + If the iterator could not be created. """ ptr = lib.pactffi_sync_message_get_provider_state_iter(message._ptr) if ptr == ffi.NULL: @@ -4897,8 +4999,9 @@ def create_mock_server_for_transport( A handle to the mock server. Raises: - RuntimeError: If the mock server could not be created. The error message - will contain details of the error. + RuntimeError: + If the mock server could not be created. The error message will + contain details of the error. """ ret: int = lib.pactffi_create_mock_server_for_transport( pact._ref, @@ -4950,8 +5053,9 @@ def mock_server_mismatches( # Errors Raises: - RuntimeError: If there is no mock server with the provided port number, - or the function panics. + RuntimeError: + If there is no mock server with the provided port number, or the + function panics. """ ptr = lib.pactffi_mock_server_mismatches(mock_server_handle._ref) if ptr == ffi.NULL: @@ -4978,7 +5082,8 @@ def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: Handle to the mock server to cleanup. Raises: - RuntimeError: If the mock server could not be cleaned up. + RuntimeError: + If the mock server could not be cleaned up. """ success: bool = lib.pactffi_cleanup_mock_server(mock_server_handle._ref) if not success: @@ -5013,7 +5118,8 @@ def write_pact_file( pact file will be merged with any existing pact file. Raises: - RuntimeError: If there was an error writing the pact file. + RuntimeError: + If there was an error writing the pact file. """ ret: int = lib.pactffi_write_pact_file( mock_server_handle._ref, @@ -5053,7 +5159,8 @@ def mock_server_logs(mock_server_handle: PactServerHandle) -> str: `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mock_server_logs) Raises: - RuntimeError: If the logs for the mock server can not be retrieved. + RuntimeError: + If the logs for the mock server can not be retrieved. """ ptr = lib.pactffi_mock_server_logs(mock_server_handle._ref) if ptr == ffi.NULL: @@ -5283,7 +5390,11 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> None: The interaction description. It needs to be unique for each Pact. Raises: - RuntimeError: If the interaction description could not be set. + NotImplementedError: + This function has intentionally been left unimplemented. + + RuntimeError: + If the interaction description could not be set. """ # This function has intentionally been left unimplemented. The rationale is # to avoid code of the form: @@ -5318,7 +5429,8 @@ def given(interaction: InteractionHandle, description: str) -> None: The provider state description. It needs to be unique. Raises: - RuntimeError: If the provider state could not be specified. + RuntimeError: + If the provider state could not be specified. """ success: bool = lib.pactffi_given(interaction._ref, description.encode("utf-8")) if not success: @@ -5343,23 +5455,10 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> Non test_name: The test name to set. - # Safety - - The test name parameter must be a valid pointer to a NULL terminated string. - Raises: - RuntimeError: If the test name can not be set. - - # Error Handling - - If the test name can not be set, this will return a positive value. + RuntimeError: + If the test name can not be set. - * `1` - Function panicked. Error message will be available by calling - `pactffi_get_error_message`. - * `2` - Handle was not valid. - * `3` - Mock server was already started and the integration can not be - modified. - * `4` - Not a V4 interaction. """ ret: int = lib.pactffi_interaction_test_name( interaction._ref, @@ -5410,7 +5509,8 @@ def given_with_param( Parameter value as JSON. Raises: - RuntimeError: If the interaction state could not be updated. + RuntimeError: + If the interaction state could not be updated. """ success: bool = lib.pactffi_given_with_param( interaction._ref, @@ -5448,19 +5548,8 @@ def given_with_params( Parameter values as a JSON fragment. Raises: - RuntimeError: If the interaction state could not be updated. - - # Errors - - Returns EXIT_FAILURE (1) if the interaction or Pact can't be modified (i.e. - the mock server for it has already started). - - Returns 2 and sets the error message (which can be retrieved with - `pactffi_get_error_message`) if the parameter values con't be parsed as - JSON. - - Returns 3 if any of the C strings are not valid. - + RuntimeError: + If the interaction state could not be updated. """ ret: int = lib.pactffi_given_with_params( interaction._ref, @@ -5511,7 +5600,8 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None ``` Raises: - RuntimeError: If the request could not be specified. + RuntimeError: + If the request could not be specified. """ success: bool = lib.pactffi_with_request( interaction._ref, @@ -5625,7 +5715,8 @@ def with_query_parameter_v2( rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). Raises: - RuntimeError: If there was an error setting the query parameter. + RuntimeError: + If there was an error setting the query parameter. """ success: bool = lib.pactffi_with_query_parameter_v2( interaction._ref, @@ -5651,6 +5742,10 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None: version: The spec version to use. + + Raises: + RuntimeError: + If the Pact specification could not be set. """ success: bool = lib.pactffi_with_specification(pact._ref, version.value) if not success: @@ -5703,6 +5798,10 @@ def with_pact_metadata( value: The value to set + + Raises: + RuntimeError: + If the metadata could not be set. """ success: bool = lib.pactffi_with_pact_metadata( pact._ref, @@ -5771,6 +5870,10 @@ def with_metadata( The key and value parameters must be valid pointers to NULL terminated strings, or `NULL` for the value parameter if the metadata key should be removed. + + Raises: + RuntimeError: + If the metadata could not be set. """ success: bool = lib.pactffi_with_metadata( interaction._ref, @@ -5856,7 +5959,8 @@ def with_header_v2( rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). Raises: - RuntimeError: If there was an error setting the header. + RuntimeError: + If there was an error setting the header. """ success: bool = lib.pactffi_with_header_v2( interaction._ref, @@ -5903,7 +6007,8 @@ def set_header( The header value. This is handled as-is, with no processing. Raises: - RuntimeError: If the header could not be set. + RuntimeError: + If the header could not be set. """ success: bool = lib.pactffi_set_header( interaction._ref, @@ -5931,7 +6036,8 @@ def response_status(interaction: InteractionHandle, status: int) -> None: The response status. Defaults to 200. Raises: - RuntimeError: If the response status could not be set. + RuntimeError: + If the response status could not be set. """ success: bool = lib.pactffi_response_status(interaction._ref, status) if not success: @@ -5977,7 +6083,8 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). Raises: - RuntimeError: If the response status could not be set. + RuntimeError: + If the response status could not be set. """ success: bool = lib.pactffi_response_status_v2( interaction._ref, status.encode("utf-8") @@ -6021,7 +6128,8 @@ def with_body( in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). Raises: - RuntimeError: If the body could not be specified. + RuntimeError: + If the body could not be specified. """ success: bool = lib.pactffi_with_body( interaction._ref, @@ -6068,7 +6176,8 @@ def with_binary_body( The body contents. If `None`, the body will be set to null. Raises: - RuntimeError: If the body could not be modified. + RuntimeError: + If the body could not be modified. """ raise NotImplementedError @@ -6108,6 +6217,10 @@ def with_binary_file( body: The body contents. If `None`, the body will be set to null. + + Raises: + RuntimeError: + If the body could not be set. """ if len(gc.get_referrers(body)) == 0: warnings.warn( @@ -6153,7 +6266,8 @@ def with_matching_rules( JSON string of the matching rules to add to the interaction. Raises: - RuntimeError: If the rules could not be added. + RuntimeError: + If the rules could not be added. """ success: bool = lib.pactffi_with_matching_rules( interaction._ref, @@ -6192,6 +6306,9 @@ def with_generators( generators: JSON string of the generators to add to the interaction. + Raises: + RuntimeError: + If the generators could not be added. """ success: bool = lib.pactffi_with_generators( interaction._ref, @@ -6319,6 +6436,10 @@ def set_key(interaction: InteractionHandle, key: str | None) -> None: key: Key value. This must be a valid UTF-8 null-terminated string, or `None` to clear the key. + + Raises: + RuntimeError: + If the key could not be set. """ success: bool = lib.pactffi_set_key( interaction._ref, @@ -6342,6 +6463,10 @@ def set_pending(interaction: InteractionHandle, *, pending: bool) -> None: pending: Boolean value to toggle the pending state of the interaction. + + Raises: + RuntimeError: + If the pending status could not be updated. """ success: bool = lib.pactffi_set_pending(interaction._ref, pending) if not success: @@ -6370,7 +6495,8 @@ def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> null. Raises: - RuntimeError: If the comments could not be updated. + RuntimeError: + If the comments could not be updated. """ success: bool = lib.pactffi_set_comment( interaction._ref, @@ -6395,6 +6521,10 @@ def add_text_comment(interaction: InteractionHandle, comment: str) -> None: comment: Comment value. This is a regular string value. + + Raises: + RuntimeError: + If the comment could not be added. """ success: bool = lib.pactffi_add_text_comment( interaction._ref, @@ -6507,6 +6637,10 @@ def pact_handle_write_file( If `True`, the file will be overwritten with the contents of the current pact. Otherwise, it will be merged with any existing pact file. + + Raises: + RuntimeError: + If there was an error writing the pact file. """ ret: int = lib.pactffi_pact_handle_write_file( pact._ref, @@ -6534,7 +6668,8 @@ def free_pact_handle(pact: PactHandle) -> None: `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_free_pact_handle) Raises: - RuntimeError: If the handle could not be freed. + RuntimeError: + If the handle could not be freed. """ ret: int = lib.pactffi_free_pact_handle(pact._ref) if ret == 0: @@ -6778,6 +6913,10 @@ def verifier_set_verification_options( request_timeout: The timeout for the request in milliseconds. + + Raises: + RuntimeError: + If the options could not be set. """ retval: int = lib.pactffi_verifier_set_verification_options( handle._ref, @@ -6808,6 +6947,10 @@ def verifier_set_coloured_output( enabled: A boolean value to enable or disable coloured output. + + Raises: + RuntimeError: + If the coloured output could not be set. """ retval: int = lib.pactffi_verifier_set_coloured_output( handle._ref, @@ -6831,6 +6974,10 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> enabled: If `True`, an error will be raised when no pacts are found to verify. + + Raises: + RuntimeError: + If the no pacts is error setting could not be set. """ retval: int = lib.pactffi_verifier_set_no_pacts_is_error( handle._ref, @@ -6869,6 +7016,10 @@ def verifier_set_publish_options( provider_branch: Name of the branch used for verification. + + Raises: + RuntimeError: + If the publish options could not be set. """ retval: int = lib.pactffi_verifier_set_publish_options( handle._ref, @@ -7113,6 +7264,10 @@ def verifier_execute(handle: VerifierHandle) -> None: Runs the verification. (https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_execute) + + Raises: + RuntimeError: + If the verifier could not be executed. """ success: int = lib.pactffi_verifier_execute(handle._ref) if success != 0: @@ -7189,6 +7344,10 @@ def verifier_logs(handle: VerifierHandle) -> OwnedString: This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` function call to avoid leaking memory. + + Raises: + RuntimeError: + If the logs could not be extracted. """ ptr = lib.pactffi_verifier_logs(handle._ref) if ptr == ffi.NULL: @@ -7207,6 +7366,10 @@ def verifier_logs_for_provider(provider_name: str) -> OwnedString: This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` function call to avoid leaking memory. + + Raises: + RuntimeError: + If the logs could not be extracted. """ ptr = lib.pactffi_verifier_logs_for_provider(provider_name.encode("utf-8")) if ptr == ffi.NULL: @@ -7230,6 +7393,10 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: This parameter controls ANSI escape codes. Setting it to a non-zero value will cause the ANSI control codes to be stripped from the output. + + Raises: + RuntimeError: + If the output could not be extracted. """ ptr = lib.pactffi_verifier_output(handle._ref, strip_ansi) if ptr == ffi.NULL: @@ -7244,6 +7411,10 @@ def verifier_json(handle: VerifierHandle) -> OwnedString: [Rust `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_json) + + Raises: + RuntimeError: + If the JSON could not be extracted. """ ptr = lib.pactffi_verifier_json(handle._ref) if ptr == ffi.NULL: @@ -7279,6 +7450,10 @@ def using_plugin( plugin_version: Version of the plugin to use. If `None`, the latest version will be used. + + Raises: + RuntimeError: + If the plugin could not be loaded. """ ret: int = lib.pactffi_using_plugin( pact._ref, @@ -7340,6 +7515,10 @@ def interaction_contents( contents: JSON contents that gets passed to the plugin. + + Raises: + RuntimeError: + If the interaction could not be configured """ ret: int = lib.pactffi_interaction_contents( interaction._ref, From 157bceb3c6a177717fb8ccfbe4ca0ed9184ca0ea Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 11:47:36 +1000 Subject: [PATCH 0390/1376] chore(ffi): bump links to 0.4.21 Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 490 +++++++++---------- src/pact/v3/interaction/_http_interaction.py | 4 +- 2 files changed, 247 insertions(+), 247 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index d0eeaee96..8108dcee4 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -417,7 +417,7 @@ class InteractionHandle: Handle to a HTTP Interaction. [Rust - `InteractionHandle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/mock_server/handles/struct.InteractionHandle.html) + `InteractionHandle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/handles/struct.InteractionHandle.html) """ def __init__(self, ref: int) -> None: @@ -879,7 +879,7 @@ class PactHandle: Handle to a Pact. [Rust - `PactHandle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/mock_server/handles/struct.PactHandle.html) + `PactHandle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/handles/struct.PactHandle.html) """ def __init__(self, ref: int) -> None: @@ -1517,7 +1517,7 @@ class VerifierHandle: """ Handle to a Verifier. - [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/verifier/handle/struct.VerifierHandle.html) + [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/verifier/handle/struct.VerifierHandle.html) """ def __init__(self, ref: cffi.FFI.CData) -> None: @@ -1553,7 +1553,7 @@ class ExpressionValueType(Enum): """ Expression Value Type. - [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/models/expressions/enum.ExpressionValueType.html) + [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/expressions/enum.ExpressionValueType.html) """ UNKNOWN = lib.ExpressionValueType_Unknown @@ -1580,7 +1580,7 @@ class GeneratorCategory(Enum): """ Generator Category. - [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/models/generators/enum.GeneratorCategory.html) + [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/generators/enum.GeneratorCategory.html) """ METHOD = lib.GeneratorCategory_METHOD @@ -1608,7 +1608,7 @@ class InteractionPart(Enum): """ Interaction Part. - [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/mock_server/handles/enum.InteractionPart.html) + [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/handles/enum.InteractionPart.html) """ REQUEST = lib.InteractionPart_Request @@ -1654,7 +1654,7 @@ class MatchingRuleCategory(Enum): """ Matching Rule Category. - [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) + [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) """ METHOD = lib.MatchingRuleCategory_METHOD @@ -1683,7 +1683,7 @@ class PactSpecification(Enum): """ Pact Specification. - [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/models/pact_specification/enum.PactSpecification.html) + [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/pact_specification/enum.PactSpecification.html) """ UNKNOWN = lib.PactSpecification_Unknown @@ -1736,7 +1736,7 @@ class _StringResult(Enum): """ Internal enum from Pact FFI. - [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/mock_server/enum.StringResult.html) + [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/enum.StringResult.html) """ FAILED = lib.StringResult_Failed @@ -1892,7 +1892,7 @@ def version() -> str: """ Return the version of the pact_ffi library. - [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_version) + [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_version) Returns: The version of the pact_ffi library as a string, in the form of `x.y.z`. @@ -1912,7 +1912,7 @@ def init(log_env_var: str) -> None: tracing subscriber. [Rust - `pactffi_init`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_init) + `pactffi_init`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_init) # Safety @@ -1929,7 +1929,7 @@ def init_with_log_level(level: str = "INFO") -> None: tracing subscriber. [Rust - `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_init_with_log_level) + `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_init_with_log_level) Args: level: @@ -1949,7 +1949,7 @@ def enable_ansi_support() -> None: On non-Windows platforms, this function is a no-op. [Rust - `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_enable_ansi_support) + `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_enable_ansi_support) # Safety @@ -1967,7 +1967,7 @@ def log_message( Log using the shared core logging facility. [Rust - `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_message) + `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_message) This is useful for callers to have a single set of logs. @@ -1999,7 +1999,7 @@ def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: Get an iterator over mismatches. [Rust - `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatches_get_iter) + `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_get_iter) """ raise NotImplementedError @@ -2008,7 +2008,7 @@ def mismatches_delete(mismatches: Mismatches) -> None: """ Delete mismatches. - [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatches_delete) + [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_delete) """ raise NotImplementedError @@ -2017,7 +2017,7 @@ def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: """ Get the next mismatch from a mismatches iterator. - [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatches_iter_next) + [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_iter_next) Returns a null pointer if no mismatches remain. """ @@ -2028,7 +2028,7 @@ def mismatches_iter_delete(iter: MismatchesIterator) -> None: """ Delete a mismatches iterator when you're done with it. - [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatches_iter_delete) + [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_iter_delete) """ raise NotImplementedError @@ -2037,7 +2037,7 @@ def mismatch_to_json(mismatch: Mismatch) -> str: """ Get a JSON representation of the mismatch. - [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_to_json) + [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_to_json) """ raise NotImplementedError @@ -2046,7 +2046,7 @@ def mismatch_type(mismatch: Mismatch) -> str: """ Get the type of a mismatch. - [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_type) + [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_type) """ raise NotImplementedError @@ -2055,7 +2055,7 @@ def mismatch_summary(mismatch: Mismatch) -> str: """ Get a summary of a mismatch. - [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_summary) + [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_summary) """ raise NotImplementedError @@ -2064,7 +2064,7 @@ def mismatch_description(mismatch: Mismatch) -> str: """ Get a description of a mismatch. - [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_description) + [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_description) """ raise NotImplementedError @@ -2073,7 +2073,7 @@ def mismatch_ansi_description(mismatch: Mismatch) -> str: """ Get an ANSI-compatible description of a mismatch. - [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_ansi_description) + [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_ansi_description) """ raise NotImplementedError @@ -2083,7 +2083,7 @@ def get_error_message(length: int = 1024) -> str | None: Provide the error message from `LAST_ERROR` to the calling C code. [Rust - `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_get_error_message) + `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_get_error_message) This function should be called after any other function in the pact_matching FFI indicates a failure with its own error message, if the caller wants to @@ -2137,7 +2137,7 @@ def log_to_stdout(level_filter: LevelFilter) -> int: """ Convenience function to direct all logging to stdout. - [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_stdout) + [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_stdout) """ raise NotImplementedError @@ -2147,7 +2147,7 @@ def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: Convenience function to direct all logging to stderr. [Rust - `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_stderr) + `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_stderr) Args: level_filter: @@ -2172,7 +2172,7 @@ def log_to_file(file_name: str, level_filter: LevelFilter) -> int: Convenience function to direct all logging to a file. [Rust - `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_file) + `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_file) # Safety @@ -2186,7 +2186,7 @@ def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: """ Convenience function to direct all logging to a task local memory buffer. - [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_buffer) + [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_buffer) Raises: RuntimeError: @@ -2204,7 +2204,7 @@ def logger_init() -> None: """ Initialize the FFI logger with no sinks. - [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_logger_init) + [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_logger_init) This initialized logger does nothing until `pactffi_logger_apply` has been called. @@ -2226,7 +2226,7 @@ def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: Attach an additional sink to the thread-local logger. [Rust - `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_logger_attach_sink) + `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_logger_attach_sink) This logger does nothing until `pactffi_logger_apply` has been called. @@ -2273,7 +2273,7 @@ def logger_apply() -> int: Apply the previously configured sinks and levels to the program. [Rust - `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_logger_apply) + `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_logger_apply) If no sinks have been setup, will set the log level to info and the target to standard out. @@ -2289,7 +2289,7 @@ def fetch_log_buffer(log_id: str) -> str: Fetch the in-memory logger buffer contents. [Rust - `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_fetch_log_buffer) + `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_fetch_log_buffer) This will only have any contents if the `buffer` sink has been configured to log to. The contents will be allocated on the heap and will need to be freed @@ -2315,7 +2315,7 @@ def parse_pact_json(json: str) -> Pact: Parses the provided JSON into a Pact model. [Rust - `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_parse_pact_json) + `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_parse_pact_json) The returned Pact model must be freed with the `pactffi_pact_model_delete` function when no longer needed. @@ -2332,7 +2332,7 @@ def pact_model_delete(pact: Pact) -> None: """ Frees the memory used by the Pact model. - [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_model_delete) + [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_model_delete) """ raise NotImplementedError @@ -2342,7 +2342,7 @@ def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: Returns an iterator over all the interactions in the Pact. [Rust - `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_model_interaction_iterator) + `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_model_interaction_iterator) The iterator will have to be deleted using the `pactffi_pact_interaction_iter_delete` function. The iterator will contain a @@ -2361,7 +2361,7 @@ def pact_spec_version(pact: Pact) -> PactSpecification: """ Returns the Pact specification enum that the Pact is for. - [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_spec_version) + [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_spec_version) """ raise NotImplementedError @@ -2370,7 +2370,7 @@ def pact_interaction_delete(interaction: PactInteraction) -> None: """ Frees the memory used by the Pact interaction model. - [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_delete) + [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_delete) """ raise NotImplementedError @@ -2379,7 +2379,7 @@ def async_message_new() -> AsynchronousMessage: """ Get a mutable pointer to a newly-created default message on the heap. - [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_new) + [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_new) # Safety @@ -2396,7 +2396,7 @@ def async_message_delete(message: AsynchronousMessage) -> None: """ Destroy the `AsynchronousMessage` being pointed to. - [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_delete) + [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_delete) """ lib.pactffi_async_message_delete(message._ptr) @@ -2406,7 +2406,7 @@ def async_message_get_contents(message: AsynchronousMessage) -> MessageContents Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. [Rust - `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents) + `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents) If the message contents are missing, this function will return `None`. """ @@ -2425,7 +2425,7 @@ def async_message_generate_contents( contents as would be received by the consumer. [Rust - `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_generate_contents) + `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_generate_contents) If the message contents are missing, this function will return `None`. """ @@ -2439,7 +2439,7 @@ def async_message_get_contents_str(message: AsynchronousMessage) -> str: """ Get the message contents of an `AsynchronousMessage` in string form. - [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents_str) + [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents_str) # Safety @@ -2466,7 +2466,7 @@ def async_message_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_set_contents_str) + `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_set_contents_str) - `message` - the message to set the contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -2494,7 +2494,7 @@ def async_message_get_contents_length(message: AsynchronousMessage) -> int: Get the length of the contents of a `AsynchronousMessage`. [Rust - `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents_length) + `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents_length) # Safety @@ -2513,7 +2513,7 @@ def async_message_get_contents_bin(message: AsynchronousMessage) -> str: Get the contents of an `AsynchronousMessage` as bytes. [Rust - `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents_bin) + `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents_bin) # Safety @@ -2540,7 +2540,7 @@ def async_message_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_set_contents_bin) + `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_set_contents_bin) * `message` - the message to set the contents for * `contents` - pointer to contents to copy from @@ -2567,7 +2567,7 @@ def async_message_get_description(message: AsynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_description) + `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_description) Raises: RuntimeError: @@ -2587,7 +2587,7 @@ def async_message_set_description( """ Write the `description` field on the `AsynchronousMessage`. - [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_set_description) + [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_set_description) # Safety @@ -2612,7 +2612,7 @@ def async_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_provider_state) + `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_provider_state) Raises: RuntimeError: @@ -2631,7 +2631,7 @@ def async_message_get_provider_state_iter( """ Get an iterator over provider states. - [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) + [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) # Safety @@ -2646,7 +2646,7 @@ def consumer_get_name(consumer: Consumer) -> str: r""" Get a copy of this consumer's name. - [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_consumer_get_name) + [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_consumer_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -2692,7 +2692,7 @@ def pact_get_consumer(pact: Pact) -> Consumer: `pactffi_pact_consumer_delete` when no longer required. [Rust - `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_get_consumer) + `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_get_consumer) # Errors @@ -2706,7 +2706,7 @@ def pact_consumer_delete(consumer: Consumer) -> None: """ Frees the memory used by the Pact consumer. - [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_consumer_delete) + [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_consumer_delete) """ raise NotImplementedError @@ -2722,7 +2722,7 @@ def message_contents_delete(contents: MessageContents) -> None: Deleting a message content which is associated with an interaction will result in undefined behaviour. - [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_delete) + [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_delete) """ lib.pactffi_message_contents_delete(contents._ptr) @@ -2731,7 +2731,7 @@ def message_contents_get_contents_str(contents: MessageContents) -> str | None: """ Get the message contents in string form. - [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_str) + [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_contents_str) If the message has no contents or contain invalid UTF-8 characters, this function will return `None`. @@ -2751,7 +2751,7 @@ def message_contents_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_set_contents_str) + `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_set_contents_str) * `contents` - the message contents to set the contents for * `contents_str` - pointer to contents to copy from. Must be a valid @@ -2778,7 +2778,7 @@ def message_contents_get_contents_length(contents: MessageContents) -> int: """ Get the length of the message contents. - [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_length) + [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_contents_length) If the message has not contents, this function will return 0. """ @@ -2790,7 +2790,7 @@ def message_contents_get_contents_bin(contents: MessageContents) -> bytes | None Get the contents of a message as a pointer to an array of bytes. [Rust - `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_bin) + `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_contents_bin) If the message has no contents, this function will return `None`. """ @@ -2813,7 +2813,7 @@ def message_contents_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_set_contents_bin) + `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_set_contents_bin) * `message` - the message contents to set the contents for * `contents_bin` - pointer to contents to copy from @@ -2842,7 +2842,7 @@ def message_contents_get_metadata_iter( Get an iterator over the metadata of a message. [Rust - `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) + `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) # Safety @@ -2871,7 +2871,7 @@ def message_contents_get_matching_rule_iter( Get an iterator over the matching rules for a category of a message. [Rust - `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) + `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -2913,7 +2913,7 @@ def request_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP request. - [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) + [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -2950,7 +2950,7 @@ def response_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP response. - [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) + [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -2988,7 +2988,7 @@ def message_contents_get_generators_iter( Get an iterator over the generators for a category of a message. [Rust - `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_generators_iter) + `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_generators_iter) # Safety @@ -3014,7 +3014,7 @@ def request_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP request. [Rust - `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_request_contents_get_generators_iter) + `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_request_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -3039,7 +3039,7 @@ def response_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP response. [Rust - `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_response_contents_get_generators_iter) + `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -3064,7 +3064,7 @@ def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: any generator. [Rust - `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_parse_matcher_definition) + `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_parse_matcher_definition) The following are examples of matching rule definitions: @@ -3100,7 +3100,7 @@ def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: using the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_error) + `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_error) """ raise NotImplementedError @@ -3114,7 +3114,7 @@ def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_value) + `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_value) Note that different expressions values can have types other than a string. Use `pactffi_matcher_definition_value_type` to get the actual type of the @@ -3128,7 +3128,7 @@ def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: """ Frees the memory used by the result of parsing the matching definition expression. - [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_delete) + [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_delete) """ raise NotImplementedError @@ -3141,7 +3141,7 @@ def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Ge NULL pointer, otherwise returns the generator as a pointer. [Rust - `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_generator) + `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_generator) The generator pointer will be a valid pointer as long as `pactffi_matcher_definition_delete` has not been called on the definition. @@ -3160,7 +3160,7 @@ def matcher_definition_value_type( If there was an error parsing the expression, it will return Unknown. [Rust - `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_value_type) + `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_value_type) """ raise NotImplementedError @@ -3169,7 +3169,7 @@ def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: """ Free the iterator when you're done using it. - [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_iter_delete) + [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_iter_delete) """ raise NotImplementedError @@ -3184,7 +3184,7 @@ def matcher_definition_iter( `pactffi_matching_rule_iter_delete` function once done with it. [Rust - `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_iter) + `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_iter) If there was an error parsing the expression, this function will return a NULL pointer. @@ -3200,7 +3200,7 @@ def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: deleted but will be cleaned up when the iterator is deleted. [Rust - `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_iter_next) + `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_iter_next) Will return a NULL pointer when the iterator has advanced past the end of the list. @@ -3222,7 +3222,7 @@ def matching_rule_id(rule_result: MatchingRuleResult) -> int: Return the ID of the matching rule. [Rust - `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_id) + `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_id) The ID corresponds to the following rules: @@ -3268,7 +3268,7 @@ def matching_rule_value(rule_result: MatchingRuleResult) -> str: pointer. [Rust - `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_value) + `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_value) The associated values for the rules are: @@ -3316,7 +3316,7 @@ def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: Will return a NULL pointer if the matching rule result was a reference. [Rust - `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_pointer) + `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_pointer) # Safety @@ -3334,7 +3334,7 @@ def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: structure. I.e., [Rust - `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_reference_name) + `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_reference_name) ```json { @@ -3361,7 +3361,7 @@ def validate_datetime(value: str, format: str) -> None: Validates the date/time value against the date/time format string. [Rust - `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_validate_datetime) + `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_validate_datetime) Raises: ValueError: @@ -3388,7 +3388,7 @@ def generator_to_json(generator: Generator) -> str: Get the JSON form of the generator. [Rust - `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generator_to_json) + `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generator_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -3411,7 +3411,7 @@ def generator_generate_string(generator: Generator, context_json: str) -> str: function). [Rust - `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generator_generate_string) + `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generator_generate_string) If anything goes wrong, it will return a NULL pointer. """ @@ -3434,7 +3434,7 @@ def generator_generate_integer(generator: Generator, context_json: str) -> int: should be the values returned from the Provider State callback function). [Rust - `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generator_generate_integer) + `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generator_generate_integer) If anything goes wrong or the generator is not a type that can generate an integer value, it will return a zero value. @@ -3450,7 +3450,7 @@ def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generators_iter_delete) + `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generators_iter_delete) """ lib.pactffi_generators_iter_delete(iter._ptr) @@ -3460,7 +3460,7 @@ def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePa Get the next path and generator out of the iterator, if possible. [Rust - `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generators_iter_next) + `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generators_iter_next) The returned pointer must be deleted with `pactffi_generator_iter_pair_delete`. @@ -3480,7 +3480,7 @@ def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: Free a pair of key and value returned from `pactffi_generators_iter_next`. [Rust - `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generators_iter_pair_delete) + `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generators_iter_pair_delete) """ lib.pactffi_generators_iter_pair_delete(pair._ptr) @@ -3489,7 +3489,7 @@ def sync_http_new() -> SynchronousHttp: """ Get a mutable pointer to a newly-created default interaction on the heap. - [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_new) + [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_new) # Safety @@ -3507,7 +3507,7 @@ def sync_http_delete(interaction: SynchronousHttp) -> None: Destroy the `SynchronousHttp` interaction being pointed to. [Rust - `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_delete) + `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_delete) """ lib.pactffi_sync_http_delete(interaction) @@ -3517,7 +3517,7 @@ def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: Get the request of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request) + `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request) # Safety @@ -3537,7 +3537,7 @@ def sync_http_get_request_contents(interaction: SynchronousHttp) -> str | None: Get the request contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request_contents) + `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request_contents) Note that this function will return `None` if either the body is missing or is `null`. @@ -3557,7 +3557,7 @@ def sync_http_set_request_contents( Sets the request contents of the interaction. [Rust - `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_request_contents) + `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_request_contents) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -3585,7 +3585,7 @@ def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: Get the length of the request contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) + `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) This function will return 0 if the body is missing. """ @@ -3597,7 +3597,7 @@ def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes | Get the request contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) + `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) Note that this function will return `None` if either the body is missing or is `null`. @@ -3621,7 +3621,7 @@ def sync_http_set_request_contents_bin( Sets the request contents of the interaction as an array of bytes. [Rust - `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) + `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from @@ -3648,7 +3648,7 @@ def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: Get the response of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response) + `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response) # Safety @@ -3668,7 +3668,7 @@ def sync_http_get_response_contents(interaction: SynchronousHttp) -> str | None: Get the response contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response_contents) + `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response_contents) Note that this function will return `None` if either the body is missing or is `null`. @@ -3688,7 +3688,7 @@ def sync_http_set_response_contents( Sets the response contents of the interaction. [Rust - `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_response_contents) + `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_response_contents) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -3716,7 +3716,7 @@ def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: Get the length of the response contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) + `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) This function will return 0 if the body is missing. """ @@ -3728,7 +3728,7 @@ def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes | Get the response contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) + `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) Note that this function will return `None` if either the body is missing or is `null`. @@ -3752,7 +3752,7 @@ def sync_http_set_response_contents_bin( Sets the response contents of the `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) + `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from @@ -3779,7 +3779,7 @@ def sync_http_get_description(interaction: SynchronousHttp) -> str: Get a copy of the description. [Rust - `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_description) + `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_description) Raises: RuntimeError: @@ -3797,7 +3797,7 @@ def sync_http_set_description(interaction: SynchronousHttp, description: str) -> Write the `description` field on the `SynchronousHttp`. [Rust - `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_description) + `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_description) # Safety @@ -3822,7 +3822,7 @@ def sync_http_get_provider_state( Get a copy of the provider state at the given index from this interaction. [Rust - `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_provider_state) + `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_provider_state) # Safety @@ -3848,7 +3848,7 @@ def sync_http_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) + `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) # Safety @@ -3877,7 +3877,7 @@ def pact_interaction_as_synchronous_http( longer required. [Rust - `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) + `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) # Safety This function is safe as long as the interaction pointer is a valid pointer. @@ -3899,7 +3899,7 @@ def pact_interaction_as_asynchronous_message( no longer required. [Rust - `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) + `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) Note that if the interaction is a V3 `Message`, it will be converted to a V4 `AsynchronousMessage` before being returned. @@ -3924,7 +3924,7 @@ def pact_interaction_as_synchronous_message( no longer required. [Rust - `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) + `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) # Safety This function is safe as long as the interaction pointer is a valid pointer. @@ -3939,7 +3939,7 @@ def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> Asynchronous Get the next asynchronous message from the iterator. [Rust - `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_async_message_iter_next) + `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_async_message_iter_next) Raises: StopIteration: @@ -3956,7 +3956,7 @@ def pact_async_message_iter_delete(iter: PactAsyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_async_message_iter_delete) + `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_async_message_iter_delete) """ lib.pactffi_pact_async_message_iter_delete(iter._ptr) @@ -3966,7 +3966,7 @@ def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMes Get the next synchronous request/response message from the V4 pact. [Rust - `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_message_iter_next) + `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_message_iter_next) Raises: StopIteration: @@ -3983,7 +3983,7 @@ def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) + `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) """ lib.pactffi_pact_sync_message_iter_delete(iter._ptr) @@ -3993,7 +3993,7 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: Get the next synchronous HTTP request/response interaction from the V4 pact. [Rust - `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_http_iter_next) + `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_http_iter_next) Raises: StopIteration: @@ -4010,7 +4010,7 @@ def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) + `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) """ lib.pactffi_pact_sync_http_iter_delete(iter._ptr) @@ -4020,7 +4020,7 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction Get the next interaction from the pact. [Rust - `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_iter_next) + `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_iter_next) Raises: StopIteration: @@ -4038,7 +4038,7 @@ def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_iter_delete) + `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_iter_delete) """ lib.pactffi_pact_interaction_iter_delete(iter._ptr) @@ -4048,7 +4048,7 @@ def matching_rule_to_json(rule: MatchingRule) -> str: Get the JSON form of the matching rule. [Rust - `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_to_json) + `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -4065,7 +4065,7 @@ def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rules_iter_delete) + `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rules_iter_delete) """ lib.pactffi_matching_rules_iter_delete(iter._ptr) @@ -4077,7 +4077,7 @@ def matching_rules_iter_next( Get the next path and matching rule out of the iterator, if possible. [Rust - `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rules_iter_next) + `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rules_iter_next) The returned pointer must be deleted with `pactffi_matching_rules_iter_pair_delete`. @@ -4099,7 +4099,7 @@ def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) + `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) """ lib.pactffi_matching_rules_iter_pair_delete(pair._ptr) @@ -4109,7 +4109,7 @@ def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: Get the next value from the iterator. [Rust - `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_iter_next) + `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_iter_next) # Safety @@ -4130,7 +4130,7 @@ def provider_state_iter_delete(iter: ProviderStateIterator) -> None: Delete the iterator. [Rust - `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_iter_delete) + `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_iter_delete) """ lib.pactffi_provider_state_iter_delete(iter._ptr) @@ -4140,7 +4140,7 @@ def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadata Get the next key and value out of the iterator, if possible. [Rust - `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_metadata_iter_next) + `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_metadata_iter_next) The returned pointer must be deleted with `pactffi_message_metadata_pair_delete`. @@ -4166,7 +4166,7 @@ def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: Free the metadata iterator when you're done using it. [Rust - `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_metadata_iter_delete) + `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_metadata_iter_delete) """ lib.pactffi_message_metadata_iter_delete(iter._ptr) @@ -4176,7 +4176,7 @@ def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_metadata_pair_delete) + `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_metadata_pair_delete) """ lib.pactffi_message_metadata_pair_delete(pair._ptr) @@ -4186,7 +4186,7 @@ def provider_get_name(provider: Provider) -> str: Get a copy of this provider's name. [Rust - `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_get_name) + `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -4232,7 +4232,7 @@ def pact_get_provider(pact: Pact) -> Provider: `pactffi_pact_provider_delete` when no longer required. [Rust - `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_get_provider) + `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_get_provider) # Errors @@ -4247,7 +4247,7 @@ def pact_provider_delete(provider: Provider) -> None: Frees the memory used by the Pact provider. [Rust - `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_provider_delete) + `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_provider_delete) """ raise NotImplementedError @@ -4257,7 +4257,7 @@ def provider_state_get_name(provider_state: ProviderState) -> str | None: Get the name of the provider state as a string. [Rust - `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_get_name) + `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_get_name) Raises: RuntimeError: @@ -4277,7 +4277,7 @@ def provider_state_get_param_iter( Get an iterator over the params of a provider state. [Rust - `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_get_param_iter) + `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_get_param_iter) # Safety @@ -4305,7 +4305,7 @@ def provider_state_param_iter_next( Get the next key and value out of the iterator, if possible. [Rust - `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_param_iter_next) + `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_param_iter_next) # Safety @@ -4326,7 +4326,7 @@ def provider_state_delete(provider_state: ProviderState) -> None: Free the provider state when you're done using it. [Rust - `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_delete) + `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_delete) """ raise NotImplementedError @@ -4336,7 +4336,7 @@ def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: Free the provider state param iterator when you're done using it. [Rust - `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_param_iter_delete) + `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_param_iter_delete) """ lib.pactffi_provider_state_param_iter_delete(iter._ptr) @@ -4346,7 +4346,7 @@ def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: Free a pair of key and value. [Rust - `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_param_pair_delete) + `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_param_pair_delete) """ lib.pactffi_provider_state_param_pair_delete(pair._ptr) @@ -4356,7 +4356,7 @@ def sync_message_new() -> SynchronousMessage: Get a mutable pointer to a newly-created default message on the heap. [Rust - `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_new) + `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_new) # Safety @@ -4374,7 +4374,7 @@ def sync_message_delete(message: SynchronousMessage) -> None: Destroy the `Message` being pointed to. [Rust - `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_delete) + `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_delete) """ lib.pactffi_sync_message_delete(message._ptr) @@ -4384,7 +4384,7 @@ def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: Get the request contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) + `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) # Safety @@ -4411,7 +4411,7 @@ def sync_message_set_request_contents_str( Sets the request contents of the message. [Rust - `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) + `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) - `message` - the message to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -4439,7 +4439,7 @@ def sync_message_get_request_contents_length(message: SynchronousMessage) -> int Get the length of the request contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) + `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) # Safety @@ -4458,7 +4458,7 @@ def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes: Get the request contents of a `SynchronousMessage` as a bytes. [Rust - `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) + `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) # Safety @@ -4485,7 +4485,7 @@ def sync_message_set_request_contents_bin( Sets the request contents of the message as an array of bytes. [Rust - `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) + `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) * `message` - the message to set the request contents for * `contents` - pointer to contents to copy from @@ -4512,7 +4512,7 @@ def sync_message_get_request_contents(message: SynchronousMessage) -> MessageCon Get the request contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_request_contents) + `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents) # Safety @@ -4539,7 +4539,7 @@ def sync_message_generate_request_contents( contents as would be received by the consumer. [Rust - `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_generate_request_contents) + `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_generate_request_contents) Raises: RuntimeError: @@ -4557,7 +4557,7 @@ def sync_message_get_number_responses(message: SynchronousMessage) -> int: Get the number of response messages in the `SynchronousMessage`. [Rust - `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_number_responses) + `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_number_responses) If the message is null, this function will return 0. """ @@ -4572,7 +4572,7 @@ def sync_message_get_response_contents_str( Get the response contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) + `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) # Safety @@ -4605,7 +4605,7 @@ def sync_message_set_response_contents_str( with default values. [Rust - `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) + `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response. @@ -4637,7 +4637,7 @@ def sync_message_get_response_contents_length( Get the length of the response contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) + `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) # Safety @@ -4659,7 +4659,7 @@ def sync_message_get_response_contents_bin( Get the response contents of a `SynchronousMessage` as bytes. [Rust - `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) + `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) # Safety @@ -4690,7 +4690,7 @@ def sync_message_set_response_contents_bin( responses will be padded with default values. [Rust - `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) + `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response @@ -4721,7 +4721,7 @@ def sync_message_get_response_contents( Get the response contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_response_contents) + `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents) # Safety @@ -4750,7 +4750,7 @@ def sync_message_generate_response_contents( received by the consumer. [Rust - `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_generate_response_contents) + `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_generate_response_contents) Raises: RuntimeError: @@ -4768,7 +4768,7 @@ def sync_message_get_description(message: SynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_description) + `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_description) Raises: RuntimeError: @@ -4786,7 +4786,7 @@ def sync_message_set_description(message: SynchronousMessage, description: str) Write the `description` field on the `SynchronousMessage`. [Rust - `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_description) + `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_description) # Safety @@ -4811,7 +4811,7 @@ def sync_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_provider_state) + `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_provider_state) # Safety @@ -4837,7 +4837,7 @@ def sync_message_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) + `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) # Safety @@ -4859,7 +4859,7 @@ def string_delete(string: OwnedString) -> None: Delete a string previously returned by this FFI. [Rust - `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_string_delete) + `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_string_delete) """ lib.pactffi_string_delete(string._ptr) @@ -4874,7 +4874,7 @@ def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: the mock server is returned. [Rust - `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_create_mock_server) + `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_create_mock_server) * `pact_str` - Pact JSON * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:80) @@ -4910,7 +4910,7 @@ def get_tls_ca_certificate() -> OwnedString: Fetch the CA Certificate used to generate the self-signed certificate. [Rust - `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_get_tls_ca_certificate) + `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_get_tls_ca_certificate) **NOTE:** The string for the result is allocated on the heap, and will have to be freed by the caller using pactffi_string_delete. @@ -4931,7 +4931,7 @@ def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) - operating system. The port of the mock server is returned. [Rust - `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_create_mock_server_for_pact) + `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_create_mock_server_for_pact) * `pact` - Handle to a Pact model created with created with `pactffi_new_pact`. @@ -4974,7 +4974,7 @@ def create_mock_server_for_transport( Create a mock server for the provided Pact handle and transport. [Rust - `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_create_mock_server_for_transport) + `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_create_mock_server_for_transport) Args: pact: @@ -5036,7 +5036,7 @@ def mock_server_matched(mock_server_handle: PactServerHandle) -> bool: if any request has not been successfully matched, or the method panics. [Rust - `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mock_server_matched) + `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mock_server_matched) """ return lib.pactffi_mock_server_matched(mock_server_handle._ref) @@ -5048,7 +5048,7 @@ def mock_server_mismatches( External interface to get all the mismatches from a mock server. [Rust - `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mock_server_mismatches) + `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mock_server_mismatches) # Errors @@ -5075,7 +5075,7 @@ def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: and cleanup any memory allocated for it. [Rust - `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_cleanup_mock_server) + `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_cleanup_mock_server) Args: mock_server_handle: @@ -5104,7 +5104,7 @@ def write_pact_file( directory to write the file to is passed as the second parameter. [Rust - `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_write_pact_file) + `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_write_pact_file) Args: mock_server_handle: @@ -5156,7 +5156,7 @@ def mock_server_logs(mock_server_handle: PactServerHandle) -> str: started. [Rust - `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mock_server_logs) + `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mock_server_logs) Raises: RuntimeError: @@ -5180,7 +5180,7 @@ def generate_datetime_string(format: str) -> StringResult: string needs to be freed with the `pactffi_string_delete` function [Rust - `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generate_datetime_string) + `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generate_datetime_string) # Safety @@ -5197,7 +5197,7 @@ def check_regex(regex: str, example: str) -> bool: Checks that the example string matches the given regex. [Rust - `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_check_regex) + `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_check_regex) # Safety @@ -5216,7 +5216,7 @@ def generate_regex_value(regex: str) -> StringResult: `pactffi_string_delete` function. [Rust - `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generate_regex_value) + `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generate_regex_value) # Safety @@ -5231,7 +5231,7 @@ def free_string(s: str) -> None: [DEPRECATED] Frees the memory allocated to a string by another function. [Rust - `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_free_string) + `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_free_string) This function is deprecated. Use `pactffi_string_delete` instead. @@ -5253,7 +5253,7 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle: Creates a new Pact model and returns a handle to it. [Rust - `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_pact) + `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_pact) Args: consumer_name: @@ -5294,7 +5294,7 @@ def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: will result in that interaction being replaced with the new one. [Rust - `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_interaction) + `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_interaction) Args: pact: @@ -5322,7 +5322,7 @@ def new_message_interaction(pact: PactHandle, description: str) -> InteractionHa will result in that interaction being replaced with the new one. [Rust - `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_message_interaction) + `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_message_interaction) Args: pact: @@ -5353,7 +5353,7 @@ def new_sync_message_interaction( will result in that interaction being replaced with the new one. [Rust - `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_sync_message_interaction) + `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_sync_message_interaction) Args: pact: @@ -5378,7 +5378,7 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> None: Sets the description for the Interaction. [Rust - `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_upon_receiving) + `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_upon_receiving) This function @@ -5419,7 +5419,7 @@ def given(interaction: InteractionHandle, description: str) -> None: Adds a provider state to the Interaction. [Rust - `pactffi_given`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_given) + `pactffi_given`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_given) Args: interaction: @@ -5446,7 +5446,7 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> Non used with V4 interactions. [Rust - `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_interaction_test_name) + `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_interaction_test_name) Args: interaction: @@ -5493,7 +5493,7 @@ def given_with_param( be parsed as JSON. [Rust - `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_given_with_param) + `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_given_with_param) Args: interaction: @@ -5535,7 +5535,7 @@ def given_with_params( with a `value` key. [Rust - `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_given_with_params) + `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_given_with_params) Args: interaction: @@ -5574,7 +5574,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None Configures the request for the Interaction. [Rust - `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_request) + `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_request) Args: interaction: @@ -5588,7 +5588,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) which allows regex patterns. For examples: ```json @@ -5623,7 +5623,7 @@ def with_query_parameter_v2( Configures a query parameter for the Interaction. [Rust - `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_query_parameter_v2) + `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_query_parameter_v2) To setup a query parameter with multiple values, you can either call this function multiple times with a different index value: @@ -5660,7 +5660,7 @@ def with_query_parameter_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) If you want the matching rules to apply to all values (and not just the one with the given index), make sure to set the value to be an array. @@ -5712,7 +5712,7 @@ def with_query_parameter_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -5734,7 +5734,7 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None: Sets the specification version for a given Pact model. [Rust - `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_specification) + `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_specification) Args: pact: @@ -5758,7 +5758,7 @@ def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: Fetches the Pact specification version for the given Pact model. [Rust - `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_handle_get_pact_spec_version) + `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_handle_get_pact_spec_version) Args: handle: @@ -5784,7 +5784,7 @@ def with_pact_metadata( mock server for it has already started) [Rust - `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_pact_metadata) + `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_pact_metadata) Args: pact: @@ -5847,7 +5847,7 @@ def with_metadata( ``` See - [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) # Note @@ -5896,7 +5896,7 @@ def with_header_v2( r""" Configures a header for the Interaction. - [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_header_v2) + [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_header_v2) To setup a header with multiple values, you can either call this function multiple times with a different index value: @@ -5934,7 +5934,7 @@ def with_header_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -5956,7 +5956,7 @@ def with_header_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -5988,7 +5988,7 @@ def set_header( and generators can not be configured with it. [Rust - `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_set_header) + `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_header) If matching rules are required to be set, use `pactffi_with_header_v2`. @@ -6026,7 +6026,7 @@ def response_status(interaction: InteractionHandle, status: int) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_response_status) + `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_status) Args: interaction: @@ -6050,7 +6050,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_response_status_v2) + `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_status_v2) To include matching rules for the status (only statusCode or integer really makes sense to use), include the matching rule JSON format with the value as @@ -6069,7 +6069,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -6080,7 +6080,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -6104,7 +6104,7 @@ def with_body( Adds the body for the interaction. [Rust - `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_body) + `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -6125,7 +6125,7 @@ def with_body( body: The body contents. For JSON payloads, matching rules can be embedded - in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). + in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -6152,7 +6152,7 @@ def with_binary_body( Adds the body for the interaction. [Rust - `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_binary_body) + `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_binary_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -6196,7 +6196,7 @@ def with_binary_file( already started) [Rust - `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_binary_file) + `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_binary_file) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -6250,7 +6250,7 @@ def with_matching_rules( Add matching rules to the interaction. [Rust - `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_matching_rules) + `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_matching_rules) This function can be called multiple times, in which case the matching rules will be merged. @@ -6288,7 +6288,7 @@ def with_generators( Add generators to the interaction. [Rust - `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_generators) + `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_generators) This function can be called multiple times, in which case the generators will be combined (provide they don't clash). @@ -6336,7 +6336,7 @@ def with_multipart_file_v2( # noqa: PLR0913 already started) or an error occurs. [Rust - `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_multipart_file_v2) + `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_multipart_file_v2) This function can be called multiple times. In that case, each subsequent call will be appended to the existing multipart body as a new part. @@ -6390,7 +6390,7 @@ def with_multipart_file( already started) or an error occurs. [Rust - `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_multipart_file) + `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_multipart_file) * `interaction` - Interaction handle to set the body for. * `part` - Request or response part. @@ -6427,7 +6427,7 @@ def set_key(interaction: InteractionHandle, key: str | None) -> None: Sets the key attribute for the interaction. [Rust - `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_set_key) + `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_key) Args: interaction: @@ -6455,7 +6455,7 @@ def set_pending(interaction: InteractionHandle, *, pending: bool) -> None: Mark the interaction as pending. [Rust - `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_set_pending) + `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_pending) Args: interaction: @@ -6479,7 +6479,7 @@ def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> Add a comment to the interaction. [Rust - `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_set_comment) + `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_comment) Args: interaction: @@ -6513,7 +6513,7 @@ def add_text_comment(interaction: InteractionHandle, comment: str) -> None: Add a text comment to the interaction. [Rust - `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_add_text_comment) + `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_add_text_comment) Args: interaction: @@ -6543,7 +6543,7 @@ def pact_handle_get_async_message_iter(pact: PactHandle) -> PactAsyncMessageIter `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6569,7 +6569,7 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6595,7 +6595,7 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: `pactffi_pact_sync_http_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) + `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) # Safety @@ -6621,7 +6621,7 @@ def pact_handle_write_file( External interface to write out the pact file. [Rust - `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_write_file) + `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_write_file) This function should be called if all the consumer tests have passed. @@ -6665,7 +6665,7 @@ def free_pact_handle(pact: PactHandle) -> None: Delete a Pact handle and free the resources used by it. [Rust - `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_free_pact_handle) + `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_free_pact_handle) Raises: RuntimeError: @@ -6685,7 +6685,7 @@ def verify(args: str) -> int: """ External interface to verifier a provider. - [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verify) + [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verify) * `args` - the same as the CLI interface, except newline delimited @@ -6712,7 +6712,7 @@ def verifier_new_for_application() -> VerifierHandle: Get a Handle to a newly created verifier. [Rust - `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_new_for_application) + `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_new_for_application) """ from pact import __version__ @@ -6727,7 +6727,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None: """ Shutdown the verifier and release all resources. - [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_shutdown) + [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_shutdown) """ lib.pactffi_verifier_shutdown(handle._ref) @@ -6744,7 +6744,7 @@ def verifier_set_provider_info( # noqa: PLR0913 Set the provider details for the Pact verifier. [Rust - `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_provider_info) + `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_provider_info) Args: handle: @@ -6790,7 +6790,7 @@ def verifier_add_provider_transport( Adds a new transport for the given provider. [Rust - `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_add_provider_transport) + `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_provider_transport) Args: handle: @@ -6831,7 +6831,7 @@ def verifier_set_filter_info( Set the filters for the Pact verifier. [Rust - `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_filter_info) + `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_filter_info) Set filters to narrow down the interactions to verify. @@ -6867,7 +6867,7 @@ def verifier_set_provider_state( Set the provider state URL for the Pact verifier. [Rust - `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_provider_state) + `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_provider_state) Args: handle: @@ -6902,7 +6902,7 @@ def verifier_set_verification_options( Set the options used by the verifier when calling the provider. [Rust - `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_verification_options) + `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_verification_options) Args: handle: @@ -6937,7 +6937,7 @@ def verifier_set_coloured_output( Enables or disables coloured output using ANSI escape codes. [Rust - `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_coloured_output) + `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_coloured_output) By default, coloured output is enabled. @@ -6966,7 +6966,7 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> Enables or disables if no pacts are found to verify results in an error. [Rust - `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) + `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) Args: handle: @@ -6999,7 +6999,7 @@ def verifier_set_publish_options( Set the options used when publishing verification results to the Broker. [Rust - `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_publish_options) + `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_publish_options) Args: handle: @@ -7042,7 +7042,7 @@ def verifier_set_consumer_filters( Set the consumer filters for the Pact verifier. [Rust - `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_consumer_filters) + `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_consumer_filters) """ lib.pactffi_verifier_set_consumer_filters( handle._ref, @@ -7060,7 +7060,7 @@ def verifier_add_custom_header( Adds a custom header to be added to the requests made to the provider. [Rust - `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_add_custom_header) + `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_custom_header) """ lib.pactffi_verifier_add_custom_header( handle._ref, @@ -7074,7 +7074,7 @@ def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: Adds a Pact file as a source to verify. [Rust - `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_add_file_source) + `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_file_source) """ lib.pactffi_verifier_add_file_source(handle._ref, file.encode("utf-8")) @@ -7086,7 +7086,7 @@ def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> Non All pacts from the directory that match the provider name will be verified. [Rust - `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_add_directory_source) + `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_directory_source) # Safety @@ -7108,7 +7108,7 @@ def verifier_url_source( Adds a URL as a source to verify. [Rust - `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_url_source) + `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_url_source) Args: handle: @@ -7148,7 +7148,7 @@ def verifier_broker_source( Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_broker_source) + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_broker_source) This will fetch all the pact files from the broker that match the provider name. @@ -7196,7 +7196,7 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) + `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) This will fetch all the pact files from the broker that match the provider name and the consumer version selectors (See [Consumer Version @@ -7263,7 +7263,7 @@ def verifier_execute(handle: VerifierHandle) -> None: """ Runs the verification. - (https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_execute) + (https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_execute) Raises: RuntimeError: @@ -7283,7 +7283,7 @@ def verifier_cli_args() -> str: string. [Rust - `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_cli_args) + `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_cli_args) The purpose is to then be able to use in other languages which wrap the FFI library, to implement the same CLI functionality automatically without @@ -7339,7 +7339,7 @@ def verifier_logs(handle: VerifierHandle) -> OwnedString: Extracts the logs for the verification run. [Rust - `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_logs) + `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_logs) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` @@ -7361,7 +7361,7 @@ def verifier_logs_for_provider(provider_name: str) -> OwnedString: Extracts the logs for the verification run for the provider name. [Rust - `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_logs_for_provider) + `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_logs_for_provider) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` @@ -7383,7 +7383,7 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: Extracts the standard output for the verification run. [Rust - `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_output) + `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_output) Args: handle: @@ -7410,7 +7410,7 @@ def verifier_json(handle: VerifierHandle) -> OwnedString: Extracts the verification result as a JSON document. [Rust - `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_json) + `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_json) Raises: RuntimeError: @@ -7438,7 +7438,7 @@ def using_plugin( otherwise you will have plugin processes left running. [Rust - `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_using_plugin) + `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_using_plugin) Args: pact: @@ -7481,7 +7481,7 @@ def cleanup_plugins(pact: PactHandle) -> None: zero). [Rust - `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_cleanup_plugins) + `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_cleanup_plugins) """ lib.pactffi_cleanup_plugins(pact._ref) @@ -7500,7 +7500,7 @@ def interaction_contents( format of the JSON contents. [Rust - `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_interaction_contents) + `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_interaction_contents) Args: interaction: @@ -7560,7 +7560,7 @@ def matches_string_value( function once it is no longer required. [Rust - `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_string_value) + `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_string_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string @@ -7591,7 +7591,7 @@ def matches_u64_value( function once it is no longer required. [Rust - `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_u64_value) + `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_u64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7621,7 +7621,7 @@ def matches_i64_value( function once it is no longer required. [Rust - `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_i64_value) + `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_i64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7651,7 +7651,7 @@ def matches_f64_value( function once it is no longer required. [Rust - `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_f64_value) + `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_f64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7681,7 +7681,7 @@ def matches_bool_value( function once it is no longer required. [Rust - `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_bool_value) + `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_bool_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get, 0 == false and 1 == true @@ -7713,7 +7713,7 @@ def matches_binary_value( # noqa: PLR0913 function once it is no longer required. [Rust - `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_binary_value) + `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_binary_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7748,7 +7748,7 @@ def matches_json_value( function once it is no longer required. [Rust - `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_json_value) + `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_json_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string diff --git a/src/pact/v3/interaction/_http_interaction.py b/src/pact/v3/interaction/_http_interaction.py index 335d6293c..1f00bf194 100644 --- a/src/pact/v3/interaction/_http_interaction.py +++ b/src/pact/v3/interaction/_http_interaction.py @@ -149,7 +149,7 @@ def with_header( # JSON Matching Pact's matching rules are defined in the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) and support a wide range of matching rules. These can be specified using a JSON object as a strong using `json.dumps(...)`. For example, the above rule whereby the `X-Foo` header has multiple values can be @@ -391,7 +391,7 @@ def with_query_parameter(self, name: str, value: str) -> Self: ``` For more information on the format of the JSON object, see the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). Args: name: From ab519175dc90e301ad4167b5e8dc536fb7c649a2 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 12:16:06 +1000 Subject: [PATCH 0391/1376] docs: minor refinements A couple of minor changes to the docs clarifying the role of `Interaction Part`, especially in the context of asynchronous messages. Signed-off-by: JP-Ellis --- .../v3/interaction/_async_message_interaction.py | 12 +++++++++++- src/pact/v3/interaction/_base.py | 13 +++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/pact/v3/interaction/_async_message_interaction.py b/src/pact/v3/interaction/_async_message_interaction.py index 011853af3..15621650a 100644 --- a/src/pact/v3/interaction/_async_message_interaction.py +++ b/src/pact/v3/interaction/_async_message_interaction.py @@ -33,7 +33,7 @@ def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> Non Args: pact_handle: - Handle for the Pact. + The Pact instance this interaction belongs to. description: Description of the interaction. This must be unique within the @@ -54,4 +54,14 @@ def _handle(self) -> pact.v3.ffi.InteractionHandle: @property def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + """ + Interaction part. + + Where interactions have multiple parts, this property keeps track + of which part is currently being set. + + As this is an asynchronous message interaction, this will always + return a [`REQUEST`][pact.v3.ffi.InteractionPart.REQUEST], as there the + consumer of the message does not send any responses. + """ return pact.v3.ffi.InteractionPart.REQUEST diff --git a/src/pact/v3/interaction/_base.py b/src/pact/v3/interaction/_base.py index d840c4a58..9696f37a3 100644 --- a/src/pact/v3/interaction/_base.py +++ b/src/pact/v3/interaction/_base.py @@ -37,6 +37,19 @@ class Interaction(abc.ABC): - [`HttpInteraction`][pact.v3.interaction.HttpInteraction] - [`AsyncMessageInteraction`][pact.v3.interaction.AsyncMessageInteraction] - [`SyncMessageInteraction`][pact.v3.interaction.SyncMessageInteraction] + + # Interaction Part + + For HTTP and synchronous message interactions, the interaction is split into + two parts: the request and the response. The interaction part is used to + specify which part of the interaction is being set. This is specified using + the `part` argument of various methods (which defaults to an intelligent + choice based on the order of the methods called). + + The asynchronous message interaction does not have parts, as the interaction + contains a single message from the provider (a.ka. the producer of the + message) to the consumer. An attempt to set a response part will raise an + error. """ def __init__(self, description: str) -> None: From d14e2b9477aebfc68fbc0922424bf24844f222ce Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 12:17:36 +1000 Subject: [PATCH 0392/1376] docs(example): clarify purpose of fs interface The previous docstrings did not clearly explain the purpose for the filesystem class. As the examples are intended to be quite pedagogical, the new docstring makes it very clear what it is meant to do/why it exists. Signed-off-by: JP-Ellis --- examples/src/message.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/examples/src/message.py b/examples/src/message.py index cab137ec0..815719903 100644 --- a/examples/src/message.py +++ b/examples/src/message.py @@ -14,7 +14,22 @@ class Filesystem: - """Filesystem interface.""" + """ + Filesystem interface. + + In practice, the handler would process messages and perform some actions on + other systems, whether that be a database, a filesystem, or some other + service. This capability would typically be offered by some library; + however, when running tests, we typically wish to avoid actually interacting + with this external service. + + In order to avoid side effects while testing, the test setup should mock out + the calls to the external service. + + This class provides a simple dummy filesystem interface (which evidently + would fail if actually used), and serves to demonstrate how to mock out + external services when testing. + """ def __init__(self) -> None: """Initialize the filesystem connection.""" From 5a421183404ad697babb494024d43d03d38c31a8 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 12:40:42 +1000 Subject: [PATCH 0393/1376] feat(v3): improve exception types Define a new `PactError` base class, and define new errors to encapsulate verification errors. Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- src/pact/v3/pact.py | 141 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 115 insertions(+), 26 deletions(-) diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index 231c01dd7..58c3c95d5 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -64,6 +64,7 @@ import json import logging +from abc import ABC from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, Set, overload @@ -87,6 +88,120 @@ logger = logging.getLogger(__name__) +class PactError(Exception, ABC): + """ + Base class for exceptions raised by the Pact module. + """ + + +class InteractionVerificationError(PactError): + """ + Exception raised due during the verification of an interaction. + + This error is raised when an error occurs during the manual verification of an + interaction. This is typically raised when the consumer fails to handle the + interaction correctly thereby generating its own exception. The cause of the + error is stored in the `error` attribute. + """ + + def __init__(self, description: str, error: Exception) -> None: + """ + Initialise a new InteractionVerificationError. + + Args: + description: + Description of the interaction that failed verification. + + error: Error that occurred during the verification of the + interaction. + """ + super().__init__(f"Error verifying interaction '{description}': {error}") + self._description = description + self._error = error + + @property + def description(self) -> str: + """ + Description of the interaction that failed verification. + """ + return self._description + + @property + def error(self) -> Exception: + """ + Error that occurred during the verification of the interaction. + """ + return self._error + + +class PactVerificationError(PactError): + """ + Exception raised due to errors in the verification of a Pact. + + This is raised when performing manual verification of the Pact through the + [`verify`][pact.v3.Pact.verify] method: + + ```python + pact = Pact("consumer", "provider") + # Define interactions... + try: + pact.verify(handler, kind="Async") + except PactVerificationError as e: + print(e.errors) + ``` + + All of the errors that occurred during the verification of all of the + interactions are stored in the `errors` attribute. + + This is different from the [`MismatchesError`][pact.v3.MismatchesError] + which is raised when there are mismatches detected by the mock server. + """ + + def __init__(self, errors: list[InteractionVerificationError]) -> None: + """ + Initialise a new PactVerificationError. + + Args: + errors: + Errors that occurred during the verification of the Pact. + """ + super().__init__(f"Error verifying Pact (count: {len(errors)})") + self._errors = errors + + @property + def errors(self) -> list[InteractionVerificationError]: + """ + Errors that occurred during the verification of the Pact. + """ + return self._errors + + +class MismatchesError(PactError): + """ + Exception raised when there are mismatches between the Pact and the server. + """ + + def __init__(self, mismatches: list[dict[str, Any]]) -> None: + """ + Initialise a new MismatchesError. + + Args: + mismatches: + Mismatches between the Pact and the server. + """ + super().__init__(f"Mismatched interaction (count: {len(mismatches)})") + self._mismatches = mismatches + + # TODO: Replace the list of dicts with a more structured object. + # https://github.com/pact-foundation/pact-python/issues/644 + @property + def mismatches(self) -> list[dict[str, Any]]: + """ + Mismatches between the Pact and the server. + """ + return self._mismatches + + class Pact: """ A Pact between a consumer and a provider. @@ -443,32 +558,6 @@ def write_file( ) -class MismatchesError(Exception): - """ - Exception raised when there are mismatches between the Pact and the server. - """ - - def __init__(self, mismatches: list[dict[str, Any]]) -> None: - """ - Initialise a new MismatchesError. - - Args: - mismatches: - Mismatches between the Pact and the server. - """ - super().__init__(f"Mismatched interaction (count: {len(mismatches)})") - self._mismatches = mismatches - - # TODO: Replace the list of dicts with a more structured object. - # https://github.com/pact-foundation/pact-python/issues/644 - @property - def mismatches(self) -> list[dict[str, Any]]: - """ - Mismatches between the Pact and the server. - """ - return self._mismatches - - class PactServer: """ Pact Server. From af0f7adc4baa65d9b7f15b532260f56b39cb78ff Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 12:43:16 +1000 Subject: [PATCH 0394/1376] feat(v3): remove deprecated messages iterator Signed-off-by: JP-Ellis --- src/pact/v3/pact.py | 21 --------------------- tests/v3/test_pact.py | 10 ---------- 2 files changed, 31 deletions(-) diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index 58c3c95d5..ba5bd423c 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -461,27 +461,6 @@ def serve( # noqa: PLR0913 verbose=verbose, ) - def messages(self) -> pact.v3.ffi.PactMessageIterator: - """ - Iterate over the messages in the Pact. - - This function returns an iterator over the messages in the Pact. This - is useful for validating the Pact against the provider. - - ```python - pact = Pact("consumer", "provider") - with pact.serve() as srv: - for message in pact.messages(): - # Validate the message against the provider. - ... - ``` - - Note that the Pact must be written to a file before the messages can be - iterated over. This is because the messages are not stored in memory, - but rather are streamed directly from the file. - """ - return pact.v3.ffi.pact_handle_get_message_iter(self._handle) - @overload def interactions( self, diff --git a/tests/v3/test_pact.py b/tests/v3/test_pact.py index 5f32e7d80..2264346d0 100644 --- a/tests/v3/test_pact.py +++ b/tests/v3/test_pact.py @@ -93,16 +93,6 @@ def test_interactions_iter( raise RuntimeError(msg) -def test_messages(pact: Pact) -> None: - messages = pact.messages() - assert messages is not None - for _message in messages: - # This should be an empty list and therefore the error should never be - # raised. - msg = "Should not be reached" - raise RuntimeError(msg) - - def test_write_file(pact: Pact, temp_dir: Path) -> None: pact.write_file(temp_dir) outfile = temp_dir / "consumer-provider.json" From c2ccb19feabe24403a839a72eb8e9edb8c657b50 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 12:44:43 +1000 Subject: [PATCH 0395/1376] refactor(v3): new interaction iterators Instead of returning the iterators themselves, use `yield from` constructs. This helps for two reasons: 1. Avoids needlessly exposing additional classes which serve no other purpose than to iterate over data. 2. Manages the lifetime of the iterators more clearly, preventing possible issues due to the underlying data mutating during iteration. Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- src/pact/v3/pact.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index ba5bd423c..f9cdbfd77 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -66,7 +66,7 @@ import logging from abc import ABC from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Set, overload +from typing import TYPE_CHECKING, Any, Generator, Literal, Set, overload from yarl import URL @@ -465,27 +465,27 @@ def serve( # noqa: PLR0913 def interactions( self, kind: Literal["HTTP"], - ) -> pact.v3.ffi.PactSyncHttpIterator: ... + ) -> Generator[pact.v3.ffi.SynchronousHttp, None, None]: ... @overload def interactions( self, kind: Literal["Sync"], - ) -> pact.v3.ffi.PactSyncMessageIterator: ... + ) -> Generator[pact.v3.ffi.SynchronousMessage, None, None]: ... @overload def interactions( self, kind: Literal["Async"], - ) -> pact.v3.ffi.PactMessageIterator: ... + ) -> Generator[pact.v3.ffi.AsynchronousMessage, None, None]: ... def interactions( self, - kind: str = "HTTP", + kind: Literal["HTTP", "Sync", "Async"] = "HTTP", ) -> ( - pact.v3.ffi.PactSyncHttpIterator - | pact.v3.ffi.PactSyncMessageIterator - | pact.v3.ffi.PactMessageIterator + Generator[pact.v3.ffi.SynchronousHttp, None, None] + | Generator[pact.v3.ffi.SynchronousMessage, None, None] + | Generator[pact.v3.ffi.AsynchronousMessage, None, None] ): """ Return an iterator over the Pact's interactions. @@ -496,13 +496,15 @@ def interactions( # TODO: Add an iterator for `All` interactions. # https://github.com/pact-foundation/pact-python/issues/451 if kind == "HTTP": - return pact.v3.ffi.pact_handle_get_sync_http_iter(self._handle) - if kind == "Sync": - return pact.v3.ffi.pact_handle_get_sync_message_iter(self._handle) - if kind == "Async": - return pact.v3.ffi.pact_handle_get_message_iter(self._handle) - msg = f"Unknown interaction type: {kind}" - raise ValueError(msg) + yield from pact.v3.ffi.pact_handle_get_sync_http_iter(self._handle) + elif kind == "Sync": + yield from pact.v3.ffi.pact_handle_get_sync_message_iter(self._handle) + elif kind == "Async": + yield from pact.v3.ffi.pact_handle_get_async_message_iter(self._handle) + else: + msg = f"Unknown interaction type: {kind}" + raise ValueError(msg) + return # Ensures that the parent object outlives the generator def write_file( self, From 42654ba634fb5eb0cf646e73d985970dde5834b6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 12:49:01 +1000 Subject: [PATCH 0396/1376] feat(v3): implement message verification As messages abstract away the transport layer, it is necessary for Python to directly handle the messages through some user-defined handler. The `verify` method implements this on the consumer side by verifying that each message can be consumed. Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- src/pact/v3/pact.py | 102 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index f9cdbfd77..1c0dd2fd4 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -64,9 +64,20 @@ import json import logging +import warnings from abc import ABC from pathlib import Path -from typing import TYPE_CHECKING, Any, Generator, Literal, Set, overload +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generator, + List, + Literal, + Set, + overload, +) from yarl import URL @@ -506,6 +517,95 @@ def interactions( raise ValueError(msg) return # Ensures that the parent object outlives the generator + @overload + def verify( + self, + handler: Callable[[str | bytes | None, Dict[str, str]], None], + kind: Literal["Async", "Sync"], + *, + raises: Literal[True] = True, + ) -> None: ... + @overload + def verify( + self, + handler: Callable[[str | bytes | None, Dict[str, str]], None], + kind: Literal["Async", "Sync"], + *, + raises: Literal[False], + ) -> List[InteractionVerificationError]: ... + + def verify( + self, + handler: Callable[[str | bytes | None, Dict[str, str]], None], + kind: Literal["Async", "Sync"], + *, + raises: bool = True, + ) -> List[InteractionVerificationError] | None: + """ + Verify message interactions. + + This function is used to ensure that the consumer is able to handle the + messages that are defined in the Pact. The `handler` function is called + for each message in the Pact. + + The end-user is responsible for defining the `handler` function and + verifying that the messages are handled correctly. For example, if the + handler is meant to call an API, then the API call should be mocked out + and once the verification is complete, the mock should be verified. Any + exceptions raised by the handler will be caught and reported as + mismatches. + + Args: + handler: + The function that will be called for each message in the Pact. + + The first argument to the function is the message body, either as + a string or byte array. + + The second argument is the metadata for the message. If there + is no metadata, then this will be an empty dictionary. + + kind: + The type of message interaction. This must be one of `Async` + or `Sync`. + + raises: + Whether or not to raise an exception if the handler fails to + process a message. If set to `False`, then the function will + return a list of errors. + """ + errors: List[InteractionVerificationError] = [] + for message in self.interactions(kind): + request: pact.v3.ffi.MessageContents | None = None + if isinstance(message, pact.v3.ffi.SynchronousMessage): + request = message.request_contents + elif isinstance(message, pact.v3.ffi.AsynchronousMessage): + request = message.contents + else: + msg = f"Unknown message type: {type(message).__name__}" + raise TypeError(msg) + + if request is None: + warnings.warn( + f"Message '{message.description}' has no contents", + stacklevel=2, + ) + continue + + body = request.contents + metadata = {pair.key: pair.value for pair in request.metadata} + + try: + handler(body, metadata) + except Exception as e: # noqa: BLE001 + errors.append(InteractionVerificationError(message.description, e)) + + if raises: + if errors: + raise PactVerificationError(errors) + return None + return errors + def write_file( self, directory: Path | str | None = None, From 0e4a1740a5656c02ea7073a54b154f22f8c3bd2a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 12:50:48 +1000 Subject: [PATCH 0397/1376] chore(tests): implement v3/v4 consumer message compatibility suite Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- .../compatibility_suite/test_v3_consumer.py | 14 +- .../test_v3_message_consumer.py | 663 ++++++++++++++++++ .../compatibility_suite/test_v4_consumer.py | 21 +- .../test_v4_message_consumer.py | 148 ++++ tests/v3/compatibility_suite/util/__init__.py | 30 +- tests/v3/compatibility_suite/util/consumer.py | 39 +- 6 files changed, 891 insertions(+), 24 deletions(-) create mode 100644 tests/v3/compatibility_suite/test_v3_message_consumer.py create mode 100644 tests/v3/compatibility_suite/test_v4_message_consumer.py diff --git a/tests/v3/compatibility_suite/test_v3_consumer.py b/tests/v3/compatibility_suite/test_v3_consumer.py index b7014d6f7..f2f218a3f 100644 --- a/tests/v3/compatibility_suite/test_v3_consumer.py +++ b/tests/v3/compatibility_suite/test_v3_consumer.py @@ -10,7 +10,7 @@ from pytest_bdd import given, parsers, scenario, then from pact.v3.pact import HttpInteraction, Pact -from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util import PactInteractionTuple, parse_markdown_table from tests.v3.compatibility_suite.util.consumer import ( the_pact_file_for_the_test_is_generated, ) @@ -48,21 +48,21 @@ def test_supports_data_for_provider_states() -> None: target_fixture="pact_interaction", ) def an_integration_is_being_defined_for_a_consumer_test() -> ( - Generator[tuple[Pact, HttpInteraction], Any, None] + Generator[PactInteractionTuple[HttpInteraction], Any, None] ): """An integration is being defined for a consumer test.""" pact = Pact("consumer", "provider") pact.with_specification("V3") - yield (pact, pact.upon_receiving("a request")) + yield PactInteractionTuple(pact, pact.upon_receiving("a request")) @given(parsers.re(r'a provider state "(?P[^"]+)" is specified')) def a_provider_state_is_specified( - pact_interaction: tuple[Pact, HttpInteraction], + pact_interaction: PactInteractionTuple[HttpInteraction], state: str, ) -> None: """A provider state is specified.""" - pact_interaction[1].given(state) + pact_interaction.interaction.given(state) @given( @@ -74,7 +74,7 @@ def a_provider_state_is_specified( converters={"table": parse_markdown_table}, ) def a_provider_state_is_specified_with_the_following_data( - pact_interaction: tuple[Pact, HttpInteraction], + pact_interaction: PactInteractionTuple[HttpInteraction], state: str, table: list[dict[str, Any]], ) -> None: @@ -93,7 +93,7 @@ def a_provider_state_is_specified_with_the_following_data( elif value.replace(".", "", 1).isdigit(): row[key] = float(value) - pact_interaction[1].given(state, parameters=table[0]) + pact_interaction.interaction.given(state, parameters=table[0]) ################################################################################ diff --git a/tests/v3/compatibility_suite/test_v3_message_consumer.py b/tests/v3/compatibility_suite/test_v3_message_consumer.py new file mode 100644 index 000000000..bdb038368 --- /dev/null +++ b/tests/v3/compatibility_suite/test_v3_message_consumer.py @@ -0,0 +1,663 @@ +"""V3 Message consumer feature tests.""" + +from __future__ import annotations + +import ast +import json +import logging +import re +from typing import TYPE_CHECKING, Any, List, NamedTuple + +from pytest_bdd import ( + given, + parsers, + scenario, + then, + when, +) + +from tests.v3.compatibility_suite.util import ( + FIXTURES_ROOT, + PactInteractionTuple, + parse_markdown_table, +) +from tests.v3.compatibility_suite.util.consumer import ( + a_message_integration_is_being_defined_for_a_consumer_test, +) + +if TYPE_CHECKING: + from pathlib import Path + + from pact.v3.pact import AsyncMessageInteraction, InteractionVerificationError + +logger = logging.getLogger(__name__) + +################################################################################ +## Helpers +################################################################################ + + +class ReceivedMessage(NamedTuple): + """Holder class for Message Received Payload.""" + + body: Any + context: Any + + +class PactResult(NamedTuple): + """Holder class for Pact Result objects.""" + + messages: List[ReceivedMessage] + pact_data: dict[str, Any] | None + errors: List[InteractionVerificationError] + + +def assert_type(expected_type: str, value: Any) -> None: # noqa: ANN401 + logger.debug("Ensuring that %s is of type %s", value, expected_type) + if expected_type == "integer": + assert value is not None + assert isinstance(value, int) or re.match(r"^\d+$", value) + else: + msg = f"Unknown type: {expected_type}" + raise ValueError(msg) + + +################################################################################ +## Scenarios +################################################################################ + + +@scenario( + "definition/features/V3/message_consumer.feature", + "When all messages are successfully processed", +) +def test_when_all_messages_are_successfully_processed() -> None: + """When all messages are successfully processed.""" + + +@scenario( + "definition/features/V3/message_consumer.feature", + "When not all messages are successfully processed", +) +def test_when_not_all_messages_are_successfully_processed() -> None: + """When not all messages are successfully processed.""" + + +@scenario( + "definition/features/V3/message_consumer.feature", + "Supports arbitrary message metadata", +) +def test_supports_arbitrary_message_metadata() -> None: + """Supports arbitrary message metadata.""" + + +@scenario( + "definition/features/V3/message_consumer.feature", + "Supports specifying provider states", +) +def test_supports_specifying_provider_states() -> None: + """Supports specifying provider states.""" + + +@scenario( + "definition/features/V3/message_consumer.feature", + "Supports data for provider states", +) +def test_supports_data_for_provider_states() -> None: + """Supports data for provider states.""" + + +@scenario( + "definition/features/V3/message_consumer.feature", + "Supports the use of generators with the message body", +) +def test_supports_the_use_of_generators_with_the_message_body() -> None: + """Supports the use of generators with the message body.""" + + +@scenario( + "definition/features/V3/message_consumer.feature", + "Supports the use of generators with message metadata", +) +def test_supports_the_use_of_generators_with_message_metadata() -> None: + """Supports the use of generators with message metadata.""" + + +################################################################################ +## Given +################################################################################ + + +a_message_integration_is_being_defined_for_a_consumer_test("V3") + + +@given( + parsers.re( + r'a provider state "(?P[^"]+)" for the message is specified' + r"( with the following data:\n)?(?P.*)", + re.DOTALL, + ), + converters={"table": lambda v: parse_markdown_table(v) if v else None}, +) +def a_provider_state_for_the_message_is_specified_with_the_following_data( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + state: str, + table: list[dict[str, str]] | None, +) -> None: + """A provider state for the message is specified with the following data.""" + logger.debug("Specifying provider state '%s': %s", state, table) + if table: + parameters = {k: ast.literal_eval(v) for k, v in table[0].items()} + pact_interaction.interaction.given(state, parameters=parameters) + else: + pact_interaction.interaction.given(state) + + +@given("a message is defined") +def a_message_is_defined() -> None: + """A message is defined.""" + + +@given( + parsers.re( + r"the message contains the following metadata:\n(?P
.+)", + re.DOTALL, + ), + converters={"table": parse_markdown_table}, +) +def the_message_contains_the_following_metadata( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + table: list[dict[str, Any]], +) -> None: + """The message contains the following metadata.""" + logger.debug("Adding metadata to message: %s", table) + for metadata in table: + if metadata.get("value", "").startswith("JSON: "): + metadata["value"] = metadata["value"].replace("JSON:", "") + pact_interaction.interaction.with_metadata({metadata["key"]: metadata["value"]}) + + +@given( + parsers.re( + r"the message is configured with the following:\n(?P
.+)", + re.DOTALL, + ), + converters={"table": parse_markdown_table}, +) +def the_message_is_configured_with_the_following( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + table: list[dict[str, Any]], +) -> None: + """The message is configured with the following.""" + assert len(table) == 1, "Only one row is expected" + config: dict[str, str] = table[0] + + if body := config.pop("body", None): + if body.startswith("file: "): + file = FIXTURES_ROOT / body.replace("file: ", "") + content_type = "application/json" if file.suffix == ".json" else None + pact_interaction.interaction.with_body(file.read_text(), content_type) + else: + msg = f"Unsupported body configuration: {config['body']}" + raise ValueError(msg) + + if generators := config.pop("generators", None): + if generators.startswith("JSON: "): + data = json.loads(generators.replace("JSON: ", "")) + pact_interaction.interaction.with_generators(data) + else: + file = FIXTURES_ROOT / generators + pact_interaction.interaction.with_generators(file.read_text()) + + if metadata := config.pop("metadata", None): + data = json.loads(metadata) + pact_interaction.interaction.with_metadata({ + k: json.dumps(v) for k, v in data.items() + }) + + if config: + msg = f"Unknown configuration keys: {', '.join(config.keys())}" + raise ValueError(msg) + + +@given( + parsers.re(r'the message payload contains the "(?P[^"]+)" JSON document') +) +def the_message_payload_contains_the_basic_json_document( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + basename: str, +) -> None: + """The message payload contains the "basic" JSON document.""" + json_path = FIXTURES_ROOT / f"{basename}.json" + if not json_path.is_file(): + msg = f"File not found: {json_path}" + raise FileNotFoundError(msg) + pact_interaction.interaction.with_body( + json_path.read_text(), + content_type="application/json", + ) + + +################################################################################ +## When +################################################################################ + + +@when("the message is successfully processed", target_fixture="pact_result") +def the_message_is_successfully_processed( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + temp_dir: Path, +) -> PactResult: + """The message is successfully processed.""" + messages: list[ReceivedMessage] = [] + + def handler( + body: str | bytes | None, + context: dict[str, str], + ) -> None: + messages.append(ReceivedMessage(body, context)) + + # While the expectation is that the message will be processed successfully, + # we don't raise an exception and instead capture any errors that occur. + errors = pact_interaction.pact.verify(handler, "Async", raises=False) + if errors: + logger.error("%d errors occured during verification:", len(errors)) + for error in errors: + logger.error(error) + msg = "Errors occurred during verification" + raise AssertionError(msg) + + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact_interaction.pact.write_file(temp_dir / "pacts") + with (temp_dir / "pacts" / "consumer-provider.json").open() as file: + pact_data = json.load(file) + + return PactResult(messages, pact_data, errors) + + +@when( + parsers.re( + r"the message is NOT successfully processed " + r'with a "(?P[^"]+)" exception' + ), + target_fixture="pact_result", +) +def the_message_is_not_successfully_processed_with_an_exception( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + failure: str, +) -> PactResult: + """The message is NOT successfully processed with a "Test failed" exception.""" + messages: list[ReceivedMessage] = [] + + def handler(body: str | bytes | None, context: dict[str, str]) -> None: + messages.append(ReceivedMessage(body, context)) + raise AssertionError(failure) + + errors = pact_interaction.pact.verify(handler, "Async", raises=False) + return PactResult(messages, None, errors) + + +################################################################################ +## Then +################################################################################ + + +@then( + parsers.re( + r"a Pact file for the message interaction " + r"will(?P( NOT)?) have been written" + ), + converters={"success": lambda x: x != " NOT"}, +) +def a_pact_file_for_the_message_interaction_will_maybe_have_been_written( + temp_dir: Path, + success: bool, # noqa: FBT001 +) -> None: + """A Pact file for the message interaction will maybe have been written.""" + assert (temp_dir / "pacts" / "consumer-provider.json").exists() == success + + +@then(parsers.re(r'the consumer test error will be "(?P[^"]+)"')) +def the_consumer_test_error_will_be_test_failed( + pact_result: PactResult, + error: str, +) -> None: + """The consumer test error will be "Test failed".""" + assert len(pact_result.errors) == 1 + assert error in str(pact_result.errors[0].error) + + +@then( + parsers.re(r"the consumer test will have (?Ppassed|failed)"), + converters={"success": lambda x: x == "passed"}, +) +def the_consumer_test_will_have_passed_or_failed( + pact_result: PactResult, + success: bool, # noqa: FBT001 +) -> None: + """The consumer test will have passed or failed.""" + assert (len(pact_result.errors) == 0) == success + + +@then( + parsers.re( + r"the first message in the pact file content type " + r'will be "(?P[^"]+)"' + ) +) +def the_first_message_in_the_pact_file_content_type_will_be( + pact_result: PactResult, + content_type: str, +) -> None: + """The first message in the pact file content type will be.""" + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages: list[dict[str, dict[str, Any]]] = pact_result.pact_data["messages"] + if not isinstance(messages, list) or not messages: + msg = "No messages found" + raise RuntimeError(msg) + assert messages[0].get("metadata", {}).get("contentType") == content_type + + +@then( + parsers.re( + r"the first message in the pact file will contain " + r"(?P\d+) provider states?" + ), + converters={"state_count": int}, +) +def the_first_message_in_the_pact_file_will_contain( + pact_result: PactResult, + state_count: int, +) -> None: + """The first message in the pact file will contain 1 provider state.""" + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages: list[dict[str, list[Any]]] = pact_result.pact_data["messages"] + if not isinstance(messages, list) or not messages: + msg = "No messages found" + raise RuntimeError(msg) + assert len(messages[0].get("providerStates", [])) == state_count + + +@then( + parsers.re( + r"the first message in the Pact file will contain " + r'provider state "(?P[^"]+)"' + ) +) +def the_first_message_in_the_pact_file_will_contain_provider_state( + pact_result: PactResult, + state: str, +) -> None: + """The first message in the Pact file will contain provider state.""" + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages = pact_result.pact_data["messages"] + if not isinstance(messages, list) or not messages: + msg = "No messages found" + raise RuntimeError(msg) + message: dict[str, Any] = messages[0] + provider_states: list[dict[str, Any]] = message.get("providerStates", []) + for provider_state in provider_states: + if provider_state["name"] == state: + break + else: + msg = f"Provider state not found: {state}" + raise AssertionError(msg) + + +@then( + parsers.re( + r"the first message in the pact file will contain " + r'the "(?P[^"]+)" document' + ) +) +def the_first_message_in_the_pact_file_will_contain_the_basic_json_document( + pact_result: PactResult, + basename: str, +) -> None: + """The first message in the pact file will contain the "basic.json" document.""" + path = FIXTURES_ROOT / basename + if not path.is_file(): + msg = f"File not found: {path}" + raise FileNotFoundError(msg) + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages: list[dict[str, Any]] = pact_result.pact_data["messages"] + if not isinstance(messages, list) or not messages: + msg = "No messages found" + raise RuntimeError(msg) + try: + assert messages[0]["contents"] == json.loads(path.read_text()) + except json.JSONDecodeError as e: + logger.info("Error decoding JSON: %s", e) + logger.info("Performing basic string comparison") + assert messages[0]["contents"] == path.read_text() + + +@then( + parsers.re( + r"the first message in the pact file will contain " + r'the message metadata "(?P[^"]+)" == "(?P[^"\\]*(?:\\.[^"\\]*)*)"' + ) +) +def the_first_message_in_the_pact_file_will_contain_the_message_metadata( + pact_result: PactResult, + key: str, + value: Any, # noqa: ANN401 +) -> None: + """The first message in the pact file will contain the message metadata.""" + if value.startswith("JSON: "): + value = value.replace("JSON: ", "") + value = value.replace('\\"', '"') + value = json.loads(value) + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages: list[dict[str, dict[str, Any]]] = pact_result.pact_data["messages"] + assert messages[0]["metadata"][key] == value + + +@then( + parsers.re( + r'the message contents for "(?P[^"]+)" ' + r'will have been replaced with an? "(?P[^"]+)"' + ) +) +def the_message_contents_will_have_been_replaced_with( + pact_result: PactResult, + path: str, + expected_type: str, +) -> None: + """The message contents for "$.one" will have been replaced with an "integer".""" + json_path = path.split(".") + assert len(json_path) == 2, "Only one level of nesting is supported" + assert json_path[0] == "$", "Only root level replacement is supported" + key = json_path[1] + + assert len(pact_result.messages) == 1 + message = pact_result.messages[0] + value = json.loads(message.body).get(key) + assert_type(expected_type, value) + + +@then( + parsers.parse( + "the pact file will contain {interaction_count:d} message interaction" + ) +) +def the_pact_file_will_contain_message_interaction( + pact_result: PactResult, + interaction_count: int, +) -> None: + """The pact file will contain N message interaction.""" + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages: list[Any] = pact_result.pact_data["messages"] + assert len(messages) == interaction_count + + +@then( + parsers.re( + r'the provider state "(?P[^"]+)" for the message ' + r"will contain the following parameters:\n(?P
.+)", + re.DOTALL, + ), + converters={"table": parse_markdown_table}, +) +def the_provider_state_for_the_message_will_contain_the_following_parameters( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + pact_result: PactResult, + state: str, + table: list[dict[str, Any]], +) -> None: + """The provider state for the message will contain the following parameters.""" + assert len(table) == 1, "Only one row is expected" + expected = json.loads(table[0]["parameters"]) + + # It is unclear whether this test is meant to verify the `Interaction` + # object, or the result as written to the Pact file. As a result, we + # will perform both checks. + + ## Verifying the Pact File + + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages: list[dict[str, list[dict[str, Any]]]] = pact_result.pact_data["messages"] + assert len(messages) == 1, "Only one message is expected" + message = messages[0] + + assert len(message["providerStates"]) > 0, "At least one provider state is expected" + provider_states = message["providerStates"] + for provider_state_dict in provider_states: + if provider_state_dict["name"] == state: + assert expected == provider_state_dict["params"] + break + else: + msg = f"Provider state not found in Pact file: {state}" + raise AssertionError(msg) + + ## Verifying the Interaction Object + + for interaction in pact_interaction.pact.interactions("Async"): + for provider_state in interaction.provider_states(): + if provider_state.name == state: + provider_state_params = { + k: ast.literal_eval(v) for k, v in provider_state.parameters() + } + assert expected == provider_state_params + break + else: + msg = f"Provider state not found: {state}" + raise ValueError(msg) + break + else: + msg = "No interactions found" + raise ValueError(msg) + + +@then( + parsers.re(r'the received message content type will be "(?P[^"]+)"') +) +def the_received_message_content_type_will_be( + pact_result: PactResult, + content_type: str, +) -> None: + """The received message content type will be "application/json".""" + assert len(pact_result.messages) == 1 + message = pact_result.messages[0] + assert message.context.get("contentType") == content_type + + +@then( + parsers.re( + r"the received message metadata will contain " + r'"(?P[^"]+)" == "(?P[^"\\]*(?:\\.[^"\\]*)*)"' + ) +) +def the_received_message_metadata_will_contain( + pact_result: PactResult, + key: str, + value: Any, # noqa: ANN401 +) -> None: + """The received message metadata will contain.""" + # If we're given some JSON value, we will need to parse the value from the + # `message.context` and compare it to the parsed JSON value; otherwise, + # equivalent JSON values may not match due to formatting differences. + json_matching = False + if value.startswith("JSON: "): + value = value.replace("JSON: ", "").replace(r"\"", '"') + value = json.loads(value) + json_matching = True + + assert len(pact_result.messages) == 1 + message = pact_result.messages[0] + for k, v in message.context.items(): + if k == key: + if json_matching: + assert json.loads(v) == value + else: + assert v == value + break + else: + msg = f"Key '{key}' not found in message metadata" + raise AssertionError(msg) + + +@then( + parsers.re( + r'the received message metadata will contain "(?P[^"]+)" ' + r'replaced with an? "(?P[^"]+)"' + ) +) +def the_received_message_metadata_will_contain_replaced_with( + pact_result: PactResult, + key: str, + expected_type: str, +) -> None: + """The received message metadata will contain "ID" replaced with an "integer".""" + assert isinstance(pact_result.messages, list) + assert len(pact_result.messages) == 1, "Only one message is expected" + message = pact_result.messages[0] + value = message.context.get(key) + assert_type(expected_type, value) + + +@then( + parsers.re( + r"the received message payload will contain " + r'the "(?P[^"]+)" JSON document' + ) +) +def the_received_message_payload_will_contain_the_basic_json_document( + pact_result: PactResult, + basename: str, +) -> None: + """The received message payload will contain the JSON document.""" + json_path = FIXTURES_ROOT / f"{basename}.json" + if not json_path.is_file(): + msg = f"File not found: {json_path}" + raise FileNotFoundError(msg) + + assert len(pact_result.messages) == 1 + message = pact_result.messages[0] + + try: + assert json.loads(message.body) == json.loads(json_path.read_text()) + except json.JSONDecodeError as e: + logger.info("Error decoding JSON: %s", e) + logger.info("Performing basic comparison") + if isinstance(message.body, str): + assert message.body == json_path.read_text() + elif isinstance(message.body, bytes): + assert message.body == json_path.read_bytes() + else: + msg = f"Unexpected message body type: {type(message.body).__name__}" + raise TypeError(msg) from None diff --git a/tests/v3/compatibility_suite/test_v4_consumer.py b/tests/v3/compatibility_suite/test_v4_consumer.py index 7b7ab019d..de70ea6f0 100644 --- a/tests/v3/compatibility_suite/test_v4_consumer.py +++ b/tests/v3/compatibility_suite/test_v4_consumer.py @@ -9,7 +9,7 @@ from pytest_bdd import given, parsers, scenario, then from pact.v3.pact import HttpInteraction, Pact -from tests.v3.compatibility_suite.util import string_to_int +from tests.v3.compatibility_suite.util import PactInteractionTuple, string_to_int from tests.v3.compatibility_suite.util.consumer import ( the_pact_file_for_the_test_is_generated, ) @@ -63,41 +63,38 @@ def test_supports_adding_comments() -> None: target_fixture="pact_interaction", ) def an_http_interaction_is_being_defined_for_a_consumer_test() -> ( - Generator[tuple[Pact, HttpInteraction], Any, None] + Generator[PactInteractionTuple[HttpInteraction], Any, None] ): """An HTTP interaction is being defined for a consumer test.""" pact = Pact("consumer", "provider") pact.with_specification("V4") - yield (pact, pact.upon_receiving("a request")) + yield PactInteractionTuple(pact, pact.upon_receiving("a request")) @given(parsers.re(r'a key of "(?P[^"]+)" is specified for the HTTP interaction')) def a_key_is_specified_for_the_http_interaction( - pact_interaction: tuple[Pact, HttpInteraction], + pact_interaction: PactInteractionTuple[HttpInteraction], key: str, ) -> None: """A key is specified for the HTTP interaction.""" - _, interaction = pact_interaction - interaction.set_key(key) + pact_interaction.interaction.set_key(key) @given("the HTTP interaction is marked as pending") def the_http_interaction_is_marked_as_pending( - pact_interaction: tuple[Pact, HttpInteraction], + pact_interaction: PactInteractionTuple[HttpInteraction], ) -> None: """The HTTP interaction is marked as pending.""" - _, interaction = pact_interaction - interaction.set_pending(pending=True) + pact_interaction.interaction.set_pending(pending=True) @given(parsers.re(r'a comment "(?P[^"]+)" is added to the HTTP interaction')) def a_comment_is_added_to_the_http_interaction( - pact_interaction: tuple[Pact, HttpInteraction], + pact_interaction: PactInteractionTuple[HttpInteraction], comment: str, ) -> None: """A comment of "" is added to the HTTP interaction.""" - _, interaction = pact_interaction - interaction.set_comment("text", [comment]) + pact_interaction.interaction.set_comment("text", [comment]) ################################################################################ diff --git a/tests/v3/compatibility_suite/test_v4_message_consumer.py b/tests/v3/compatibility_suite/test_v4_message_consumer.py new file mode 100644 index 000000000..5b1332165 --- /dev/null +++ b/tests/v3/compatibility_suite/test_v4_message_consumer.py @@ -0,0 +1,148 @@ +"""Message consumer feature tests.""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING, Any + +from pytest_bdd import ( + given, + parsers, + scenario, + then, +) + +from tests.v3.compatibility_suite.util import PactInteractionTuple, string_to_int +from tests.v3.compatibility_suite.util.consumer import ( + a_message_integration_is_being_defined_for_a_consumer_test, + the_pact_file_for_the_test_is_generated, +) + +if TYPE_CHECKING: + from pact.v3.pact import AsyncMessageInteraction + + +@scenario( + "definition/features/V4/message_consumer.feature", + "Sets the type for the interaction", +) +def test_sets_the_type_for_the_interaction() -> None: + """Sets the type for the interaction.""" + + +@scenario( + "definition/features/V4/message_consumer.feature", + "Supports specifying a key for the interaction", +) +def test_supports_specifying_a_key_for_the_interaction() -> None: + """Supports specifying a key for the interaction.""" + + +@scenario( + "definition/features/V4/message_consumer.feature", + "Supports specifying the interaction is pending", +) +def test_supports_specifying_the_interaction_is_pending() -> None: + """Supports specifying the interaction is pending.""" + + +@scenario( + "definition/features/V4/message_consumer.feature", + "Supports adding comments", +) +def test_supports_adding_comments() -> None: + """Supports adding comments.""" + + +################################################################################ +## Given +################################################################################ + +a_message_integration_is_being_defined_for_a_consumer_test("V4") + + +@given( + parsers.re(r'a comment "(?P[^"]+)" is added to the message interaction') +) +def a_comment_is_added_to_the_message_interaction( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + comment: str, +) -> None: + """A comment "{comment}" is added to the message interaction.""" + pact_interaction.interaction.add_text_comment(comment) + + +@given( + parsers.re(r'a key of "(?P[^"]+)" is specified for the message interaction') +) +def a_key_is_specified_for_the_message_interaction( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + key: str, +) -> None: + """A key is specified for the HTTP interaction.""" + pact_interaction.interaction.set_key(key) + + +@given("the message interaction is marked as pending") +def the_message_interaction_is_marked_as_pending( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], +) -> None: + """The message interaction is marked as pending.""" + pact_interaction.interaction.set_pending(pending=True) + + +################################################################################ +## When +################################################################################ + + +the_pact_file_for_the_test_is_generated() + + +################################################################################ +## Then +################################################################################ + + +@then( + parsers.re( + r"the (?P[^ ]+) interaction in the Pact file" + r" will have \"(?P[^\"]+)\" = '(?P[^']+)'" + ), + converters={"num": string_to_int}, +) +def the_interaction_in_the_pact_file_will_have_a_key_of( + pact_data: dict[str, Any], + num: int, + key: str, + value: str, +) -> None: + """The interaction in the Pact file will have a key of value.""" + assert "interactions" in pact_data + assert len(pact_data["interactions"]) >= num + interaction = pact_data["interactions"][num - 1] + assert key in interaction + value = json.loads(value) + if isinstance(value, list): + assert interaction[key] in value + else: + assert interaction[key] == value + + +@then( + parsers.re( + r"the (?P[^ ]+) interaction in the Pact file" + r' will have a type of "(?P[^"]+)"' + ), + converters={"num": string_to_int}, +) +def the_interaction_in_the_pact_file_will_container_provider_states( + pact_data: dict[str, Any], + num: int, + interaction_type: str, +) -> None: + """The interaction in the Pact file will container provider states.""" + assert "interactions" in pact_data + assert len(pact_data["interactions"]) >= num + interaction = pact_data["interactions"][num - 1] + assert interaction["type"] == interaction_type diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 1351b1146..91b5871d1 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -31,7 +31,7 @@ def _(): from collections.abc import Collection, Mapping from datetime import date, datetime, time from pathlib import Path -from typing import Any +from typing import Any, Generic, TypeVar from xml.etree import ElementTree import flask @@ -47,6 +47,34 @@ def _(): SUITE_ROOT = Path(__file__).parent.parent / "definition" FIXTURES_ROOT = SUITE_ROOT / "fixtures" +_T = TypeVar("_T") + + +class PactInteractionTuple(Generic[_T]): + """ + Pact and interaction tuple. + + A number of steps in the compatibility suite require one or both of a `Pact` + and an `Interaction` subclass. This named tuple is used to pass these + objects around more easily. + + !!! note + + This should be simplified in the future to simply being a + [`NamedTuple`][typing.NamedTuple]; however, earlier versions of Python + do not support inheriting from multiple classes, thereby preventing + `class PactInteractionTuple(NamedTuple, Generic[_T])` (even if + [`Generic[_T]`][typing.Generic] serves no purpose other than to allow + type hinting). + """ + + def __init__(self, pact: Pact, interaction: _T) -> None: + """ + Instantiate the tuple. + """ + self.pact = pact + self.interaction = interaction + def string_to_int(word: str) -> int: """ diff --git a/tests/v3/compatibility_suite/util/consumer.py b/tests/v3/compatibility_suite/util/consumer.py index f9834187d..38bf7676e 100644 --- a/tests/v3/compatibility_suite/util/consumer.py +++ b/tests/v3/compatibility_suite/util/consumer.py @@ -11,12 +11,13 @@ import pytest import requests -from pytest_bdd import parsers, then, when +from pytest_bdd import given, parsers, then, when from yarl import URL from pact.v3 import Pact from tests.v3.compatibility_suite.util import ( FIXTURES_ROOT, + PactInteractionTuple, parse_markdown_table, string_to_int, truncate, @@ -26,11 +27,41 @@ from collections.abc import Generator from pathlib import Path - from pact.v3.pact import HttpInteraction, PactServer + from pact.v3.interaction._async_message_interaction import AsyncMessageInteraction + from pact.v3.pact import PactServer from tests.v3.compatibility_suite.util import InteractionDefinition logger = logging.getLogger(__name__) +################################################################################ +## Given +################################################################################ + + +def a_message_integration_is_being_defined_for_a_consumer_test( + version: str, + stacklevel: int = 1, +) -> None: + @given( + parsers.re( + r"a message (integration|interaction) " + r"is being defined for a consumer test" + ), + target_fixture="pact_interaction", + stacklevel=stacklevel + 1, + ) + def _() -> PactInteractionTuple[AsyncMessageInteraction]: + """ + A message integration is being defined for a consumer test. + """ + pact = Pact("consumer", "provider") + pact.with_specification(version) + return PactInteractionTuple( + pact, + pact.upon_receiving("an asynchronous message", "Async"), + ) + + ################################################################################ ## When ################################################################################ @@ -201,10 +232,10 @@ def the_pact_file_for_the_test_is_generated(stacklevel: int = 1) -> None: ) def _( temp_dir: Path, - pact_interaction: tuple[Pact, HttpInteraction], + pact_interaction: PactInteractionTuple[Any], ) -> dict[str, Any]: """The Pact file for the test is generated.""" - pact_interaction[0].write_file(temp_dir) + pact_interaction.pact.write_file(temp_dir) with (temp_dir / "consumer-provider.json").open("r") as file: return json.load(file) From 73f6599143d0e6a5461625ba5a08d63ec9c8c66c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 24 Jun 2024 12:51:30 +1000 Subject: [PATCH 0398/1376] chore(examples): add v3 message consumer examples This change also includes a minor fix to ensure that the server is fully started before the test proceeds. Co-authored-by: valkolovos Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- .gitignore | 3 + examples/docker-compose.yml | 4 +- examples/tests/test_01_provider_fastapi.py | 2 + examples/tests/test_01_provider_flask.py | 2 + examples/tests/v3/__init__.py | 0 examples/tests/v3/test_01_message_consumer.py | 170 ++++++++++++++++++ pyproject.toml | 2 + 7 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 examples/tests/v3/__init__.py create mode 100644 examples/tests/v3/test_01_message_consumer.py diff --git a/.gitignore b/.gitignore index 763596319..5ed2af480 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ src/pact/bin src/pact/data +# Test outputs +examples/tests/pacts + # Version is determined from the VCS src/pact/__version__.py diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml index 2405a49a9..1b39fa089 100644 --- a/examples/docker-compose.yml +++ b/examples/docker-compose.yml @@ -15,7 +15,8 @@ services: broker: image: pactfoundation/pact-broker:latest-multi depends_on: - - postgres + postgres: + condition: service_healthy ports: - "9292:9292" restart: always @@ -41,3 +42,4 @@ services: interval: 1s timeout: 2s retries: 5 + start_period: 30s diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/tests/test_01_provider_fastapi.py index 7ccd3128a..a95b5b5f8 100644 --- a/examples/tests/test_01_provider_fastapi.py +++ b/examples/tests/test_01_provider_fastapi.py @@ -24,6 +24,7 @@ from __future__ import annotations +import time from multiprocessing import Process from typing import Any, Dict, Generator, Union from unittest.mock import MagicMock @@ -93,6 +94,7 @@ def verifier() -> Generator[Verifier, Any, None]: provider_base_url=str(PROVIDER_URL), ) proc.start() + time.sleep(2) yield verifier proc.kill() diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py index ba5c39d43..b7082dabb 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/tests/test_01_provider_flask.py @@ -24,6 +24,7 @@ from __future__ import annotations +import time from multiprocessing import Process from typing import Any, Dict, Generator, Union from unittest.mock import MagicMock @@ -81,6 +82,7 @@ def verifier() -> Generator[Verifier, Any, None]: provider_base_url=str(PROVIDER_URL), ) proc.start() + time.sleep(2) yield verifier proc.kill() diff --git a/examples/tests/v3/__init__.py b/examples/tests/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/tests/v3/test_01_message_consumer.py b/examples/tests/v3/test_01_message_consumer.py new file mode 100644 index 000000000..9016be9b8 --- /dev/null +++ b/examples/tests/v3/test_01_message_consumer.py @@ -0,0 +1,170 @@ +""" +Consumer test of example message handler using the v3 API. + +This test will create a pact between the message handler +and the message provider. +""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generator, +) +from unittest.mock import MagicMock + +import pytest + +from examples.src.message import Handler +from pact.v3.pact import Pact + +if TYPE_CHECKING: + from collections.abc import Callable + + +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="module") +def pact() -> Generator[Pact, None, None]: + """ + Set up Message Pact Consumer. + + This fixtures sets up the Message Pact consumer and the pact it has with a + provider. The consumer defines the expected messages it will receive from + the provider, and the Python test suite verifies that the correct actions + are taken. + + The verify method takes a function as an argument. This function + will be called with one or two arguments - the value of `with_body` and + the contents of `with_metadata` if provided. + + If the function under test does not take those parameters, you can create + a wrapper function to convert the pact parameters into the values + expected by your function. + + + For each interaction, the consumer defines the following: + + ```python + ( + pact = Pact("consumer name", "provider name") + processed_messages: list[MessagePact.MessagePactResult] = pact \ + .with_specification("V3") + .upon_receiving("a request", "Async") \ + .given("a request to write test.txt") \ + .with_body(msg) \ + .with_metadata({"Content-Type": "application/json"}) + .verify(pact_handler) + ) + + ``` + """ + pact_dir = Path(Path(__file__).parent.parent / "pacts") + pact = Pact("v3_message_consumer", "v3_message_provider") + log.info("Creating Message Pact with V3 specification") + yield pact.with_specification("V3") + pact.write_file(pact_dir, overwrite=True) + + +@pytest.fixture() +def handler() -> Handler: + """ + Fixture for the Handler. + + This fixture mocks the filesystem calls in the handler, so that we can + verify that the handler is calling the filesystem correctly. + """ + handler = Handler() + handler.fs = MagicMock() + handler.fs.write.return_value = None + handler.fs.read.return_value = "Hello world!" + return handler + + +@pytest.fixture() +def verifier( + handler: Handler, +) -> Generator[Callable[[str | bytes | None, Dict[str, Any]], None], Any, None]: + """ + Verifier function for the Pact. + + This function is passed to the `verify` method of the Pact object. It is + responsible for taking in the messages (along with the context/metadata) + and ensuring that the consumer is able to process the message correctly. + + In our case, we deserialize the message and pass it to the (pre-mocked) + handler for processing. We then verify that the underlying filesystem + calls were made as expected. + """ + assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked" + + def _verifier(msg: str | bytes | None, context: Dict[str, Any]) -> None: + assert msg is not None, "Message is None" + data = json.loads(msg) + log.info( + "Processing message: ", + extra={"input": msg, "processed_message": data, "context": context}, + ) + handler.process(data) + + yield _verifier + + assert handler.fs.mock_calls, "Handler did not call the filesystem" + + +def test_async_message_handler_write( + pact: Pact, + handler: Handler, + verifier: Callable[[str | bytes | None, Dict[str, Any]], None], +) -> None: + """ + Create a pact between the message handler and the message provider. + """ + assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked" + + ( + pact.upon_receiving("a write request", "Async") + .given("a request to write test.txt") + .with_body( + json.dumps({ + "action": "WRITE", + "path": "my_file.txt", + "contents": "Hello, world!", + }) + ) + ) + pact.verify(verifier, "Async") + + handler.fs.write.assert_called_once_with("my_file.txt", "Hello, world!") + + +def test_async_message_handler_read( + pact: Pact, + handler: Handler, + verifier: Callable[[str | bytes | None, Dict[str, Any]], None], +) -> None: + """ + Create a pact between the message handler and the message provider. + """ + assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked" + + ( + pact.upon_receiving("a read request", "Async") + .given("a request to read test.txt") + .with_body( + json.dumps({ + "action": "READ", + "path": "my_file.txt", + "contents": "Hello, world!", + }) + ) + ) + pact.verify(verifier, "Async") + + handler.fs.read.assert_called_once_with("my_file.txt") diff --git a/pyproject.toml b/pyproject.toml index 29dc4fb89..b1017f093 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -189,8 +189,10 @@ addopts = [ "--cov-report=xml", ] filterwarnings = [ + "ignore::DeprecationWarning:examples", "ignore::DeprecationWarning:pact", "ignore::DeprecationWarning:tests", + "ignore::PendingDeprecationWarning:examples", "ignore::PendingDeprecationWarning:pact", "ignore::PendingDeprecationWarning:tests", ] From 4373de099eccb50b7af2117096630c304d5486c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 21:55:25 +0000 Subject: [PATCH 0399/1376] chore(deps): update softprops/action-gh-release digest to a74c6b7 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 987bc3ee7..e6e2f7a0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -233,7 +233,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87 # v2 + uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 # v2 with: files: wheels/* body_path: ${{ runner.temp }}/changelog From e441b93cc49e7354e2aaed3aad5108da58f16713 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:28:34 +0000 Subject: [PATCH 0400/1376] chore(deps): update dependency psutil to v6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b1017f093..35aaf2c5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ "cffi ~=1.0", "click ~=8.0", "fastapi ~=0.0", - "psutil ~=5.0", + "psutil ~=6.0", "requests ~=2.0", "six ~=1.0", "typing-extensions ~=4.0 ; python_version < '3.10'", From d9fe48f314b07db201cbc8211d2f66e4efa4096c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:38:30 +0000 Subject: [PATCH 0401/1376] chore(deps): update peter-evans/create-pull-request digest to c5a7806 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6e2f7a0b..9aa4f097f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -249,7 +249,7 @@ jobs: packages-dir: wheels - name: Create PR for changelog update - uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6 + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6 with: token: ${{ secrets.GH_TOKEN }} commit-message: "chore: update changelog ${{ github.ref_name }}" From 31e28483049888609bd651b454a7192698b031df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:14:41 +0000 Subject: [PATCH 0402/1376] chore(deps): update actions/checkout digest to 692973e --- .github/workflows/build.yml | 8 ++++---- .github/workflows/docs.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/test.yml | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9aa4f097f..48ddb83d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: # Fetch all tags fetch-depth: 0 @@ -71,7 +71,7 @@ jobs: archs: AMD64 steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: # Fetch all tags fetch-depth: 0 @@ -143,7 +143,7 @@ jobs: build: "" steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: # Fetch all tags fetch-depth: 0 @@ -198,7 +198,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: # Fetch all tags fetch-depth: 0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 42854dff4..292b34bb0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: fetch-depth: 0 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index bc031c1d5..e5c952fd5 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Synchronize labels uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 693b00b1f..16167e95d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,7 +52,7 @@ jobs: experimental: true steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: submodules: true @@ -117,7 +117,7 @@ jobs: python-version: "3.9" steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: submodules: true @@ -170,7 +170,7 @@ jobs: PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Set up Python 3 uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 @@ -203,7 +203,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Set up Python uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 From 4914e02269f726404263ccdc9772d5430b85e0e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:26:52 +0000 Subject: [PATCH 0403/1376] chore(deps): update codecov/codecov-action digest to e28ff12 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 16167e95d..94fc96e99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,7 +89,7 @@ jobs: - name: Upload coverage # TODO: Configure code coverage monitoring if: false && matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # v4 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4 with: token: ${{ secrets.CODECOV_TOKEN }} From 4e99009bb819e781db94d69049d305b1a9e34fef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:22:25 +0000 Subject: [PATCH 0404/1376] chore(deps): update pypa/gh-action-pypi-publish action to v1.9.0 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48ddb83d3..5d49460ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -242,7 +242,7 @@ jobs: generate_release_notes: true - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14 + uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # v1.9.0 with: skip-existing: true password: ${{ secrets.PYPI_TOKEN }} From 2cbbcb7bb1e39c68146acc0b1a87b640b315aa46 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:26:56 +0000 Subject: [PATCH 0405/1376] chore(deps): update pypa/cibuildwheel action to v2.19.1 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d49460ee..a58a4f804 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,7 +104,7 @@ jobs: echo "MACOSX_DEPLOYMENT_TARGET=10.12" >> "$GITHUB_ENV" - name: Create wheels - uses: pypa/cibuildwheel@a8d190a111314a07eb5116036c4b3fb26a4e3162 # v2.19.0 + uses: pypa/cibuildwheel@932529cab190fafca8c735a551657247fa8f8eaf # v2.19.1 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} @@ -165,7 +165,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@a8d190a111314a07eb5116036c4b3fb26a4e3162 # v2.19.0 + uses: pypa/cibuildwheel@932529cab190fafca8c735a551657247fa8f8eaf # v2.19.1 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} From a31eb9c404a3b267a38abfabe21dbe4bd71fd06b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 01:45:57 +0000 Subject: [PATCH 0406/1376] chore(deps): update ubuntu:24.04 docker digest to 2e863c4 --- Dockerfile.ubuntu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 67e41f127..92667d1b7 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ubuntu:24.04@sha256:562456a05a0dbd62a671c1854868862a4687bf979a96d48ae8e766642cd911e8 +FROM ubuntu:24.04@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30 ENV DEBIAN_FRONTEND=noninteractive ARG PYTHON_VERSION 3.9 From 1b77939fb9df67b316a579fa06626e4403007531 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Jun 2024 01:06:04 +0000 Subject: [PATCH 0407/1376] chore(deps): update dependency devel-types/mypy to v1.10.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 35aaf2c5c..c963dda08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ pact-verifier = "pact.cli.verify:main" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. devel-types = [ - "mypy ==1.10.0", + "mypy ==1.10.1", "types-cffi ~=1.0", "types-requests ~=2.0", ] From 99462a049bc40116a468cbb7b8494c172b0f42af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Jun 2024 01:19:24 +0000 Subject: [PATCH 0408/1376] chore(deps): update dependency devel-test/pytest to ~=8.2.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c963dda08..17f73e9e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ devel-test = [ "mock ~=5.0", # TODO: Upgrade to PyTest 8.1 # Pending on https://github.com/pytest-dev/pytest-bdd/issues/673 - "pytest ~=8.0.0", + "pytest ~=8.2.2", "pytest-asyncio ~=0.0", "pytest-bdd ~=7.0", "pytest-cov ~=5.0", From 82c331db124fc586fd86e4b5bfe7a04fd7746967 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Jun 2024 20:13:20 +0000 Subject: [PATCH 0409/1376] chore(deps): update ruff to v0.4.10 Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- tests/v3/compatibility_suite/test_v1_provider.py | 8 ++++---- tests/v3/compatibility_suite/util/consumer.py | 2 ++ tests/v3/compatibility_suite/util/provider.py | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 15567089b..102362799 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.2 + rev: v0.4.10 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 17f73e9e3..7a1a94bfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.4.2"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.4.10"] ################################################################################ ## Hatch Build Configuration diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py index 349ef3357..1078f714a 100644 --- a/tests/v3/compatibility_suite/test_v1_provider.py +++ b/tests/v3/compatibility_suite/test_v1_provider.py @@ -93,7 +93,7 @@ def test_incorrect_request_is_made_to_provider() -> None: ) def test_verifying_a_simple_http_request_via_a_pact_broker() -> None: """Verifying a simple HTTP request via a Pact broker.""" - reset_broker_var.set(True) # noqa: FBT003 + reset_broker_var.set(True) @pytest.mark.skipif( @@ -107,7 +107,7 @@ def test_verifying_a_simple_http_request_via_a_pact_broker() -> None: ) def test_verifying_a_simple_http_request_via_a_pact_broker_with_publishing() -> None: """Verifying a simple HTTP request via a Pact broker with publishing.""" - reset_broker_var.set(True) # noqa: FBT003 + reset_broker_var.set(True) @pytest.mark.skipif( @@ -121,7 +121,7 @@ def test_verifying_a_simple_http_request_via_a_pact_broker_with_publishing() -> ) def test_verifying_multiple_pact_files_via_a_pact_broker() -> None: """Verifying multiple Pact files via a Pact broker.""" - reset_broker_var.set(True) # noqa: FBT003 + reset_broker_var.set(True) @pytest.mark.skipif( @@ -135,7 +135,7 @@ def test_verifying_multiple_pact_files_via_a_pact_broker() -> None: ) def test_incorrect_request_is_made_to_provider_via_a_pact_broker() -> None: """Incorrect request is made to provider via a Pact broker.""" - reset_broker_var.set(True) # noqa: FBT003 + reset_broker_var.set(True) @pytest.mark.skipif( diff --git a/tests/v3/compatibility_suite/util/consumer.py b/tests/v3/compatibility_suite/util/consumer.py index 38bf7676e..d58f5fe24 100644 --- a/tests/v3/compatibility_suite/util/consumer.py +++ b/tests/v3/compatibility_suite/util/consumer.py @@ -164,6 +164,7 @@ def _( ), headers=definition.headers if definition.headers else None, # type: ignore[arg-type] data=definition.body.bytes if definition.body else None, + timeout=5, ) @@ -213,6 +214,7 @@ def _( ), headers=definition.headers if definition.headers else None, # type: ignore[arg-type] data=definition.body.bytes if definition.body else None, + timeout=5, ) diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 4c9d0d5dd..616b11dc4 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -786,7 +786,7 @@ def _( if reset_broker_var.get(): logger.debug("Resetting Pact broker") pact_broker.reset() - reset_broker_var.set(False) # noqa: FBT003 + reset_broker_var.set(False) pact_broker.publish(pacts_dir) verifier.broker_source(pact_broker.url) yield pact_broker From 08a3a9dda8a0d47d02330ce1ea505653cd4d8075 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:26:21 +0000 Subject: [PATCH 0410/1376] chore(deps): update pypa/cibuildwheel action to v2.19.2 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a58a4f804..d653a51ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,7 +104,7 @@ jobs: echo "MACOSX_DEPLOYMENT_TARGET=10.12" >> "$GITHUB_ENV" - name: Create wheels - uses: pypa/cibuildwheel@932529cab190fafca8c735a551657247fa8f8eaf # v2.19.1 + uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} @@ -165,7 +165,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@932529cab190fafca8c735a551657247fa8f8eaf # v2.19.1 + uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} From d00ca78fb70c0aa09f33d75d8b4bf88621b747be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Jun 2024 19:28:45 +0000 Subject: [PATCH 0411/1376] chore(deps): update ruff to v0.5.0 Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- docs/scripts/markdown.py | 4 ++-- docs/scripts/python.py | 4 ++-- pyproject.toml | 2 +- tests/v3/compatibility_suite/util/consumer.py | 4 ++-- tests/v3/compatibility_suite/util/provider.py | 8 ++++---- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 102362799..38ad04aee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.10 + rev: v0.5.0 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/docs/scripts/markdown.py b/docs/scripts/markdown.py index 5f81ab8ca..c27b8b73b 100644 --- a/docs/scripts/markdown.py +++ b/docs/scripts/markdown.py @@ -75,8 +75,8 @@ def process_markdown( ] files = sorted( Path(p) - for p in subprocess.check_output( - ["git", "ls-files", src], # noqa: S603, S607 + for p in subprocess.check_output( # noqa: S603 + ["git", "ls-files", src], # noqa: S607 ) .decode("utf-8") .splitlines() diff --git a/docs/scripts/python.py b/docs/scripts/python.py index 14ee78f33..21e024fee 100644 --- a/docs/scripts/python.py +++ b/docs/scripts/python.py @@ -165,8 +165,8 @@ def process_python( ignore_spec = PathSpec.from_lines("gitwildmatch", ignore or []) files = sorted( Path(p) - for p in subprocess.check_output( - ["git", "ls-files", src], # noqa: S603, S607 + for p in subprocess.check_output( # noqa: S603 + ["git", "ls-files", src], # noqa: S607 ) .decode("utf-8") .splitlines() diff --git a/pyproject.toml b/pyproject.toml index 7a1a94bfd..1780f266a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.4.10"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.5.0"] ################################################################################ ## Hatch Build Configuration diff --git a/tests/v3/compatibility_suite/util/consumer.py b/tests/v3/compatibility_suite/util/consumer.py index d58f5fe24..66033cd50 100644 --- a/tests/v3/compatibility_suite/util/consumer.py +++ b/tests/v3/compatibility_suite/util/consumer.py @@ -647,8 +647,8 @@ def _( expected = {key: value} actual = pact_file["interactions"][n - 1]["request"]["headers"] assert expected.keys() == actual.keys() - for key in expected: - assert expected[key] == actual[key] or [expected[key]] == actual[key] + for k in expected: + assert expected[k] == actual[k] or [expected[k]] == actual[k] def the_nth_interaction_request_content_type_will_be(stacklevel: int = 1) -> None: diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 616b11dc4..f9ab72b90 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -393,8 +393,8 @@ def publish(self, directory: Path | str, version: str | None = None) -> None: cmd.extend(["--consumer-app-version", version or next_version()]) - subprocess.run( - cmd, # noqa: S603 + subprocess.run( # noqa: S603 + cmd, encoding="utf-8", check=True, ) @@ -580,8 +580,8 @@ def _( def start_provider(provider_dir: str | Path) -> Generator[URL, None, None]: # noqa: C901 """Start the provider app with the given interactions.""" - process = subprocess.Popen( - [ # noqa: S603 + process = subprocess.Popen( # noqa: S603 + [ sys.executable, Path(__file__), str(provider_dir), From 6c400dc9fbb25cc08a8b2f727b9d1da3bd14ecef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:43:04 +0000 Subject: [PATCH 0412/1376] chore(deps): update docker/setup-qemu-action digest to 5927c83 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d653a51ea..3e135838a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -160,7 +160,7 @@ jobs: - name: Set up QEMU if: startsWith(matrix.os, 'ubuntu-') - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3 + uses: docker/setup-qemu-action@5927c834f5b4fdf503fca6f4c7eccda82949e1ee # v3 with: platforms: arm64 From 6450f8a8943b648f7d6c64e5192b1ae05891ba8c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:18:30 +0000 Subject: [PATCH 0413/1376] chore(deps): update ruff to v0.5.1 --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 38ad04aee..4191c4875 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.0 + rev: v0.5.1 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 1780f266a..8c7c39267 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.5.0"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.5.1"] ################################################################################ ## Hatch Build Configuration From 565484d41be27c99021943a8b2a1e6a860c33410 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:07:46 +0000 Subject: [PATCH 0414/1376] chore(deps): update actions/download-artifact digest to fa0a91b --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e135838a..a710e9dfb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -204,7 +204,7 @@ jobs: fetch-depth: 0 - name: Download wheels and sdist - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 with: path: wheels merge-multiple: true From 699ed5099783a29c5bea28adec81672ff36d1857 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:07:50 +0000 Subject: [PATCH 0415/1376] chore(deps): update actions/upload-artifact digest to 0b2256b --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a710e9dfb..0a803ec56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4 with: name: wheels-sdist path: ./dist/*.tar.* @@ -110,7 +110,7 @@ jobs: CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} - name: Upload wheels - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl @@ -171,7 +171,7 @@ jobs: CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} - name: Upload wheels - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }}-${{ matrix.build }} path: ./wheelhouse/*.whl From ae8e6c389fe5c534e7c9094b27c56ebd20ba95ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 20:46:12 +0000 Subject: [PATCH 0416/1376] chore(deps): update softprops/action-gh-release digest to fb2d031 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0a803ec56..18cbed81b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -233,7 +233,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 # v2 + uses: softprops/action-gh-release@fb2d03176f42a1f0dd433ca263f314051d3edd44 # v2 with: files: wheels/* body_path: ${{ runner.temp }}/changelog From ea3de4122dc808ee80e209ca1117f9168eef8adb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:16:30 +0000 Subject: [PATCH 0417/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.28.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4191c4875..6df39530c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,7 +53,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.27.0 + rev: v3.28.0 hooks: - id: commitizen stages: [commit-msg] From 9424a822825f29533df6ca45f58ef02bfd6b6045 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 14 Jul 2024 16:30:30 +0000 Subject: [PATCH 0418/1376] chore(deps): update ruff to v0.5.2 --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6df39530c..6e5e3087c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.1 + rev: v0.5.2 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 8c7c39267..9c0f7395f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.5.1"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.5.2"] ################################################################################ ## Hatch Build Configuration From 59c0cd1bc8e6a302c9167446c144cd99fd4acbe0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:49:45 +0000 Subject: [PATCH 0419/1376] chore(deps): update actions/setup-python digest to 39cd149 --- .github/workflows/build.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 18cbed81b..db0fb09cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} cache: pip @@ -210,7 +210,7 @@ jobs: merge-multiple: true - name: Setup Python - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 292b34bb0..3a93be411 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} cache: pip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94fc96e99..69eb579a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,7 +63,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5 with: python-version: ${{ matrix.python-version }} cache: pip @@ -128,7 +128,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5 with: python-version: ${{ matrix.python-version }} cache: pip @@ -173,7 +173,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Set up Python 3 - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} cache: pip @@ -206,7 +206,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Set up Python - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} cache: pip From 965090218c1a7653382807255e2c90607c045395 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 18 Jul 2024 08:48:02 +1000 Subject: [PATCH 0420/1376] chore: update GitHub templates Signed-off-by: JP-Ellis --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 5 +---- .github/ISSUE_TEMPLATE/feature.yml | 8 +------- CONTRIBUTING.md | 2 +- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 7b48a6274..7513a2863 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -52,7 +52,7 @@ body: description: | Paste the link to an example repo if possible, and exact instructions to reproduce the issue. - > **What happens if you skip this step?** Someone will read your bug report, and maybe will be able to help you, but it's unlikely that it will get much attention from the team. Eventually, the issue will likely get closed in favor of issues that have reproducible demos. + > **What happens if you skip this step?** Someone will read your bug report, and maybe will be able to help you, but if we fail to reproduce the issue, we might not be able to fix it. Please remember that: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7c09cc37b..9d2ee6cbf 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,5 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - - name: 🚀 Feature request - url: https://pact.canny.io - about: The Canny board to send us feature requests, vote and measure the interest of users. Useful to submit a feature request when you have an idea but no concrete API design proposal. - name: ❓ Simple question - Slack chat url: https://slack.pact.io about: If you have a simple question, or want to discuss a feature request, join our Slack chat. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 2cfa45f03..416280548 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -1,4 +1,4 @@ -name: 💅 Feature design / RFC +name: 💡 Feature design / RFC description: Submit a detailed feature request with a concrete proposal labels: [feature, triage] body: @@ -9,7 +9,6 @@ body: - We expect the feature request to be detailed. - The request does not have to be perfect, we'll discuss it and fix it if needed. - - For a more "casual" feature request, consider using Canny instead: https://pact.canny.io. - type: checkboxes attributes: @@ -25,11 +24,6 @@ body: validations: required: true - - type: input - attributes: - label: Has this been requested on Canny? - description: Please post the [Canny](https://pact.canny.io) link, it is helpful to see how much interest there is for this feature. - - type: textarea attributes: label: Motivation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4fc271cb0..db794c92f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,7 +66,7 @@ If you're only fixing a bug, it's fine to submit a pull request right away but w ### Feature requests -If you would like to request a new feature or enhancement but are not yet thinking about opening a pull request, you can file an issue with the [feature template](https://github.com/pact-foundation/pact-python/issues/new?assignees=&labels=feature%2Cstatus%3A+needs+triage&template=feature.yml) for more thought out ideas. Alternatively, you can use the [Canny board](https://pact.canny.io/) for more casual feature requests and gain enough traction before proposing an RFC. +If you would like to request a new feature or enhancement but are not yet thinking about opening a pull request, you can file an issue with the [feature template](https://github.com/pact-foundation/pact-python/issues/new?assignees=&labels=feature%2Cstatus%3A+needs+triage&template=feature.yml) for more thought out ideas. ### Claiming issues From 4f508ce0382ebc94eaaa34cd0d2ac10a0da77dbd Mon Sep 17 00:00:00 2001 From: valkolovos Date: Mon, 8 Jul 2024 16:28:54 -0600 Subject: [PATCH 0421/1376] feat(v3): add async message provider With this PR, async message providers (aka producers) are now (mostly) working, so that the `pact.v3` module supports both asynchronous message providers and consumers. --- pyproject.toml | 1 + .../test_v3_message_producer.py | 384 ++++++++++++++++++ tests/v3/compatibility_suite/util/__init__.py | 186 ++++++--- tests/v3/compatibility_suite/util/provider.py | 107 ++++- 4 files changed, 611 insertions(+), 67 deletions(-) create mode 100644 tests/v3/compatibility_suite/test_v3_message_producer.py diff --git a/pyproject.toml b/pyproject.toml index 9c0f7395f..feb854d58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -208,6 +208,7 @@ markers = [ # Markers for the compatibility suite "consumer", "provider", + "message", ] ################################################################################ diff --git a/tests/v3/compatibility_suite/test_v3_message_producer.py b/tests/v3/compatibility_suite/test_v3_message_producer.py new file mode 100644 index 000000000..56ec0ace7 --- /dev/null +++ b/tests/v3/compatibility_suite/test_v3_message_producer.py @@ -0,0 +1,384 @@ +"""V3 Message provider feature tests.""" + +from __future__ import annotations + +import json +import logging +import pickle +import re +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest +from pytest_bdd import ( + given, + parsers, + scenario, +) + +from pact.v3.pact import Pact +from tests.v3.compatibility_suite.util import ( + InteractionDefinition, + parse_horizontal_markdown_table, + parse_markdown_table, +) +from tests.v3.compatibility_suite.util.provider import ( + a_provider_is_started_that_can_generate_the_message, + the_provider_state_callback_will_be_called_after_the_verification_is_run, + the_provider_state_callback_will_be_called_before_the_verification_is_run, + the_provider_state_callback_will_receive_a_setup_call, + the_verification_is_run_with_start_context, + the_verification_results_will_contain_a_error, + the_verification_will_be_successful, +) + +if TYPE_CHECKING: + from pact.v3.verifier import Verifier + +TEST_PACT_FILE_DIRECTORY = Path(Path(__file__).parent / "pacts") + +logger = logging.getLogger(__name__) + + +@scenario( + "definition/features/V3/message_provider.feature", + "Incorrect message is generated by the provider", +) +def test_incorrect_message_is_generated_by_the_provider() -> None: + """Incorrect message is generated by the provider.""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Message with JSON body (negative case)", +) +def test_message_with_json_body_negative_case() -> None: + """Message with JSON body (negative case).""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Message with JSON body (positive case)", +) +def test_message_with_json_body_positive_case() -> None: + """Message with JSON body (positive case).""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Message with XML body (negative case)", +) +def test_message_with_xml_body_negative_case() -> None: + """Message with XML body (negative case).""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Message with XML body (positive case)", +) +def test_message_with_xml_body_positive_case() -> None: + """Message with XML body (positive case).""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Message with binary body (negative case)", +) +def test_message_with_binary_body_negative_case() -> None: + """Message with binary body (negative case).""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Message with binary body (positive case)", +) +def test_message_with_binary_body_positive_case() -> None: + """Message with binary body (positive case).""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Message with plain text body (negative case)", +) +def test_message_with_plain_text_body_negative_case() -> None: + """Message with plain text body (negative case).""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Message with plain text body (positive case)", +) +def test_message_with_plain_text_body_positive_case() -> None: + """Message with plain text body (positive case).""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Supports matching rules for the message body (negative case)", +) +def test_supports_matching_rules_for_the_message_body_negative_case() -> None: + """Supports matching rules for the message body (negative case).""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Supports matching rules for the message body (positive case)", +) +def test_supports_matching_rules_for_the_message_body_positive_case() -> None: + """Supports matching rules for the message body (positive case).""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Supports matching rules for the message metadata (negative case)", +) +def test_supports_matching_rules_for_the_message_metadata_negative_case() -> None: + """Supports matching rules for the message metadata (negative case).""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Supports matching rules for the message metadata (positive case)", +) +def test_supports_matching_rules_for_the_message_metadata_positive_case() -> None: + """Supports matching rules for the message metadata (positive case).""" + + +@pytest.mark.skip("Currently unable to implement") +@scenario( + "definition/features/V3/message_provider.feature", + "Supports messages with body formatted for the Kafka schema registry", +) +def test_supports_messages_with_body_formatted_for_the_kafka_schema_registry() -> None: + """Supports messages with body formatted for the Kafka schema registry.""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Verifies the message metadata", +) +def test_verifies_the_message_metadata() -> None: + """Verifies the message metadata.""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Verifying a simple message", +) +def test_verifying_a_simple_message() -> None: + """Verifying a simple message.""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Verifying an interaction with a defined provider state", +) +def test_verifying_an_interaction_with_a_defined_provider_state() -> None: + """Verifying an interaction with a defined provider state.""" + + +@scenario( + "definition/features/V3/message_provider.feature", + "Verifying multiple Pact files", +) +def test_verifying_multiple_pact_files() -> None: + """Verifying multiple Pact files.""" + + +################################################################################ +## Given +################################################################################ + + +a_provider_is_started_that_can_generate_the_message() + + +@given( + parsers.re( + r'a Pact file for "(?P[^"]+)" is to be verified with the following:\n' + r"(?P
.+)", + re.DOTALL, + ), + converters={"table": parse_horizontal_markdown_table}, +) +def a_pact_file_for_is_to_be_verified_with_the_following( + verifier: Verifier, + temp_dir: Path, + name: str, + table: dict[str, str], +) -> None: + """A Pact file for "basic" is to be verified with the following.""" + metadata = {} + if table.get("metadata"): + + def _repl(x: str) -> tuple[str, str]: + return (z.replace("JSON: ", "") for z in x.split("=")) + + metadata = dict(_repl(x) for x in table["metadata"].split("; ")) + pact = Pact("consumer", "provider") + pact.with_specification("V3") + interaction_definition = InteractionDefinition( + method="POST", + path=f"/{name}", + metadata=metadata, + response_body=table["body"], + matching_rules=table.get("matching rules"), + type="Async", + ) + interaction_definition.add_to_pact(pact, name) + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + verifier.add_source(temp_dir / "pacts") + + +@given(parsers.parse('a Pact file for "{name}":"{fixture}" is to be verified')) +def a_pact_file_for_is_to_be_verified( + verifier: Verifier, temp_dir: Path, name: str, fixture: str +) -> None: + pact = Pact("consumer", "provider") + pact.with_specification("V3") + interaction_definition = InteractionDefinition( + method="POST", + path=f"/{name}", + response_body=fixture, + type="Async", + ) + # for plain text message, the mime type needs to be set + if not re.match(r"^(file:|JSON:)", fixture): + interaction_definition.response_body.mime_type = "text/html;charset=utf-8" + interaction_definition.add_to_pact(pact, name) + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + verifier.add_source(temp_dir / "pacts") + + +@given( + parsers.parse( + 'a Pact file for "{name}":"{fixture}" is to be ' + 'verified with provider state "{provider_state}"' + ) +) +def a_pact_file_for_is_to_be_verified_with_provider_state( + temp_dir: Path, + verifier: Verifier, + name: str, + fixture: str, + provider_state: str, +) -> None: + """A Pact file is to be verified with provider state.""" + pact = Pact("consumer", "provider") + pact.with_specification("V3") + interaction_definition = InteractionDefinition( + method="POST", + path=f"/{name}", + response_body=fixture, + type="Async", + ) + states = [InteractionDefinition.State(provider_state)] + interaction_definition.states = states + interaction_definition.add_to_pact(pact, name) + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + verifier.add_source(temp_dir / "pacts") + with (temp_dir / "provider_states").open("w") as f: + logger.debug("Writing provider state to %s", temp_dir / "provider_states") + json.dump( + [s.as_dict() for s in [InteractionDefinition.State(provider_state)]], f + ) + + +@given( + parsers.re( + r'a Pact file for "(?P[^"]+)":"(?P[^"]+)" is ' + "to be verified with the following metadata:\n" + r"(?P.+)", + re.DOTALL, + ), + converters={"metadata": parse_markdown_table}, +) +def a_pact_file_for_is_to_be_verified_with_the_following_metadata( + temp_dir: Path, + verifier: Verifier, + name: str, + fixture: str, + metadata: dict[str, str], +) -> None: + """A Pact file is to be verified with the following metadata.""" + pact = Pact("consumer", "provider") + pact.with_specification("V3") + interaction_definition = InteractionDefinition( + method="POST", + path=f"/{name}", + metadata={h["key"]: h["value"].replace("JSON: ", "") for h in metadata}, + response_body=fixture, + type="Async", + ) + interaction_definition.add_to_pact(pact, name) + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + verifier.add_source(temp_dir / "pacts") + + +@given( + parsers.re( + r'a provider is started that can generate the "(?P[^"]+)" ' + r'message with "(?P[^"]+)" and the following metadata:\n' + r"(?P.+)", + re.DOTALL, + ), + converters={"metadata": parse_markdown_table}, +) +def a_provider_is_started_that_can_generate_the_message_with_the_following_metadata( + temp_dir: Path, + name: str, + fixture: str, + metadata: dict[str, str], +) -> None: + """A provider is started that can generate the message with the following metadata.""" # noqa: E501 + interaction_definitions = [] + if (temp_dir / "interactions.pkl").exists(): + with (temp_dir / "interactions.pkl").open("rb") as pkl_file: + interaction_definitions = pickle.load(pkl_file) # noqa: S301 + + def parse_metadata_value(value: str) -> str: + return ( + json.loads(value.replace("JSON: ", "")) + if value.startswith("JSON: ") + else value + ) + + interaction_definition = InteractionDefinition( + method="POST", + path=f"/{name}", + metadata={m["key"]: parse_metadata_value(m["value"]) for m in metadata}, + response_body=fixture, + type="Async", + ) + interaction_definitions.append(interaction_definition) + with (temp_dir / "interactions.pkl").open("wb") as pkl_file: + pickle.dump(interaction_definitions, pkl_file) + + +@given("a provider state callback is configured", target_fixture="callback") +def a_provider_state_callback_is_configured() -> None: + """A provider state callback is configured.""" + + +################################################################################ +## When +################################################################################ + + +the_verification_is_run_with_start_context() + + +################################################################################ +## Then +################################################################################ + + +the_provider_state_callback_will_be_called_before_the_verification_is_run() +the_provider_state_callback_will_be_called_after_the_verification_is_run() +the_provider_state_callback_will_receive_a_setup_call() +the_verification_will_be_successful() +the_verification_results_will_contain_a_error() diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 91b5871d1..1dcb18bec 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -29,6 +29,7 @@ def _(): import sys import typing from collections.abc import Collection, Mapping +from contextlib import suppress from datetime import date, datetime, time from pathlib import Path from typing import Any, Generic, TypeVar @@ -41,6 +42,7 @@ def _(): from yarl import URL if typing.TYPE_CHECKING: + from pact.v3.interaction import Interaction from pact.v3.pact import Pact logger = logging.getLogger(__name__) @@ -185,6 +187,31 @@ def parse_markdown_table(content: str) -> list[dict[str, str]]: return [dict(zip(rows[0], row)) for row in rows[1:]] +def parse_horizontal_markdown_table(content: str) -> list[dict[str, str]]: + """ + Parse a Markdown table into a list of dictionaries. + + The table is expected to be in the following format: + + ```markdown + | key1 | val1 | + | key2 | val2 | + | key3 | val3 | + ``` + """ + rows = [ + list(map(str.strip, row.split("|")))[1:-1] + for row in content.split("\n") + if row.strip() + ] + + if len(rows[0]) > 2: + msg = f"Expected at most two columns in the table, got {len(rows[0])}" + raise ValueError(msg) + + return {row[0]: row[1] for row in rows} + + def serialize(obj: Any) -> Any: # noqa: ANN401, PLR0911 """ Convert an object to a dictionary. @@ -291,7 +318,7 @@ class Body: - An XML document """ - def __init__(self, data: str) -> None: + def __init__(self, data: str | bytes) -> None: """ Instantiate the interaction body. """ @@ -299,6 +326,10 @@ def __init__(self, data: str) -> None: self.bytes: bytes | None = None self.mime_type: str | None = None + if isinstance(data, bytes): + self.bytes = data + return + if data.startswith("file: ") and data.endswith("-body.xml"): self.parse_fixture(FIXTURES_ROOT / data[6:]) return @@ -452,6 +483,9 @@ def __init__(self, **kwargs: str) -> None: self.response_body: InteractionDefinition.Body | None = None self.matching_rules: str | None = None self.response_matching_rules: str | None = None + self.metadata: dict[str, Any] | None = None + self.is_pending: bool = kwargs.pop("is_pending", False) + self.type: typing.Literal["HTTP", "Sync", "Async"] = kwargs.pop("type", "HTTP") self.update(**kwargs) @@ -533,6 +567,9 @@ def update(self, **kwargs: str) -> None: # noqa: C901, PLR0912 ): self.response_matching_rules = parse_matching_rules(matching_rules) + if metadata := kwargs.pop("metadata", None): + self.metadata = metadata + if len(kwargs) > 0: msg = f"Unexpected arguments: {kwargs.keys()}" raise TypeError(msg) @@ -545,7 +582,62 @@ def __repr__(self) -> str: ", ".join(f"{k}={v!r}" for k, v in vars(self).items()), ) - def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912, PLR0915 + def _add_body( + self, body: InteractionDefinition.Body, interaction: Interaction + ) -> None: + if body.mime_type == "application/xml": + + def _element_to_json(element: ElementTree.Element) -> dict[str, Any]: + json_dict = { + "name": element.tag, + } + if element.attrib: + json_dict["attributes"] = element.attrib + if len(element): + json_dict["children"] = [ + _element_to_json(child) for child in element + ] + else: + json_dict["children"] = [{"content": element.text}] + return json_dict + + with suppress(ElementTree.ParseError): + # try to parse the content as XML + # it _may_ be JSON, so it's ok if this errors + body.string = json.dumps({ + "root": _element_to_json( + ElementTree.fromstring(body.string) # noqa: S314 + ) + }) + if body.string: + logger.info( + "with_body(%s, %s)", + truncate(body.string), + body.mime_type, + ) + interaction.with_body( + body.string, + body.mime_type, + ) + elif body.bytes: + logger.info( + "with_binary_file(%s, %s)", + truncate(body.bytes), + body.mime_type, + ) + interaction.with_binary_body( + body.bytes, + body.mime_type, + ) + else: + msg = "Unexpected body definition" + raise RuntimeError(msg) + + def add_to_pact( # noqa: C901, PLR0912, PLR0915 + self, + pact: Pact, + name: str, + ) -> None: """ Add the interaction to the pact. @@ -560,9 +652,10 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912, PL name: Name for this interaction. Must be unique for the pact. """ - interaction = pact.upon_receiving(name) - logger.info("with_request(%s, %s)", self.method, self.path) - interaction.with_request(self.method, self.path) + interaction = pact.upon_receiving(name, self.type) + logger.info("with_request(%s, %s, %s)", self.method, self.path, self.type) + if self.type != "Async": + interaction.with_request(self.method, self.path) for state in self.states or []: if state.parameters: @@ -577,8 +670,9 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912, PL interaction.set_pending(pending=True) if self.text_comments: - logger.info("set_comment(text, %s)", self.text_comments) - interaction.set_comment("text", self.text_comments) + for comment in self.text_comments: + logger.info("add_text_comment(%s)", comment) + interaction.add_text_comment(comment) for key, value in self.comments.items(): logger.info("set_comment(%s, %s)", key, value) @@ -598,29 +692,7 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912, PL interaction.with_headers(self.headers.items()) if self.body: - if self.body.string: - logger.info( - "with_body(%s, %s)", - truncate(self.body.string), - self.body.mime_type, - ) - interaction.with_body( - self.body.string, - self.body.mime_type, - ) - elif self.body.bytes: - logger.info( - "with_binary_file(%s, %s)", - truncate(self.body.bytes), - self.body.mime_type, - ) - interaction.with_binary_body( - self.body.bytes, - self.body.mime_type, - ) - else: - msg = "Unexpected body definition" - raise RuntimeError(msg) + self._add_body(self.body, interaction) if self.matching_rules: logger.info("with_matching_rules(%s)", self.matching_rules) @@ -628,41 +700,28 @@ def add_to_pact(self, pact: Pact, name: str) -> None: # noqa: C901, PLR0912, PL if self.response: logger.info("will_respond_with(%s)", self.response) - interaction.will_respond_with(self.response) + if self.type != "Async": + interaction.will_respond_with(self.response) if self.response_headers: logger.info("with_headers(%s)", self.response_headers) interaction.with_headers(self.response_headers.items()) if self.response_body: - if self.response_body.string: - logger.info( - "with_body(%s, %s)", - truncate(self.response_body.string), - self.response_body.mime_type, - ) - interaction.with_body( - self.response_body.string, - self.response_body.mime_type, - ) - elif self.response_body.bytes: - logger.info( - "with_binary_file(%s, %s)", - truncate(self.response_body.bytes), - self.response_body.mime_type, - ) - interaction.with_binary_body( - self.response_body.bytes, - self.response_body.mime_type, - ) - else: - msg = "Unexpected body definition" - raise RuntimeError(msg) + self._add_body(self.response_body, interaction) if self.response_matching_rules: logger.info("with_matching_rules(%s)", self.response_matching_rules) interaction.with_matching_rules(self.response_matching_rules) + if self.metadata: + for key, value in self.metadata.items(): + interaction.with_metadata({key: value}) + + if self.is_pending: + logger.info("set_pending(True)") + interaction.set_pending(pending=True) + def add_to_flask(self, app: flask.Flask) -> None: """ Add an interaction to a Flask app. @@ -725,3 +784,22 @@ def route_fn() -> flask.Response: view_func=route_fn, methods=[self.method], ) + + def create_message_response(self) -> flask.Response: + """Creates a flask response for an async message.""" + if self.metadata: + self.response_headers.add( + "Pact-Message-Metadata", + base64.b64encode(json.dumps(self.metadata).encode("utf-8")).decode( + "utf-8" + ), + ) + return flask.Response( + response=self.response_body.bytes or self.response_body.string or None + if self.response_body + else None, + status=self.response, + headers=dict(**self.response_headers), + content_type=self.response_body.mime_type if self.response_body else None, + direct_passthrough=True, + ) diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index f9ab72b90..e5554b462 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -87,6 +87,13 @@ start of the scenario. """ +VERIFIER_ERROR_MAP: dict[str, str] = { + "Response status did not match": "StatusMismatch", + "Headers had differences": "HeaderMismatch", + "Body had differences": "BodyMismatch", + "Metadata had differences": "MetadataMismatch", +} + def next_version() -> str: """ @@ -136,6 +143,7 @@ def __init__(self, provider_dir: Path | str) -> None: called `interactions.pkl`. This file must contain a list of [`InteractionDefinition`] objects. """ + self._messages = {} self.provider_dir = Path(provider_dir) if not self.provider_dir.is_dir(): msg = f"Directory {self.provider_dir} does not exist" @@ -284,7 +292,26 @@ def _add_interactions(self, app: flask.Flask) -> None: interactions: list[InteractionDefinition] = pickle.load(f) # noqa: S301 for interaction in interactions: - interaction.add_to_flask(app) + if interaction.type != "Async": + interaction.add_to_flask(app) + else: + self._messages[interaction.path] = interaction + + @app.route("/message_handler", methods=["GET", "POST"]) + def handle_messages() -> flask.Response: + body = json.loads(request.data.decode("utf-8")) + message = self._messages.get("/" + body.get("description", "")) + if message: + return message.create_message_response() + return flask.Response( + response=json.dumps({ + "error": f"Message {body.get('description')} not found" + }), + status=404, + headers={"Content-Type": "application/json"}, + content_type="application/json", + direct_passthrough=True, + ) def run(self) -> None: """ @@ -578,6 +605,37 @@ def _( yield from start_provider(temp_dir) +def a_provider_is_started_that_can_generate_the_message( + stacklevel: int = 1, +) -> None: + @given( + parsers.parse( + 'a provider is started that can generate the "{name}" message with "{body}"' + ), + stacklevel=stacklevel + 1, + ) + def _( + temp_dir: Path, + name: str, + body: str, + ) -> None: + interaction_definitions = [] + if (temp_dir / "interactions.pkl").exists(): + with (temp_dir / "interactions.pkl").open("rb") as pkl_file: + interaction_definitions = pickle.load(pkl_file) # noqa: S301 + + body = body.replace('\\"', '"') + interaction_definition = InteractionDefinition( + method="POST", + path=f"/{name}", + response_body=body, + type="Async", + ) + interaction_definitions.append(interaction_definition) + with (temp_dir / "interactions.pkl").open("wb") as pkl_file: + pickle.dump(interaction_definitions, pkl_file) + + def start_provider(provider_dir: str | Path) -> Generator[URL, None, None]: # noqa: C901 """Start the provider app with the given interactions.""" process = subprocess.Popen( # noqa: S603 @@ -985,6 +1043,34 @@ def _( return verifier, None +def the_verification_is_run_with_start_context( + stacklevel: int = 1, +) -> tuple[Verifier, Exception | None]: + @when( + "the verification is run", + target_fixture="verifier_result", + stacklevel=stacklevel + 1, + ) + def _( + verifier: Verifier, + temp_dir: Path, + ) -> tuple[Verifier, Exception | None]: + """Run the verification.""" + start_provider_context_manager = contextlib.contextmanager(start_provider) + + with start_provider_context_manager(temp_dir) as provider_url: + verifier.set_state( + provider_url / "_test" / "callback", + teardown=True, + ) + verifier.set_info("provider", url=f"{provider_url}/message_handler") + try: + verifier.verify() + except Exception as e: # noqa: BLE001 + return verifier, e + return verifier, None + + ################################################################################ ## Then ################################################################################ @@ -1030,18 +1116,13 @@ def _(verifier_result: tuple[Verifier, Exception | None], error: str) -> None: verifier = verifier_result[0] logger.debug("Verification results: %s", json.dumps(verifier.results, indent=2)) - if error == "Response status did not match": - mismatch_type = "StatusMismatch" - elif error == "Headers had differences": - mismatch_type = "HeaderMismatch" - elif error == "Body had differences": - mismatch_type = "BodyMismatch" - elif error == "State change request failed": - assert "One or more of the setup state change handlers has failed" in [ - error["mismatch"]["message"] for error in verifier.results["errors"] - ] - return - else: + mismatch_type = VERIFIER_ERROR_MAP.get(error) + if not mismatch_type: + if error == "State change request failed": + assert "One or more of the setup state change handlers has failed" in [ + error["mismatch"]["message"] for error in verifier.results["errors"] + ] + return msg = f"Unknown error type: {error}" raise ValueError(msg) From ea1fe387e4136f03975e5747559e2f8ebc2c0fef Mon Sep 17 00:00:00 2001 From: valkolovos Date: Wed, 10 Jul 2024 08:55:00 -0600 Subject: [PATCH 0422/1376] chore(examples): add asynchronous message --- examples/src/message_producer.py | 106 ++++++++ examples/tests/v3/provider_server.py | 253 ++++++++++++++++++ examples/tests/v3/test_01_message_consumer.py | 32 ++- examples/tests/v3/test_02_message_provider.py | 74 +++++ 4 files changed, 461 insertions(+), 4 deletions(-) create mode 100644 examples/src/message_producer.py create mode 100644 examples/tests/v3/provider_server.py create mode 100644 examples/tests/v3/test_02_message_provider.py diff --git a/examples/src/message_producer.py b/examples/src/message_producer.py new file mode 100644 index 000000000..efbe7e762 --- /dev/null +++ b/examples/src/message_producer.py @@ -0,0 +1,106 @@ +""" +Message producer for non-HTTP interactions. + +This modules implements a very basic message producer which could +send to an eventing system, such as Kafka, or a message queue. +""" + +from __future__ import annotations + +import enum +import json +from typing import Literal, NamedTuple + + +class FileSystemAction(enum.Enum): + """ + Represents a file system action. + """ + + READ = "READ" + WRITE = "WRITE" + + +class FileSystemEvent(NamedTuple): + """ + Represents a file system event. + """ + + action: Literal[FileSystemAction.READ, FileSystemAction.WRITE] + path: str + contents: str | None + + +class MockMessageQueue: + """ + A mock message queue. + """ + + def __init__(self) -> None: + """ + Initialize the message queue. + """ + self.messages: list[str] = [] + + def send(self, message: str) -> None: + """ + Send a message to the queue. + + Args: + message: The message to send. + """ + self.messages.append(message) + + +class FileSystemMessageProducer: + """ + A message producer for file system events. + """ + + def __init__(self) -> None: + """ + Initialize the message producer. + """ + self.queue = MockMessageQueue() + + def send_to_queue(self, message: FileSystemEvent) -> None: + """ + Send a message to a message queue. + + :param message: The message to send. + """ + self.queue.send( + json.dumps({ + "action": message.action.value, + "path": message.path, + "contents": message.contents, + }) + ) + + def send_write_event(self, filename: str, contents: str) -> None: + """ + Send a write event to a message queue. + + Args: + filename: The name of the file. + contents: The contents of the file. + """ + message = FileSystemEvent( + action=FileSystemAction.WRITE, + path=filename, + contents=contents, + ) + self.send_to_queue(message) + + def send_read_event(self, filename: str) -> None: + """ + Send a read event to a message queue. + + :param filename: The name of the file. + """ + message = FileSystemEvent( + action=FileSystemAction.READ, + path=filename, + contents=None, + ) + self.send_to_queue(message) diff --git a/examples/tests/v3/provider_server.py b/examples/tests/v3/provider_server.py new file mode 100644 index 000000000..0bc9aad22 --- /dev/null +++ b/examples/tests/v3/provider_server.py @@ -0,0 +1,253 @@ +""" +HTTP Server to route message requests to message producer function. +""" + +from __future__ import annotations + +import logging +import re +import signal +import socket +import subprocess +import sys +import time +from contextlib import closing, contextmanager +from importlib import import_module +from pathlib import Path +from threading import Thread +from typing import Generator, NoReturn, Tuple + +import requests + +sys.path.append(str(Path(__file__).parent.parent.parent.parent)) + +import flask +from yarl import URL + +logger = logging.getLogger(__name__) + + +class Provider: + """ + Provider class to route message requests to message producer function. + + Sets up three endpoints: + - /_test/ping: A simple ping endpoint for testing. + - /produce_message: Route message requests to the handler function. + - /set_provider_state: Set the provider state. + + The specific `produce_message` and `set_provider_state` URLs can be configured + with the `produce_message_url` and `set_provider_state_url` arguments. + """ + + def __init__( # noqa: PLR0913 + self, + handler_module: str, + handler_function: str, + produce_message_url: str, + state_provider_module: str, + state_provider_function: str, + set_provider_state_url: str, + ) -> None: + """ + Initialize the provider. + + Args: + handler_module: + The name of the module containing the handler function. + handler_function: + The name of the handler function. + produce_message_url: + The URL to route message requests to the handler function. + state_provider_module: + The name of the module containing the state provider setup function. + state_provider_function: + The name of the state provider setup function. + set_provider_state_url: + The URL to set the provider state. + """ + self.app = flask.Flask("Provider") + self.handler_function = getattr(import_module(handler_module), handler_function) + self.produce_message_url = produce_message_url + self.set_provider_state_url = set_provider_state_url + if state_provider_module: + self.state_provider_function = getattr( + import_module(state_provider_module), state_provider_function + ) + + @self.app.get("/_test/ping") + def ping() -> str: + """Simple ping endpoint for testing.""" + return "pong" + + @self.app.route(self.produce_message_url, methods=["POST"]) + def produce_message() -> flask.Response | Tuple[str, int]: + """ + Route a message request to the handler function. + + Returns: + The response from the handler function. + """ + try: + body, content_type = self.handler_function() + return flask.Response( + response=body, + status=200, + content_type=content_type, + direct_passthrough=True, + ) + except Exception as e: # noqa: BLE001 + return str(e), 500 + + @self.app.route(self.set_provider_state_url, methods=["POST"]) + def set_provider_state() -> Tuple[str, int]: + """ + Calls the state provider function with the state provided in the request. + + Returns: + A response indicating that the state has been set. + """ + if self.state_provider_function: + self.state_provider_function(flask.request.args["state"]) + return "Provider state set", 200 + + def _find_free_port(self) -> int: + """ + Find a free port. + + This is used to find a free port to host the API on when running locally. It + is allocated, and then released immediately so that it can be used by the + API. + + Returns: + The port number. + """ + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + s.bind(("", 0)) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return s.getsockname()[1] + + def run(self) -> None: + """ + Start the provider. + """ + url = URL(f"http://localhost:{self._find_free_port()}") + sys.stderr.write(f"Starting provider on {url}\n") + + self.app.run( + host=url.host, + port=url.port, + debug=True, + ) + + +@contextmanager +def start_provider(**kwargs: str) -> Generator[URL, None, None]: # noqa: C901 + """ + Start the provider app. + + Expects kwargs to to contain the following: + handler_module: Required. The name of the module containing + the handler function. + handler_function: Required. The name of the handler function. + produce_message_url: Optional. The URL to route message requests to + the handler function. + state_provider_module: Optional. The name of the module containing + the state provider setup function. + state_provider_function: Optional. The name of the state provider + setup function. + set_provider_state_url: Optional. The URL to set the provider state. + """ + process = subprocess.Popen( # noqa: S603 + [ + sys.executable, + Path(__file__), + kwargs.pop("handler_module"), + kwargs.pop("handler_function"), + kwargs.pop("produce_message_url", "/produce_message"), + kwargs.pop("state_provider_module", ""), + kwargs.pop("state_provider_function", ""), + kwargs.pop("set_provider_state_url", "/set_provider_state"), + ], + cwd=Path.cwd(), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + + pattern = re.compile(r" \* Running on (?P[^ ]+)") + while True: + if process.poll() is not None: + logger.error("Provider process exited with code %d", process.returncode) + logger.error( + "Provider stdout: %s", process.stdout.read() if process.stdout else "" + ) + logger.error( + "Provider stderr: %s", process.stderr.read() if process.stderr else "" + ) + msg = f"Provider process exited with code {process.returncode}" + raise RuntimeError(msg) + if ( + process.stderr + and (line := process.stderr.readline()) + and (match := pattern.match(line)) + ): + break + time.sleep(0.1) + + url = URL(match.group("url")) + logger.debug("Provider started on %s", url) + for _ in range(50): + try: + response = requests.get(str(url / "_test" / "ping"), timeout=1) + assert response.text == "pong" + break + except (requests.RequestException, AssertionError): + time.sleep(0.1) + continue + else: + msg = "Failed to ping provider" + raise RuntimeError(msg) + + def redirect() -> NoReturn: + while True: + if process.stdout: + while line := process.stdout.readline(): + logger.debug("Provider stdout: %s", line.strip()) + if process.stderr: + while line := process.stderr.readline(): + logger.debug("Provider stderr: %s", line.strip()) + + thread = Thread(target=redirect, daemon=True) + thread.start() + + try: + yield url + finally: + process.send_signal(signal.SIGINT) + + +if __name__ == "__main__": + import sys + + if len(sys.argv) < 5: # noqa: PLR2004 + sys.stderr.write( + f"Usage: {sys.argv[0]} " + f" " + ) + sys.exit(1) + + handler_module = sys.argv[1] + handler_function = sys.argv[2] + produce_message_url = sys.argv[3] + state_provider_module = sys.argv[4] + state_provider_function = sys.argv[5] + set_provider_state_url = sys.argv[6] + Provider( + handler_module, + handler_function, + produce_message_url, + state_provider_module, + state_provider_function, + set_provider_state_url, + ).run() diff --git a/examples/tests/v3/test_01_message_consumer.py b/examples/tests/v3/test_01_message_consumer.py index 9016be9b8..76f58c71a 100644 --- a/examples/tests/v3/test_01_message_consumer.py +++ b/examples/tests/v3/test_01_message_consumer.py @@ -65,7 +65,7 @@ def pact() -> Generator[Pact, None, None]: ``` """ - pact_dir = Path(Path(__file__).parent.parent / "pacts") + pact_dir = Path(Path(__file__).parent.parent.parent / "pacts") pact = Pact("v3_message_consumer", "v3_message_provider") log.info("Creating Message Pact with V3 specification") yield pact.with_specification("V3") @@ -136,7 +136,21 @@ def test_async_message_handler_write( "action": "WRITE", "path": "my_file.txt", "contents": "Hello, world!", - }) + }), + "application/json", + ) + .with_matching_rules( + { + "body": { + "$.path": { + "combine": "AND", + "matchers": [ + {"match": "type"}, + ], + } + } + }, + "Response", ) ) pact.verify(verifier, "Async") @@ -161,9 +175,19 @@ def test_async_message_handler_read( json.dumps({ "action": "READ", "path": "my_file.txt", - "contents": "Hello, world!", - }) + }), + "application/json", ) + .with_matching_rules({ + "body": { + "$.path": { + "combine": "AND", + "matchers": [ + {"match": "type"}, + ], + }, + } + }) ) pact.verify(verifier, "Async") diff --git a/examples/tests/v3/test_02_message_provider.py b/examples/tests/v3/test_02_message_provider.py new file mode 100644 index 000000000..472829c1e --- /dev/null +++ b/examples/tests/v3/test_02_message_provider.py @@ -0,0 +1,74 @@ +""" +Producer test of example message. + +This test will read a pact between the message handler and the message provider +and then validate the pact against the provider. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Tuple +from unittest.mock import MagicMock + +from examples.src.message_producer import FileSystemMessageProducer +from examples.tests.v3.provider_server import start_provider +from pact.v3 import Verifier + +PACT_DIR = (Path(__file__).parent.parent.parent / "pacts").resolve() + +responses: dict[str, dict[str, str]] = { + "a request to write test.txt": { + "function_name": "send_write_event", + }, + "a request to read test.txt": { + "function_name": "send_read_event", + }, +} + +CURRENT_STATE: str | None = None + + +def message_producer_function() -> Tuple[str, str]: + producer = FileSystemMessageProducer() + producer.queue = MagicMock() + + assert CURRENT_STATE is not None, "State is not set" + function_name = responses.get(CURRENT_STATE, {}).get("function_name") + assert function_name is not None, "Function name could not be found" + producer_function = getattr(producer, function_name) + + if producer_function.__name__ == "send_write_event": + producer_function("provider_file_name.txt", "Hello, world!") + elif producer_function.__name__ == "send_read_event": + producer_function("provider_file_name.txt") + + return producer.queue.send.call_args[0][0], "application/json" + + +def state_provider_function(state_name: str) -> None: + global CURRENT_STATE # noqa: PLW0603 + CURRENT_STATE = state_name + + +def test_producer() -> None: + """ + Test the message producer. + """ + with start_provider( + handler_module=__name__, + handler_function="message_producer_function", + state_provider_module=__name__, + state_provider_function="state_provider_function", + ) as provider_url: + verifier = ( + Verifier() + .set_state( + provider_url / "set_provider_state", + teardown=True, + ) + .set_info("provider", url=f"{provider_url}/produce_message") + .filter_consumers("v3_message_consumer") + .add_source(PACT_DIR / "v3_message_consumer-v3_message_provider.json") + ) + verifier.verify() From 502940531081e5d599a8d5500ba2e67385230b63 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 17 Jul 2024 11:04:48 +1000 Subject: [PATCH 0423/1376] chore(tests): replace stderr with logger The provider subprocess has an issue whereby logging is not inherited from the parent Python process. This has been fixed by passing an additional `log_level` argument which will configure logging (if not already enabled). This means that the `logger` instance can be used consistently throughout the codebase. A few minor log fixes are also included in this commit. Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/consumer.py | 11 +-- tests/v3/compatibility_suite/util/provider.py | 76 ++++++++++++++----- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/tests/v3/compatibility_suite/util/consumer.py b/tests/v3/compatibility_suite/util/consumer.py index 66033cd50..1126ed5c3 100644 --- a/tests/v3/compatibility_suite/util/consumer.py +++ b/tests/v3/compatibility_suite/util/consumer.py @@ -90,7 +90,7 @@ def _( pact.with_specification(version) for iid in ids: definition = interaction_definitions[iid] - logging.info("Adding interaction %s", iid) + logger.info("Adding interaction %s", iid) definition.add_to_pact(pact, f"interaction {iid}") with pact.serve(raises=False) as srv: @@ -122,7 +122,7 @@ def _( pact.with_specification(version) definition = interaction_definitions[iid] definition.update(**content[0]) - logging.info("Adding modified interaction %s", iid) + logger.info("Adding modified interaction %s", iid) definition.add_to_pact(pact, f"interaction {iid}") with pact.serve(raises=False) as srv: @@ -263,7 +263,7 @@ def _( """ A response is returned. """ - logging.info( + logger.info( "Request Information:\n%s", json.dumps( { @@ -277,7 +277,7 @@ def _( indent=2, ), ) - logging.info("Mismatches:\n%s", json.dumps(srv.mismatches, indent=2)) + logger.info("Mismatches:\n%s", json.dumps(srv.mismatches, indent=2)) assert response.status_code == code @@ -310,7 +310,8 @@ def _( response: requests.Response, content_type: str, ) -> None: - assert response.headers["Content-Type"] == content_type + assert "Content-Type" in response.headers, "Content-Type not set" + assert response.headers["Content-Type"] == content_type, "Content-Type mismatch" def the_mock_server_status_will_be(stacklevel: int = 1) -> None: diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index e5554b462..0f0e101d0 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -53,6 +53,7 @@ parse_headers, parse_markdown_table, serialize, + truncate, ) if TYPE_CHECKING: @@ -127,12 +128,35 @@ def _find_free_port() -> int: return s.getsockname()[1] +def _setup_logging(log_level: int) -> None: + """ + Set up logging for the provider. + + Pytest is responsible for setting up the logging for the main Python + process, but the provider runs in a subprocess and does not automatically + inherit the logging configuration. + + This function sets up the logging within the provider subprocess, provided + that it wasn't already set up (in case any logging configuration is + inherited). + """ + if logging.getLogger().handlers: + return + + logging.basicConfig( + level=log_level, + format="%(asctime)s.%(msec)03d [%(levelname)s] %(name)s: %(message)s", + datefmt="%H:%M:%S", + ) + logger.debug("Debug logging enabled") + + class Provider: """ HTTP Provider. """ - def __init__(self, provider_dir: Path | str) -> None: + def __init__(self, provider_dir: Path | str, log_level: int) -> None: """ Instantiate a new provider. @@ -142,8 +166,12 @@ def __init__(self, provider_dir: Path | str) -> None: provider. At a minimum, this directory must contain a file called `interactions.pkl`. This file must contain a list of [`InteractionDefinition`] objects. + + log_level: + The log level for the provider. """ self._messages = {} + _setup_logging(log_level) self.provider_dir = Path(provider_dir) if not self.provider_dir.is_dir(): msg = f"Directory {self.provider_dir} does not exist" @@ -191,13 +219,17 @@ def callback() -> tuple[str, int] | str: provider_states_path = self.provider_dir / "provider_states" if provider_states_path.exists(): + logger.debug("Provider states file found") with provider_states_path.open() as f: states = [InteractionDefinition.State(**s) for s in json.load(f)] + logger.debug("Provider states: %s", states) for state in states: if request.args["state"] == state.name: + logger.debug("State found: %s", state) for k, v in state.parameters.items(): assert k in request.args assert str(request.args[k]) == str(v) + logger.debug("State parameters match") break else: msg = "State not found" @@ -216,7 +248,7 @@ def callback() -> tuple[str, int] | str: "query_params": serialize(request.args), "headers_list": serialize(request.headers), "headers_dict": serialize(dict(request.headers)), - "body": request.data.decode("utf-8"), + "body": request.data.decode("utf-8", errors="backslashreplace"), "form": serialize(request.form), }, f, @@ -234,12 +266,11 @@ def _add_after_request(self, app: flask.Flask) -> None: @app.after_request def log_request(response: flask.Response) -> flask.Response: - sys.stderr.write(f"START REQUEST: {request.method} {request.path}\n") - sys.stderr.write(f"Query string: {request.query_string.decode('utf-8')}\n") - sys.stderr.write(f"Header: {serialize(request.headers)}\n") - sys.stderr.write(f"Body: {request.data.decode('utf-8')}\n") - sys.stderr.write(f"Form: {serialize(request.form)}\n") - sys.stderr.write("END REQUEST\n") + logger.debug("Received request: %s %s", request.method, request.path) + logger.debug("-> Query string: %s", request.query_string.decode("utf-8")) + logger.debug("-> Headers: %s", serialize(request.headers)) + logger.debug("-> Body: %s", truncate(request.get_data().decode("utf-8"))) + logger.debug("-> Form: %s", serialize(request.form)) with ( self.provider_dir @@ -253,7 +284,7 @@ def log_request(response: flask.Response) -> flask.Response: "query_params": serialize(request.args), "headers_list": serialize(request.headers), "headers_dict": serialize(dict(request.headers)), - "body": request.data.decode("utf-8"), + "body": request.data.decode("utf-8", errors="backslashreplace"), "form": serialize(request.form), }, f, @@ -262,12 +293,14 @@ def log_request(response: flask.Response) -> flask.Response: @app.after_request def log_response(response: flask.Response) -> flask.Response: - sys.stderr.write(f"START RESPONSE: {response.status_code}\n") - sys.stderr.write(f"Headers: {serialize(response.headers)}\n") - sys.stderr.write( - f"Body: {response.get_data().decode('utf-8', errors='replace')}\n" + logger.debug("Returning response: %d", response.status_code) + logger.debug("-> Headers: %s", serialize(response.headers)) + logger.debug( + "-> Body: %s", + truncate( + response.get_data().decode("utf-8", errors="backslashreplace") + ), ) - sys.stderr.write("END RESPONSE\n") with ( self.provider_dir @@ -278,7 +311,9 @@ def log_response(response: flask.Response) -> flask.Response: "status_code": response.status_code, "headers_list": serialize(response.headers), "headers_dict": serialize(dict(response.headers)), - "body": response.get_data().decode("utf-8", errors="replace"), + "body": response.get_data().decode( + "utf-8", errors="backslashreplace" + ), }, f, ) @@ -516,11 +551,11 @@ def latest_verification_results(self) -> requests.Response | None: if __name__ == "__main__": import sys - if len(sys.argv) != 2: - sys.stderr.write(f"Usage: {sys.argv[0]} ") + if len(sys.argv) != 3: + sys.stderr.write(f"Usage: {sys.argv[0]} \n") sys.exit(1) - Provider(sys.argv[1]).run() + Provider(sys.argv[1], sys.argv[2]).run() ################################################################################ @@ -643,6 +678,7 @@ def start_provider(provider_dir: str | Path) -> Generator[URL, None, None]: # n sys.executable, Path(__file__), str(provider_dir), + str(logger.getEffectiveLevel()), ], cwd=Path.cwd(), stdout=subprocess.PIPE, @@ -688,10 +724,10 @@ def redirect() -> NoReturn: while True: if process.stdout: while line := process.stdout.readline(): - logger.debug("Provider stdout: %s", line.strip()) + logger.debug("Provider stdout: %s", line.rstrip()) if process.stderr: while line := process.stderr.readline(): - logger.debug("Provider stderr: %s", line.strip()) + logger.debug("Provider stderr: %s", line.rstrip()) thread = Thread(target=redirect, daemon=True) thread.start() From bb8c3055557c7cf7875d09332139515386d17bc4 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 17 Jul 2024 11:10:04 +1000 Subject: [PATCH 0424/1376] chore(tests): increase message shown by `truncate` Truncating to the first and last 6 bytes/chars was far too little to be useful. Bumping both up to 128. Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 1dcb18bec..ee85eb249 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -132,7 +132,7 @@ def truncate(data: str | bytes) -> str: This is useful for printing large strings or bytes objects in tests. """ - if len(data) <= 32: + if len(data) <= 256: if isinstance(data, str): return f"{data}" return data.decode("utf-8", "backslashreplace") @@ -142,9 +142,9 @@ def truncate(data: str | bytes) -> str: checksum = hashlib.sha256(data.encode()).hexdigest() return ( '"' - + data[:6] + + data[:128] + "⋯" - + data[-6:] + + data[-128:] + '"' + f" ({length} bytes, sha256={checksum[:7]})" ) From d1d8ed17237f761ac5d7561c9148c3e7143ac389 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 17 Jul 2024 11:11:01 +1000 Subject: [PATCH 0425/1376] chore: minor typing fix Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index ee85eb249..479b202cf 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -187,7 +187,7 @@ def parse_markdown_table(content: str) -> list[dict[str, str]]: return [dict(zip(rows[0], row)) for row in rows[1:]] -def parse_horizontal_markdown_table(content: str) -> list[dict[str, str]]: +def parse_horizontal_markdown_table(content: str) -> dict[str, str]: """ Parse a Markdown table into a list of dictionaries. From 941d6bb7f7a803d5af2777f649439df1c84d6631 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 17 Jul 2024 11:12:14 +1000 Subject: [PATCH 0426/1376] refactor(tests): make `_add_body` a method of Body Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/__init__.py | 105 ++++++++---------- 1 file changed, 49 insertions(+), 56 deletions(-) diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 479b202cf..e8e4bb082 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -318,7 +318,7 @@ class Body: - An XML document """ - def __init__(self, data: str | bytes) -> None: + def __init__(self, data: str) -> None: """ Instantiate the interaction body. """ @@ -326,10 +326,6 @@ def __init__(self, data: str | bytes) -> None: self.bytes: bytes | None = None self.mime_type: str | None = None - if isinstance(data, bytes): - self.bytes = data - return - if data.startswith("file: ") and data.endswith("-body.xml"): self.parse_fixture(FIXTURES_ROOT / data[6:]) return @@ -419,6 +415,54 @@ def parse_file(self, file: Path) -> None: msg = "Unknown file type" raise ValueError(msg) + def add_to_interaction( + self, + interaction: Interaction, + ) -> None: + """ + Add a body to the interaction. + + This is a helper method that adds the body to the interaction. This + relies on Pact's intelligent understanding of whether it is dealing with + a request or response (which is determined through the use of + `will_respond_with`). + + Args: + body: + The body to add to the interaction. + + interaction: + The interaction to add the body to. + + """ + if self.string: + logger.info( + "with_body(%r, %r)", + truncate(self.string), + self.mime_type, + ) + interaction.with_body( + self.string, + self.mime_type, + ) + elif self.bytes: + logger.info( + "with_binary_body(%r, %r)", + truncate(self.bytes), + self.mime_type, + ) + interaction.with_binary_body( + self.bytes, + self.mime_type, + ) + else: + msg = "Unexpected body definition" + raise RuntimeError(msg) + + if self.mime_type and isinstance(interaction, HttpInteraction): + logger.info('set_header("Content-Type", %r)', self.mime_type) + interaction.set_header("Content-Type", self.mime_type) + class State: """ Provider state. @@ -582,57 +626,6 @@ def __repr__(self) -> str: ", ".join(f"{k}={v!r}" for k, v in vars(self).items()), ) - def _add_body( - self, body: InteractionDefinition.Body, interaction: Interaction - ) -> None: - if body.mime_type == "application/xml": - - def _element_to_json(element: ElementTree.Element) -> dict[str, Any]: - json_dict = { - "name": element.tag, - } - if element.attrib: - json_dict["attributes"] = element.attrib - if len(element): - json_dict["children"] = [ - _element_to_json(child) for child in element - ] - else: - json_dict["children"] = [{"content": element.text}] - return json_dict - - with suppress(ElementTree.ParseError): - # try to parse the content as XML - # it _may_ be JSON, so it's ok if this errors - body.string = json.dumps({ - "root": _element_to_json( - ElementTree.fromstring(body.string) # noqa: S314 - ) - }) - if body.string: - logger.info( - "with_body(%s, %s)", - truncate(body.string), - body.mime_type, - ) - interaction.with_body( - body.string, - body.mime_type, - ) - elif body.bytes: - logger.info( - "with_binary_file(%s, %s)", - truncate(body.bytes), - body.mime_type, - ) - interaction.with_binary_body( - body.bytes, - body.mime_type, - ) - else: - msg = "Unexpected body definition" - raise RuntimeError(msg) - def add_to_pact( # noqa: C901, PLR0912, PLR0915 self, pact: Pact, From 18aeb4e09bfcf405b16b9615e34609b77d015617 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 17 Jul 2024 11:17:18 +1000 Subject: [PATCH 0427/1376] chore(tests): significant refactor of InteractionDefinition The `InteractionDefinition` class and the way it works with the `Provider` class during testing has been fairly signifcantly refactored. This should hopefully make it clearer and easier to maintaing going forward. Signed-off-by: JP-Ellis --- .../test_v3_message_producer.py | 121 +++--- tests/v3/compatibility_suite/util/__init__.py | 365 ++++++++++++++---- tests/v3/compatibility_suite/util/provider.py | 99 ++--- 3 files changed, 392 insertions(+), 193 deletions(-) diff --git a/tests/v3/compatibility_suite/test_v3_message_producer.py b/tests/v3/compatibility_suite/test_v3_message_producer.py index 56ec0ace7..31d87d0e8 100644 --- a/tests/v3/compatibility_suite/test_v3_message_producer.py +++ b/tests/v3/compatibility_suite/test_v3_message_producer.py @@ -7,7 +7,7 @@ import pickle import re from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Generator import pytest from pytest_bdd import ( @@ -24,15 +24,19 @@ ) from tests.v3.compatibility_suite.util.provider import ( a_provider_is_started_that_can_generate_the_message, + a_provider_state_callback_is_configured, + start_provider, the_provider_state_callback_will_be_called_after_the_verification_is_run, the_provider_state_callback_will_be_called_before_the_verification_is_run, the_provider_state_callback_will_receive_a_setup_call, - the_verification_is_run_with_start_context, + the_verification_is_run, the_verification_results_will_contain_a_error, the_verification_will_be_successful, ) if TYPE_CHECKING: + from yarl import URL + from pact.v3.verifier import Verifier TEST_PACT_FILE_DIRECTORY = Path(Path(__file__).parent / "pacts") @@ -191,6 +195,7 @@ def test_verifying_multiple_pact_files() -> None: a_provider_is_started_that_can_generate_the_message() +a_provider_state_callback_is_configured() @given( @@ -205,25 +210,26 @@ def a_pact_file_for_is_to_be_verified_with_the_following( verifier: Verifier, temp_dir: Path, name: str, - table: dict[str, str], + table: dict[str, str | dict[str, str]], ) -> None: - """A Pact file for "basic" is to be verified with the following.""" - metadata = {} - if table.get("metadata"): - - def _repl(x: str) -> tuple[str, str]: - return (z.replace("JSON: ", "") for z in x.split("=")) - - metadata = dict(_repl(x) for x in table["metadata"].split("; ")) + """ + A Pact file for "basic" is to be verified with the following. + """ pact = Pact("consumer", "provider") pact.with_specification("V3") + + if "metadata" in table: + assert isinstance(table["metadata"], str) + metadata = { + k: json.loads(v.replace("JSON: ", "")) if v.startswith("JSON: ") else v + for k, _, v in (s.partition("=") for s in table["metadata"].split("; ")) + } + table["metadata"] = metadata + interaction_definition = InteractionDefinition( - method="POST", - path=f"/{name}", - metadata=metadata, - response_body=table["body"], - matching_rules=table.get("matching rules"), type="Async", + description=name, + **table, ) interaction_definition.add_to_pact(pact, name) (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) @@ -231,31 +237,36 @@ def _repl(x: str) -> tuple[str, str]: verifier.add_source(temp_dir / "pacts") -@given(parsers.parse('a Pact file for "{name}":"{fixture}" is to be verified')) +@given( + parsers.re( + r'a Pact file for "(?P[^"]+)":"(?P[^"]+)" is to be verified' + ) +) def a_pact_file_for_is_to_be_verified( - verifier: Verifier, temp_dir: Path, name: str, fixture: str + verifier: Verifier, + temp_dir: Path, + name: str, + fixture: str, ) -> None: pact = Pact("consumer", "provider") pact.with_specification("V3") interaction_definition = InteractionDefinition( - method="POST", - path=f"/{name}", - response_body=fixture, type="Async", + description=name, + body=fixture, ) - # for plain text message, the mime type needs to be set - if not re.match(r"^(file:|JSON:)", fixture): - interaction_definition.response_body.mime_type = "text/html;charset=utf-8" interaction_definition.add_to_pact(pact, name) (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) pact.write_file(temp_dir / "pacts") + with (temp_dir / "pacts" / "consumer-provider.json").open() as f: + logger.debug("Pact file contents: %s", f.read()) verifier.add_source(temp_dir / "pacts") @given( - parsers.parse( - 'a Pact file for "{name}":"{fixture}" is to be ' - 'verified with provider state "{provider_state}"' + parsers.re( + r'a Pact file for "(?P[^"]+)":"(?P[^"]+)"' + r' is to be verified with provider state "(?P[^"]+)"' ) ) def a_pact_file_for_is_to_be_verified_with_provider_state( @@ -269,13 +280,11 @@ def a_pact_file_for_is_to_be_verified_with_provider_state( pact = Pact("consumer", "provider") pact.with_specification("V3") interaction_definition = InteractionDefinition( - method="POST", - path=f"/{name}", - response_body=fixture, type="Async", + description=name, + body=fixture, ) - states = [InteractionDefinition.State(provider_state)] - interaction_definition.states = states + interaction_definition.states = [InteractionDefinition.State(provider_state)] interaction_definition.add_to_pact(pact, name) (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) pact.write_file(temp_dir / "pacts") @@ -301,17 +310,21 @@ def a_pact_file_for_is_to_be_verified_with_the_following_metadata( verifier: Verifier, name: str, fixture: str, - metadata: dict[str, str], + metadata: list[dict[str, str]], ) -> None: """A Pact file is to be verified with the following metadata.""" pact = Pact("consumer", "provider") pact.with_specification("V3") interaction_definition = InteractionDefinition( - method="POST", - path=f"/{name}", - metadata={h["key"]: h["value"].replace("JSON: ", "") for h in metadata}, - response_body=fixture, type="Async", + description=name, + body=fixture, + metadata={ + row["key"]: json.loads(row["value"].replace("JSON: ", "")) + if row["value"].startswith("JSON: ") + else row["value"] + for row in metadata + }, ) interaction_definition.add_to_pact(pact, name) (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) @@ -327,41 +340,37 @@ def a_pact_file_for_is_to_be_verified_with_the_following_metadata( re.DOTALL, ), converters={"metadata": parse_markdown_table}, + target_fixture="provider_url", ) def a_provider_is_started_that_can_generate_the_message_with_the_following_metadata( temp_dir: Path, name: str, fixture: str, - metadata: dict[str, str], -) -> None: + metadata: list[dict[str, str]], +) -> Generator[URL, None, None]: """A provider is started that can generate the message with the following metadata.""" # noqa: E501 - interaction_definitions = [] + interaction_definitions: list[InteractionDefinition] = [] if (temp_dir / "interactions.pkl").exists(): with (temp_dir / "interactions.pkl").open("rb") as pkl_file: interaction_definitions = pickle.load(pkl_file) # noqa: S301 - def parse_metadata_value(value: str) -> str: - return ( - json.loads(value.replace("JSON: ", "")) - if value.startswith("JSON: ") - else value - ) - interaction_definition = InteractionDefinition( - method="POST", - path=f"/{name}", - metadata={m["key"]: parse_metadata_value(m["value"]) for m in metadata}, - response_body=fixture, type="Async", + description=name, + body=fixture, + metadata={ + row["key"]: json.loads(row["value"].replace("JSON: ", "")) + if row["value"].startswith("JSON: ") + else row["value"] + for row in metadata + }, ) interaction_definitions.append(interaction_definition) + with (temp_dir / "interactions.pkl").open("wb") as pkl_file: pickle.dump(interaction_definitions, pkl_file) - -@given("a provider state callback is configured", target_fixture="callback") -def a_provider_state_callback_is_configured() -> None: - """A provider state callback is configured.""" + yield from start_provider(temp_dir) ################################################################################ @@ -369,7 +378,7 @@ def a_provider_state_callback_is_configured() -> None: ################################################################################ -the_verification_is_run_with_start_context() +the_verification_is_run() ################################################################################ diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index e8e4bb082..867ac5895 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -29,10 +29,9 @@ def _(): import sys import typing from collections.abc import Collection, Mapping -from contextlib import suppress from datetime import date, datetime, time from pathlib import Path -from typing import Any, Generic, TypeVar +from typing import Any, Generic, Literal, TypeVar from xml.etree import ElementTree import flask @@ -41,6 +40,8 @@ def _(): from typing_extensions import Self from yarl import URL +from pact.v3.interaction import HttpInteraction, Interaction + if typing.TYPE_CHECKING: from pact.v3.interaction import Interaction from pact.v3.pact import Pact @@ -506,43 +507,147 @@ def from_dict(cls, data: dict[str, Any]) -> Self: """ return cls(**data) - def __init__(self, **kwargs: str) -> None: - """Initialise the interaction definition.""" - self.id: int | None = None + def __init__(self, metadata: dict[str, Any] | None = None, **kwargs: str) -> None: + """ + Initialise the interaction definition. + + As the interaction definitions are parsed from a Markdown table, + values are expected to be strings and must be converted to the correct + type. + + The _only_ exception to this is the `metadata` key, which expects + a dictionary. + """ + # A common pattern used in the tests is to have a table with the 'base' + # definitions, and have tests modify these definitions as need be. As a + # result, the `__init__` method is designed to set all the values to + # defaults, and the `update` method is used to update the values. + if type_ := kwargs.pop("type", "HTTP"): + if type_ not in ("HTTP", "Sync", "Async"): + msg = f"Invalid value for 'type': {type_}" + raise ValueError(msg) + self.type: Literal["HTTP", "Sync", "Async"] = type_ + # General properties shared by all interaction types + self.id: int | None = None + self.description: str | None = None self.states: list[InteractionDefinition.State] = [] + self.metadata: dict[str, Any] | None = None self.pending: bool = False + self.is_pending: bool = False + self.test_name: str | None = None self.text_comments: list[str] = [] self.comments: dict[str, str] = {} - self.test_name: str | None = None - self.method: str = kwargs.pop("method") - self.path: str = kwargs.pop("path") - self.response: int = int(kwargs.pop("response", 200)) + # Request properties + self.method: str | None = None + self.path: str | None = None + self.response: int | None = None self.query: str | None = None self.headers: MultiDict[str] = MultiDict() self.body: InteractionDefinition.Body | None = None + self.matching_rules: str | None = None + # Response properties self.response_headers: MultiDict[str] = MultiDict() self.response_body: InteractionDefinition.Body | None = None - self.matching_rules: str | None = None self.response_matching_rules: str | None = None - self.metadata: dict[str, Any] | None = None - self.is_pending: bool = kwargs.pop("is_pending", False) - self.type: typing.Literal["HTTP", "Sync", "Async"] = kwargs.pop("type", "HTTP") - self.update(**kwargs) + self.update(metadata=metadata, **kwargs) - def update(self, **kwargs: str) -> None: # noqa: C901, PLR0912 + def update(self, metadata: dict[str, Any] | None = None, **kwargs: str) -> None: """ Update the interaction definition. This is a convenience method that allows the interaction definition to be updated with new values. """ + kwargs = self._update_shared(metadata, **kwargs) + kwargs = self._update_request(**kwargs) + kwargs = self._update_response(**kwargs) + + if len(kwargs) > 0: + msg = f"Unexpected arguments: {kwargs.keys()}" + raise TypeError(msg) + + def _update_shared( + self, + metadata: dict[str, Any] | None = None, + **kwargs: str, + ) -> dict[str, str]: + """ + Update the shared properties of the interaction. + + Note that the following properties are not supported and must be + modified directly: + + - `states` + - `text_comments` + - `comments` + + Args: + metadata: + Metadata for the interaction. + + kwargs: + Remaining keyword arguments, which are: + + - `No`: Interaction ID. Used purely for debugging purposes. + - `description`: Description of the interaction (used by + asynchronous messages) + - `pending`: Whether the interaction is pending. + - `test_name`: Test name for the interaction. + + Returns: + The remaining keyword arguments. + """ if interaction_id := kwargs.pop("No", None): self.id = int(interaction_id) + if description := kwargs.pop("description", None): + self.description = description + + if "states" in kwargs: + msg = "Unsupported. Modify the 'states' property directly." + raise ValueError(msg) + + if metadata: + self.metadata = metadata + + if "pending" in kwargs: + self.pending = kwargs.pop("pending") == "true" + + if test_name := kwargs.pop("test_name", None): + self.test_name = test_name + + if "text_comments" in kwargs: + msg = "Unsupported. Modify the 'text_comments' property directly." + raise ValueError(msg) + + if "comments" in kwargs: + msg = "Unsupported. Modify the 'comments' property directly." + raise ValueError(msg) + + return kwargs + + def _update_request(self, **kwargs: str) -> dict[str, str]: + """ + Update the request properties of the interaction. + + Args: + kwargs: + Remaining keyword arguments, which are: + + - `method`: Request method. + - `path`: Request path. + - `query`: Query parameters. + - `headers`: Request headers. + - `raw_headers`: Request headers. + - `body`: Request body. + - `content_type`: Request content type. + - `matching_rules`: Request matching rules. + + """ if method := kwargs.pop("method", None): self.method = method @@ -574,6 +679,30 @@ def update(self, **kwargs: str) -> None: # noqa: C901, PLR0912 self.body = InteractionDefinition.Body("") self.body.mime_type = content_type + if matching_rules := ( + kwargs.pop("matching_rules", None) or kwargs.pop("matching rules", None) + ): + self.matching_rules = parse_matching_rules(matching_rules) + + return kwargs + + def _update_response(self, **kwargs: str) -> dict[str, str]: + """ + Update the response properties of the interaction. + + Args: + kwargs: + Remaining keyword arguments, which are: + + - `response`: Response status code. + - `response_headers`: Response headers. + - `response_content`: Response content type. + - `response_body`: Response body. + - `response_matching_rules`: Response matching rules. + + Returns: + The remaining keyword arguments. + """ if response := kwargs.pop("response", None) or kwargs.pop("status", None): self.response = int(response) @@ -600,23 +729,13 @@ def update(self, **kwargs: str) -> None: # noqa: C901, PLR0912 self.response_body.mime_type or orig_content_type ) - if matching_rules := ( - kwargs.pop("matching_rules", None) or kwargs.pop("matching rules", None) - ): - self.matching_rules = parse_matching_rules(matching_rules) - if matching_rules := ( kwargs.pop("response_matching_rules", None) or kwargs.pop("response matching rules", None) ): self.response_matching_rules = parse_matching_rules(matching_rules) - if metadata := kwargs.pop("metadata", None): - self.metadata = metadata - - if len(kwargs) > 0: - msg = f"Unexpected arguments: {kwargs.keys()}" - raise TypeError(msg) + return kwargs def __repr__(self) -> str: """ @@ -646,16 +765,19 @@ def add_to_pact( # noqa: C901, PLR0912, PLR0915 Name for this interaction. Must be unique for the pact. """ interaction = pact.upon_receiving(name, self.type) - logger.info("with_request(%s, %s, %s)", self.method, self.path, self.type) - if self.type != "Async": + if isinstance(interaction, HttpInteraction): + assert self.method, "Method must be defined" + assert self.path, "Path must be defined" + + logger.info("with_request(%r, %r)", self.method, self.path) interaction.with_request(self.method, self.path) for state in self.states or []: if state.parameters: - logger.info("given(%s, parameters=%s)", state.name, state.parameters) + logger.info("given(%r, parameters=%r)", state.name, state.parameters) interaction.given(state.name, parameters=state.parameters) else: - logger.info("given(%s)", state.name) + logger.info("given(%r)", state.name) interaction.given(state.name) if self.pending: @@ -664,56 +786,69 @@ def add_to_pact( # noqa: C901, PLR0912, PLR0915 if self.text_comments: for comment in self.text_comments: - logger.info("add_text_comment(%s)", comment) + logger.info("add_text_comment(%r)", comment) interaction.add_text_comment(comment) for key, value in self.comments.items(): - logger.info("set_comment(%s, %s)", key, value) + logger.info("set_comment(%r, %r)", key, value) interaction.set_comment(key, value) if self.test_name: - logger.info("test_name(%s)", self.test_name) + logger.info("test_name(%r)", self.test_name) interaction.test_name(self.test_name) if self.query: + assert isinstance( + interaction, HttpInteraction + ), "Query parameters require an HTTP interaction" query = URL.build(query_string=self.query).query - logger.info("with_query_parameters(%s)", query.items()) + logger.info("with_query_parameters(%r)", query.items()) interaction.with_query_parameters(query.items()) if self.headers: - logger.info("with_headers(%s)", self.headers.items()) + assert isinstance( + interaction, HttpInteraction + ), "Headers require an HTTP interaction" + logger.info("with_headers(%r)", self.headers.items()) interaction.with_headers(self.headers.items()) if self.body: - self._add_body(self.body, interaction) + self.body.add_to_interaction(interaction) if self.matching_rules: - logger.info("with_matching_rules(%s)", self.matching_rules) + logger.info("with_matching_rules(%r)", self.matching_rules) interaction.with_matching_rules(self.matching_rules) if self.response: - logger.info("will_respond_with(%s)", self.response) - if self.type != "Async": - interaction.will_respond_with(self.response) + assert isinstance( + interaction, HttpInteraction + ), "Response requires an HTTP interaction" + logger.info("will_respond_with(%r)", self.response) + interaction.will_respond_with(self.response) if self.response_headers: - logger.info("with_headers(%s)", self.response_headers) + assert isinstance( + interaction, HttpInteraction + ), "Response headers require an HTTP interaction" + logger.info("with_headers(%r)", self.response_headers) interaction.with_headers(self.response_headers.items()) if self.response_body: - self._add_body(self.response_body, interaction) + assert isinstance( + interaction, HttpInteraction + ), "Response body requires an HTTP interaction" + self.response_body.add_to_interaction(interaction) if self.response_matching_rules: - logger.info("with_matching_rules(%s)", self.response_matching_rules) + logger.info("with_matching_rules(%r)", self.response_matching_rules) interaction.with_matching_rules(self.response_matching_rules) if self.metadata: for key, value in self.metadata.items(): - interaction.with_metadata({key: value}) - - if self.is_pending: - logger.info("set_pending(True)") - interaction.set_pending(pending=True) + if isinstance(value, str): + interaction.with_metadata({key: value}) + else: + interaction.with_metadata({key: json.dumps(value)}) def add_to_flask(self, app: flask.Flask) -> None: """ @@ -723,18 +858,45 @@ def add_to_flask(self, app: flask.Flask) -> None: app: The Flask app to add the interaction to. """ - sys.stderr.write( - f"Adding interaction to Flask app: {self.method} {self.path}\n" + logger.debug("Adding %s interaction to Flask app", self.type) + if self.type == "HTTP": + self._add_http_to_flask(app) + elif self.type == "Sync": + self._add_sync_to_flask(app) + elif self.type == "Async": + self._add_async_to_flask(app) + else: + msg = f"Unknown interaction type: {self.type}" + raise ValueError(msg) + + def _add_http_to_flask(self, app: flask.Flask) -> None: + """ + Add a HTTP interaction to a Flask app. + + Ths function works by defining a new function to handle the request and + produce the response. This function is then added to the Flask app as a + route. + + Args: + app: + The Flask app to add the interaction to. + """ + assert isinstance(self.method, str), "Method must be a string" + assert isinstance(self.path, str), "Path must be a string" + + logger.info( + "Adding HTTP '%s %s' interaction to Flask app", + self.method, + self.path, ) - sys.stderr.write(f" Query: {self.query}\n") - sys.stderr.write(f" Headers: {self.headers}\n") - sys.stderr.write(f" Body: {self.body}\n") - sys.stderr.write(f" Response: {self.response}\n") - sys.stderr.write(f" Response headers: {self.response_headers}\n") - sys.stderr.write(f" Response body: {self.response_body}\n") + logger.debug("-> Query: %s", self.query) + logger.debug("-> Headers: %s", self.headers) + logger.debug("-> Body: %s", self.body) + logger.debug("-> Response Status: %s", self.response) + logger.debug("-> Response Headers: %s", self.response_headers) + logger.debug("-> Response Body: %s", self.response_body) def route_fn() -> flask.Response: - sys.stderr.write(f"Received request: {self.method} {self.path}\n") if self.query: query = URL.build(query_string=self.query).query # Perform a two-way check to ensure that the query parameters @@ -778,21 +940,82 @@ def route_fn() -> flask.Response: methods=[self.method], ) - def create_message_response(self) -> flask.Response: - """Creates a flask response for an async message.""" + def _add_sync_to_flask(self, app: flask.Flask) -> None: + """ + Add a synchronous message interaction to a Flask app. + + Args: + app: + The Flask app to add the interaction to. + """ + raise NotImplementedError + + def _add_async_to_flask(self, app: flask.Flask) -> None: + """ + Add a synchronous message interaction to a Flask app. + + Args: + app: + The Flask app to add the interaction to. + """ + assert self.description, "Description must be set for async messages" + if hasattr(app, "pact_messages"): + app.pact_messages[self.description] = self + else: + app.pact_messages = {self.description: self} + + # All messages are handled by the same route. So we just need to check + # whether the route has been defined, and if not, define it. + for rule in app.url_map.iter_rules(): + if rule.rule == "/_pact/message": + sys.stderr.write("Async message route already defined\n") + return + + sys.stderr.write("Adding async message route\n") + + @app.post("/_pact/message") + def post_message() -> flask.Response: + sys.stderr.write("Received POST request for message\n") + assert hasattr(app, "pact_messages"), "No messages defined" + assert isinstance(app.pact_messages, dict), "Messages must be a dictionary" + + body: dict[str, Any] = json.loads(request.data) + description: str = body["description"] + + if description not in app.pact_messages: + return flask.Response( + response=json.dumps({ + "error": f"Message {description} not found", + }), + status=404, + headers={"Content-Type": "application/json"}, + content_type="application/json", + ) + + interaction: InteractionDefinition = app.pact_messages[description] + return interaction.create_async_message_response() + + def create_async_message_response(self) -> flask.Response: + """ + Convert the interaction to a Flask response. + + When an async message needs to be produced, Pact expects the response + from the special `/_pact/message` endppoint to generate the expected + message. + + Whilst this is a Response from Flask's perspective, the attributes + returned + """ + assert self.type == "Async", "Only async messages are supported" + if self.metadata: - self.response_headers.add( - "Pact-Message-Metadata", - base64.b64encode(json.dumps(self.metadata).encode("utf-8")).decode( - "utf-8" - ), - ) + self.headers["Pact-Message-Metadata"] = base64.b64encode( + json.dumps(self.metadata).encode("utf-8") + ).decode("utf-8") + return flask.Response( - response=self.response_body.bytes or self.response_body.string or None - if self.response_body - else None, - status=self.response, - headers=dict(**self.response_headers), - content_type=self.response_body.mime_type if self.response_body else None, + response=self.body.bytes or self.body.string or None if self.body else None, + headers=((k, v) for k, v in self.headers.items()), + content_type=self.body.mime_type if self.body else None, direct_passthrough=True, ) diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 0f0e101d0..4288230b8 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -170,8 +170,9 @@ def __init__(self, provider_dir: Path | str, log_level: int) -> None: log_level: The log level for the provider. """ - self._messages = {} _setup_logging(log_level) + + self.messages: dict[str, InteractionDefinition] = {} self.provider_dir = Path(provider_dir) if not self.provider_dir.is_dir(): msg = f"Directory {self.provider_dir} does not exist" @@ -212,7 +213,7 @@ def _add_callback(self, app: flask.Flask) -> None: contents of the file. """ - @app.route("/_test/callback", methods=["GET", "POST"]) + @app.route("/_pact/callback", methods=["GET", "POST"]) def callback() -> tuple[str, int] | str: if (self.provider_dir / "fail_callback").exists(): return "Provider state not found", 404 @@ -327,26 +328,7 @@ def _add_interactions(self, app: flask.Flask) -> None: interactions: list[InteractionDefinition] = pickle.load(f) # noqa: S301 for interaction in interactions: - if interaction.type != "Async": - interaction.add_to_flask(app) - else: - self._messages[interaction.path] = interaction - - @app.route("/message_handler", methods=["GET", "POST"]) - def handle_messages() -> flask.Response: - body = json.loads(request.data.decode("utf-8")) - message = self._messages.get("/" + body.get("description", "")) - if message: - return message.create_message_response() - return flask.Response( - response=json.dumps({ - "error": f"Message {body.get('description')} not found" - }), - status=404, - headers={"Content-Type": "application/json"}, - content_type="application/json", - direct_passthrough=True, - ) + interaction.add_to_flask(app) def run(self) -> None: """ @@ -644,31 +626,39 @@ def a_provider_is_started_that_can_generate_the_message( stacklevel: int = 1, ) -> None: @given( - parsers.parse( - 'a provider is started that can generate the "{name}" message with "{body}"' + parsers.re( + r"a provider is started" + r' that can generate the "(?P[^"]+)" message' + r' with "(?P.+)"$' ), + target_fixture="provider_url", stacklevel=stacklevel + 1, ) def _( temp_dir: Path, name: str, body: str, - ) -> None: - interaction_definitions = [] - if (temp_dir / "interactions.pkl").exists(): - with (temp_dir / "interactions.pkl").open("rb") as pkl_file: - interaction_definitions = pickle.load(pkl_file) # noqa: S301 - - body = body.replace('\\"', '"') - interaction_definition = InteractionDefinition( - method="POST", - path=f"/{name}", - response_body=body, + ) -> Generator[URL, None, None]: + interactions: list[InteractionDefinition] = [] + interactions_pkl = temp_dir / "interactions.pkl" + if interactions_pkl.exists(): + with interactions_pkl.open("rb") as f: + interactions = pickle.load(f) # noqa: S301 + + interaction = InteractionDefinition( type="Async", + description=name, + body=body.replace(r"\"", '"'), ) - interaction_definitions.append(interaction_definition) + # If there's no content type, then it is a `text/plain` message + if interaction.body and not interaction.body.mime_type: + interaction.body.mime_type = "text/plain" + interactions.append(interaction) + with (temp_dir / "interactions.pkl").open("wb") as pkl_file: - pickle.dump(interaction_definitions, pkl_file) + pickle.dump(interactions, pkl_file) + + yield from start_provider(temp_dir) def start_provider(provider_dir: str | Path) -> Generator[URL, None, None]: # noqa: C901 @@ -926,7 +916,7 @@ def _( f.write("true") verifier.set_state( - provider_url / "_test" / "callback", + provider_url / "_pact" / "callback", teardown=True, ) @@ -1072,6 +1062,11 @@ def _( logger.debug("Running verification on %r", verifier) verifier.set_info("provider", url=provider_url) + verifier.add_transport( + protocol="message", + port=provider_url.port, + path="/_pact/message", + ) try: verifier.verify() except Exception as e: # noqa: BLE001 @@ -1079,34 +1074,6 @@ def _( return verifier, None -def the_verification_is_run_with_start_context( - stacklevel: int = 1, -) -> tuple[Verifier, Exception | None]: - @when( - "the verification is run", - target_fixture="verifier_result", - stacklevel=stacklevel + 1, - ) - def _( - verifier: Verifier, - temp_dir: Path, - ) -> tuple[Verifier, Exception | None]: - """Run the verification.""" - start_provider_context_manager = contextlib.contextmanager(start_provider) - - with start_provider_context_manager(temp_dir) as provider_url: - verifier.set_state( - provider_url / "_test" / "callback", - teardown=True, - ) - verifier.set_info("provider", url=f"{provider_url}/message_handler") - try: - verifier.verify() - except Exception as e: # noqa: BLE001 - return verifier, e - return verifier, None - - ################################################################################ ## Then ################################################################################ From 9ec2c3f2512c7b7344cac2d6360818c9b6338f05 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 17 Jul 2024 11:18:30 +1000 Subject: [PATCH 0428/1376] fix(ffi): use `with_binary_body` The `with_binary_file` FFI function is perhaps poorly named, as it doesn't just set the (binary) body of an interaction, but it also sets the matcher to be a content type matcher. It would be best to have two separate FFI calls (which are now supported) of `with_binary_body` and `with_matching_rule`. Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 18 +++++++++++++++++- src/pact/v3/interaction/_base.py | 2 +- tests/v3/test_http_interaction.py | 11 ++++++----- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 8108dcee4..4d38f17b3 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -6179,7 +6179,23 @@ def with_binary_body( RuntimeError: If the body could not be modified. """ - raise NotImplementedError + if len(gc.get_referrers(body)) == 0: + warnings.warn( + "Make sure to assign the body to a variable to avoid having the byte array" + " modified.", + UserWarning, + stacklevel=3, + ) + success: bool = lib.pactffi_with_binary_body( + interaction._ref, + part.value, + content_type.encode("utf-8") if content_type else ffi.NULL, + body if body else ffi.NULL, + len(body) if body else 0, + ) + if not success: + msg = f"Unable to set body for {interaction}." + raise RuntimeError(msg) def with_binary_file( diff --git a/src/pact/v3/interaction/_base.py b/src/pact/v3/interaction/_base.py index 9696f37a3..c920d1b23 100644 --- a/src/pact/v3/interaction/_base.py +++ b/src/pact/v3/interaction/_base.py @@ -300,7 +300,7 @@ def with_binary_body( body: Body of the request. """ - pact.v3.ffi.with_binary_file( + pact.v3.ffi.with_binary_body( self._handle, self._parse_interaction_part(part), content_type, diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py index 1c76b44ce..6e56ecd79 100644 --- a/tests/v3/test_http_interaction.py +++ b/tests/v3/test_http_interaction.py @@ -13,6 +13,7 @@ import pytest from pact.v3 import Pact +from pact.v3.pact import MismatchesError if TYPE_CHECKING: from pathlib import Path @@ -362,7 +363,7 @@ async def test_with_body_response(pact: Pact, method: str) -> None: json={"test": True}, ) as resp: assert resp.status == 200 - assert await resp.json() == {"test": True} + assert json.loads(await resp.content.read()) == {"test": True} @pytest.mark.asyncio() @@ -382,7 +383,7 @@ async def test_with_body_explicit(pact: Pact) -> None: json={"request": True}, ) as resp: assert resp.status == 200 - assert await resp.json() == {"response": True} + assert json.loads(await resp.content.read()) == {"response": True} def test_with_body_invalid(pact: Pact) -> None: @@ -444,10 +445,10 @@ async def test_binary_file_request(pact: Pact) -> None: async with aiohttp.ClientSession(srv.url) as session: async with session.post("/", data=payload) as resp: assert resp.status == 200 + + with pytest.raises(MismatchesError), pact.serve() as srv: # noqa: PT012 + async with aiohttp.ClientSession(srv.url) as session: async with session.post("/", data=payload[:2]) as resp: - # The match _only_ checks the content type, not the content - # itself. See - # https://pact-foundation.slack.com/archives/C02BXLDJ7JR/p1697032990681329 assert resp.status == 200 From 66f24da3b960507867678a106ee8b82a9b1b6b83 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 17 Jul 2024 12:43:44 +1000 Subject: [PATCH 0429/1376] refactor(tests): move InteractionDefinition in own module The `__init__` was getting a bit too cluttered, so I have refactored the entire `InteractionDefinition` class into its own module. I have also refacted the nested classes to stand alone now. A minor refactor was also made to avoid assigning custom attributes to the `Flask` app, and some changes were made to the provider initialisation to avoid passing a redundant `self.app` in a method call. Signed-off-by: JP-Ellis --- .../compatibility_suite/test_v1_consumer.py | 10 +- .../compatibility_suite/test_v1_provider.py | 6 +- .../compatibility_suite/test_v2_consumer.py | 10 +- .../compatibility_suite/test_v2_provider.py | 6 +- .../test_v3_message_producer.py | 13 +- .../compatibility_suite/test_v3_provider.py | 6 +- .../compatibility_suite/test_v4_provider.py | 6 +- tests/v3/compatibility_suite/util/__init__.py | 735 +---------------- tests/v3/compatibility_suite/util/consumer.py | 14 +- .../util/interaction_definition.py | 759 ++++++++++++++++++ tests/v3/compatibility_suite/util/provider.py | 42 +- 11 files changed, 822 insertions(+), 785 deletions(-) create mode 100644 tests/v3/compatibility_suite/util/interaction_definition.py diff --git a/tests/v3/compatibility_suite/test_v1_consumer.py b/tests/v3/compatibility_suite/test_v1_consumer.py index 75c7b07d2..8f222cbf5 100644 --- a/tests/v3/compatibility_suite/test_v1_consumer.py +++ b/tests/v3/compatibility_suite/test_v1_consumer.py @@ -6,10 +6,7 @@ from pytest_bdd import given, parsers, scenario -from tests.v3.compatibility_suite.util import ( - InteractionDefinition, - parse_markdown_table, -) +from tests.v3.compatibility_suite.util import parse_markdown_table from tests.v3.compatibility_suite.util.consumer import ( a_response_is_returned, request_n_is_made_to_the_mock_server, @@ -34,6 +31,9 @@ the_pact_test_is_done, the_payload_will_contain_the_json_document, ) +from tests.v3.compatibility_suite.util.interaction_definition import ( + InteractionDefinition, +) logger = logging.getLogger(__name__) @@ -320,7 +320,7 @@ def the_following_http_interactions_have_been_defined( # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} for row in content: - interactions[int(row["No"])] = InteractionDefinition(**row) + interactions[int(row["No"])] = InteractionDefinition(**row) # type: ignore[arg-type] return interactions diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py index 1078f714a..60654a629 100644 --- a/tests/v3/compatibility_suite/test_v1_provider.py +++ b/tests/v3/compatibility_suite/test_v1_provider.py @@ -10,9 +10,9 @@ import pytest from pytest_bdd import given, parsers, scenario -from tests.v3.compatibility_suite.util import ( +from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util.interaction_definition import ( InteractionDefinition, - parse_markdown_table, ) from tests.v3.compatibility_suite.util.provider import ( a_failed_verification_result_will_be_published_back, @@ -407,7 +407,7 @@ def the_following_http_interactions_have_been_defined( # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} for row in content: - interactions[int(row["No"])] = InteractionDefinition(**row) + interactions[int(row["No"])] = InteractionDefinition(**row) # type: ignore[arg-type] return interactions diff --git a/tests/v3/compatibility_suite/test_v2_consumer.py b/tests/v3/compatibility_suite/test_v2_consumer.py index cd1447cfe..b2b6e3101 100644 --- a/tests/v3/compatibility_suite/test_v2_consumer.py +++ b/tests/v3/compatibility_suite/test_v2_consumer.py @@ -6,10 +6,7 @@ from pytest_bdd import given, parsers, scenario -from tests.v3.compatibility_suite.util import ( - InteractionDefinition, - parse_markdown_table, -) +from tests.v3.compatibility_suite.util import parse_markdown_table from tests.v3.compatibility_suite.util.consumer import ( a_response_is_returned, request_n_is_made_to_the_mock_server, @@ -35,6 +32,9 @@ the_pact_test_is_done, the_payload_will_contain_the_json_document, ) +from tests.v3.compatibility_suite.util.interaction_definition import ( + InteractionDefinition, +) logger = logging.getLogger(__name__) @@ -196,7 +196,7 @@ def the_following_http_interactions_have_been_defined( # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} for row in table: - interactions[int(row["No"])] = InteractionDefinition(**row) + interactions[int(row["No"])] = InteractionDefinition(**row) # type: ignore[arg-type] return interactions diff --git a/tests/v3/compatibility_suite/test_v2_provider.py b/tests/v3/compatibility_suite/test_v2_provider.py index 94313bae2..f891b60d1 100644 --- a/tests/v3/compatibility_suite/test_v2_provider.py +++ b/tests/v3/compatibility_suite/test_v2_provider.py @@ -10,9 +10,9 @@ import pytest from pytest_bdd import given, parsers, scenario -from tests.v3.compatibility_suite.util import ( +from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util.interaction_definition import ( InteractionDefinition, - parse_markdown_table, ) from tests.v3.compatibility_suite.util.provider import ( a_pact_file_for_interaction_is_to_be_verified, @@ -125,7 +125,7 @@ def the_following_http_interactions_have_been_defined( # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} for row in content: - interactions[int(row["No"])] = InteractionDefinition(**row) + interactions[int(row["No"])] = InteractionDefinition(**row) # type: ignore[arg-type] return interactions diff --git a/tests/v3/compatibility_suite/test_v3_message_producer.py b/tests/v3/compatibility_suite/test_v3_message_producer.py index 31d87d0e8..ed6fc99a3 100644 --- a/tests/v3/compatibility_suite/test_v3_message_producer.py +++ b/tests/v3/compatibility_suite/test_v3_message_producer.py @@ -18,10 +18,13 @@ from pact.v3.pact import Pact from tests.v3.compatibility_suite.util import ( - InteractionDefinition, parse_horizontal_markdown_table, parse_markdown_table, ) +from tests.v3.compatibility_suite.util.interaction_definition import ( + InteractionDefinition, + InteractionState, +) from tests.v3.compatibility_suite.util.provider import ( a_provider_is_started_that_can_generate_the_message, a_provider_state_callback_is_configured, @@ -229,7 +232,7 @@ def a_pact_file_for_is_to_be_verified_with_the_following( interaction_definition = InteractionDefinition( type="Async", description=name, - **table, + **table, # type: ignore[arg-type] ) interaction_definition.add_to_pact(pact, name) (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) @@ -284,16 +287,14 @@ def a_pact_file_for_is_to_be_verified_with_provider_state( description=name, body=fixture, ) - interaction_definition.states = [InteractionDefinition.State(provider_state)] + interaction_definition.states = [InteractionState(provider_state)] interaction_definition.add_to_pact(pact, name) (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) pact.write_file(temp_dir / "pacts") verifier.add_source(temp_dir / "pacts") with (temp_dir / "provider_states").open("w") as f: logger.debug("Writing provider state to %s", temp_dir / "provider_states") - json.dump( - [s.as_dict() for s in [InteractionDefinition.State(provider_state)]], f - ) + json.dump([s.as_dict() for s in [InteractionState(provider_state)]], f) @given( diff --git a/tests/v3/compatibility_suite/test_v3_provider.py b/tests/v3/compatibility_suite/test_v3_provider.py index eaa452a45..b3f6442ea 100644 --- a/tests/v3/compatibility_suite/test_v3_provider.py +++ b/tests/v3/compatibility_suite/test_v3_provider.py @@ -10,9 +10,9 @@ import pytest from pytest_bdd import given, parsers, scenario -from tests.v3.compatibility_suite.util import ( +from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util.interaction_definition import ( InteractionDefinition, - parse_markdown_table, ) from tests.v3.compatibility_suite.util.provider import ( a_pact_file_for_interaction_is_to_be_verified_with_a_provider_states_defined, @@ -100,7 +100,7 @@ def the_following_http_interactions_have_been_defined( # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} for row in content: - interactions[int(row["No"])] = InteractionDefinition(**row) + interactions[int(row["No"])] = InteractionDefinition(**row) # type: ignore[arg-type] return interactions diff --git a/tests/v3/compatibility_suite/test_v4_provider.py b/tests/v3/compatibility_suite/test_v4_provider.py index bffc68f74..5f3ba4482 100644 --- a/tests/v3/compatibility_suite/test_v4_provider.py +++ b/tests/v3/compatibility_suite/test_v4_provider.py @@ -10,9 +10,9 @@ import pytest from pytest_bdd import given, parsers, scenario -from tests.v3.compatibility_suite.util import ( +from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util.interaction_definition import ( InteractionDefinition, - parse_markdown_table, ) from tests.v3.compatibility_suite.util.provider import ( a_pact_file_for_interaction_is_to_be_verified, @@ -104,7 +104,7 @@ def the_following_http_interactions_have_been_defined( # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} for row in content: - interactions[int(row["No"])] = InteractionDefinition(**row) + interactions[int(row["No"])] = InteractionDefinition(**row) # type: ignore[arg-type] return interactions diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 867ac5895..c2ba356e3 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -22,28 +22,17 @@ def _(): from __future__ import annotations import base64 -import contextlib import hashlib -import json import logging -import sys import typing from collections.abc import Collection, Mapping from datetime import date, datetime, time from pathlib import Path -from typing import Any, Generic, Literal, TypeVar -from xml.etree import ElementTree +from typing import Any, Generic, TypeVar -import flask -from flask import request from multidict import MultiDict -from typing_extensions import Self -from yarl import URL - -from pact.v3.interaction import HttpInteraction, Interaction if typing.TYPE_CHECKING: - from pact.v3.interaction import Interaction from pact.v3.pact import Pact logger = logging.getLogger(__name__) @@ -297,725 +286,3 @@ def parse_matching_rules(matching_rules: str) -> str: with (FIXTURES_ROOT / matching_rules).open("r") as file: return file.read() - - -class InteractionDefinition: - """ - Interaction definition. - - This is a dictionary that represents a single interaction. It is used to - parse the HTTP interactions table into a more useful format. - """ - - class Body: - """ - Interaction body. - - The interaction body can be one of: - - - A file - - An arbitrary string - - A JSON document - - An XML document - """ - - def __init__(self, data: str) -> None: - """ - Instantiate the interaction body. - """ - self.string: str | None = None - self.bytes: bytes | None = None - self.mime_type: str | None = None - - if data.startswith("file: ") and data.endswith("-body.xml"): - self.parse_fixture(FIXTURES_ROOT / data[6:]) - return - - if data.startswith("file: "): - self.parse_file(FIXTURES_ROOT / data[6:]) - return - - if data.startswith("JSON: "): - self.string = data[6:] - self.bytes = self.string.encode("utf-8") - self.mime_type = "application/json" - return - - if data.startswith("XML: "): - self.string = data[5:] - self.bytes = self.string.encode("utf-8") - self.mime_type = "application/xml" - return - - self.bytes = data.encode("utf-8") - self.string = data - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return "".format( - ", ".join( - str(k) + "=" + truncate(repr(v)) for k, v in vars(self).items() - ), - ) - - def parse_fixture(self, fixture: Path) -> None: - """ - Parse a fixture file. - - This is used to parse the fixture files that contain additional - metadata about the body (such as the content type). - """ - etree = ElementTree.parse(fixture) # noqa: S314 - root = etree.getroot() - if not root or root.tag != "body": - msg = "Invalid XML fixture document" - raise ValueError(msg) - - contents = root.find("contents") - content_type = root.find("contentType") - if contents is None: - msg = "Invalid XML fixture document: no contents" - raise ValueError(msg) - if content_type is None: - msg = "Invalid XML fixture document: no contentType" - raise ValueError(msg) - self.string = typing.cast(str, contents.text) - - if eol := contents.attrib.get("eol", None): - if eol == "CRLF": - self.string = self.string.replace("\r\n", "\n") - self.string = self.string.replace("\n", "\r\n") - elif eol == "LF": - self.string = self.string.replace("\r\n", "\n") - - self.bytes = self.string.encode("utf-8") - self.mime_type = content_type.text - - def parse_file(self, file: Path) -> None: - """ - Load the contents of a file. - - The mime type is inferred from the file extension, and the contents - are loaded as a byte array, and optionally as a string. - """ - self.bytes = file.read_bytes() - with contextlib.suppress(UnicodeDecodeError): - self.string = file.read_text() - - if file.suffix == ".xml": - self.mime_type = "application/xml" - elif file.suffix == ".json": - self.mime_type = "application/json" - elif file.suffix == ".jpg": - self.mime_type = "image/jpeg" - elif file.suffix == ".pdf": - self.mime_type = "application/pdf" - else: - msg = "Unknown file type" - raise ValueError(msg) - - def add_to_interaction( - self, - interaction: Interaction, - ) -> None: - """ - Add a body to the interaction. - - This is a helper method that adds the body to the interaction. This - relies on Pact's intelligent understanding of whether it is dealing with - a request or response (which is determined through the use of - `will_respond_with`). - - Args: - body: - The body to add to the interaction. - - interaction: - The interaction to add the body to. - - """ - if self.string: - logger.info( - "with_body(%r, %r)", - truncate(self.string), - self.mime_type, - ) - interaction.with_body( - self.string, - self.mime_type, - ) - elif self.bytes: - logger.info( - "with_binary_body(%r, %r)", - truncate(self.bytes), - self.mime_type, - ) - interaction.with_binary_body( - self.bytes, - self.mime_type, - ) - else: - msg = "Unexpected body definition" - raise RuntimeError(msg) - - if self.mime_type and isinstance(interaction, HttpInteraction): - logger.info('set_header("Content-Type", %r)', self.mime_type) - interaction.set_header("Content-Type", self.mime_type) - - class State: - """ - Provider state. - """ - - def __init__( - self, - name: str, - parameters: str | dict[str, Any] | None = None, - ) -> None: - """ - Instantiate the provider state. - """ - self.name = name - self.parameters: dict[str, Any] - if isinstance(parameters, str): - self.parameters = json.loads(parameters) - else: - self.parameters = parameters or {} - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return "".format( - ", ".join( - str(k) + "=" + truncate(repr(v)) for k, v in vars(self).items() - ), - ) - - def as_dict(self) -> dict[str, str | dict[str, Any]]: - """ - Convert the provider state to a dictionary. - """ - return {"name": self.name, "parameters": self.parameters} - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> Self: - """ - Convert a dictionary to a provider state. - """ - return cls(**data) - - def __init__(self, metadata: dict[str, Any] | None = None, **kwargs: str) -> None: - """ - Initialise the interaction definition. - - As the interaction definitions are parsed from a Markdown table, - values are expected to be strings and must be converted to the correct - type. - - The _only_ exception to this is the `metadata` key, which expects - a dictionary. - """ - # A common pattern used in the tests is to have a table with the 'base' - # definitions, and have tests modify these definitions as need be. As a - # result, the `__init__` method is designed to set all the values to - # defaults, and the `update` method is used to update the values. - if type_ := kwargs.pop("type", "HTTP"): - if type_ not in ("HTTP", "Sync", "Async"): - msg = f"Invalid value for 'type': {type_}" - raise ValueError(msg) - self.type: Literal["HTTP", "Sync", "Async"] = type_ - - # General properties shared by all interaction types - self.id: int | None = None - self.description: str | None = None - self.states: list[InteractionDefinition.State] = [] - self.metadata: dict[str, Any] | None = None - self.pending: bool = False - self.is_pending: bool = False - self.test_name: str | None = None - self.text_comments: list[str] = [] - self.comments: dict[str, str] = {} - - # Request properties - self.method: str | None = None - self.path: str | None = None - self.response: int | None = None - self.query: str | None = None - self.headers: MultiDict[str] = MultiDict() - self.body: InteractionDefinition.Body | None = None - self.matching_rules: str | None = None - - # Response properties - self.response_headers: MultiDict[str] = MultiDict() - self.response_body: InteractionDefinition.Body | None = None - self.response_matching_rules: str | None = None - - self.update(metadata=metadata, **kwargs) - - def update(self, metadata: dict[str, Any] | None = None, **kwargs: str) -> None: - """ - Update the interaction definition. - - This is a convenience method that allows the interaction definition to - be updated with new values. - """ - kwargs = self._update_shared(metadata, **kwargs) - kwargs = self._update_request(**kwargs) - kwargs = self._update_response(**kwargs) - - if len(kwargs) > 0: - msg = f"Unexpected arguments: {kwargs.keys()}" - raise TypeError(msg) - - def _update_shared( - self, - metadata: dict[str, Any] | None = None, - **kwargs: str, - ) -> dict[str, str]: - """ - Update the shared properties of the interaction. - - Note that the following properties are not supported and must be - modified directly: - - - `states` - - `text_comments` - - `comments` - - Args: - metadata: - Metadata for the interaction. - - kwargs: - Remaining keyword arguments, which are: - - - `No`: Interaction ID. Used purely for debugging purposes. - - `description`: Description of the interaction (used by - asynchronous messages) - - `pending`: Whether the interaction is pending. - - `test_name`: Test name for the interaction. - - Returns: - The remaining keyword arguments. - """ - if interaction_id := kwargs.pop("No", None): - self.id = int(interaction_id) - - if description := kwargs.pop("description", None): - self.description = description - - if "states" in kwargs: - msg = "Unsupported. Modify the 'states' property directly." - raise ValueError(msg) - - if metadata: - self.metadata = metadata - - if "pending" in kwargs: - self.pending = kwargs.pop("pending") == "true" - - if test_name := kwargs.pop("test_name", None): - self.test_name = test_name - - if "text_comments" in kwargs: - msg = "Unsupported. Modify the 'text_comments' property directly." - raise ValueError(msg) - - if "comments" in kwargs: - msg = "Unsupported. Modify the 'comments' property directly." - raise ValueError(msg) - - return kwargs - - def _update_request(self, **kwargs: str) -> dict[str, str]: - """ - Update the request properties of the interaction. - - Args: - kwargs: - Remaining keyword arguments, which are: - - - `method`: Request method. - - `path`: Request path. - - `query`: Query parameters. - - `headers`: Request headers. - - `raw_headers`: Request headers. - - `body`: Request body. - - `content_type`: Request content type. - - `matching_rules`: Request matching rules. - - """ - if method := kwargs.pop("method", None): - self.method = method - - if path := kwargs.pop("path", None): - self.path = path - - if query := kwargs.pop("query", None): - self.query = query - - if headers := kwargs.pop("headers", None): - self.headers = parse_headers(headers) - - if headers := ( - kwargs.pop("raw headers", None) or kwargs.pop("raw_headers", None) - ): - self.headers = parse_headers(headers) - - if body := kwargs.pop("body", None): - # When updating the body, we _only_ update the body content, not - # the content type. - orig_content_type = self.body.mime_type if self.body else None - self.body = InteractionDefinition.Body(body) - self.body.mime_type = self.body.mime_type or orig_content_type - - if content_type := ( - kwargs.pop("content_type", None) or kwargs.pop("content type", None) - ): - if self.body is None: - self.body = InteractionDefinition.Body("") - self.body.mime_type = content_type - - if matching_rules := ( - kwargs.pop("matching_rules", None) or kwargs.pop("matching rules", None) - ): - self.matching_rules = parse_matching_rules(matching_rules) - - return kwargs - - def _update_response(self, **kwargs: str) -> dict[str, str]: - """ - Update the response properties of the interaction. - - Args: - kwargs: - Remaining keyword arguments, which are: - - - `response`: Response status code. - - `response_headers`: Response headers. - - `response_content`: Response content type. - - `response_body`: Response body. - - `response_matching_rules`: Response matching rules. - - Returns: - The remaining keyword arguments. - """ - if response := kwargs.pop("response", None) or kwargs.pop("status", None): - self.response = int(response) - - if response_headers := ( - kwargs.pop("response_headers", None) or kwargs.pop("response headers", None) - ): - self.response_headers = parse_headers(response_headers) - - if response_content := ( - kwargs.pop("response_content", None) or kwargs.pop("response content", None) - ): - if self.response_body is None: - self.response_body = InteractionDefinition.Body("") - self.response_body.mime_type = response_content - - if response_body := ( - kwargs.pop("response_body", None) or kwargs.pop("response body", None) - ): - orig_content_type = ( - self.response_body.mime_type if self.response_body else None - ) - self.response_body = InteractionDefinition.Body(response_body) - self.response_body.mime_type = ( - self.response_body.mime_type or orig_content_type - ) - - if matching_rules := ( - kwargs.pop("response_matching_rules", None) - or kwargs.pop("response matching rules", None) - ): - self.response_matching_rules = parse_matching_rules(matching_rules) - - return kwargs - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return "".format( - ", ".join(f"{k}={v!r}" for k, v in vars(self).items()), - ) - - def add_to_pact( # noqa: C901, PLR0912, PLR0915 - self, - pact: Pact, - name: str, - ) -> None: - """ - Add the interaction to the pact. - - This is a convenience method that allows the interaction definition to - be added to the pact, defining the "upon receiving ... with ... will - respond with ...". - - Args: - pact: - The pact being defined. - - name: - Name for this interaction. Must be unique for the pact. - """ - interaction = pact.upon_receiving(name, self.type) - if isinstance(interaction, HttpInteraction): - assert self.method, "Method must be defined" - assert self.path, "Path must be defined" - - logger.info("with_request(%r, %r)", self.method, self.path) - interaction.with_request(self.method, self.path) - - for state in self.states or []: - if state.parameters: - logger.info("given(%r, parameters=%r)", state.name, state.parameters) - interaction.given(state.name, parameters=state.parameters) - else: - logger.info("given(%r)", state.name) - interaction.given(state.name) - - if self.pending: - logger.info("set_pending(True)") - interaction.set_pending(pending=True) - - if self.text_comments: - for comment in self.text_comments: - logger.info("add_text_comment(%r)", comment) - interaction.add_text_comment(comment) - - for key, value in self.comments.items(): - logger.info("set_comment(%r, %r)", key, value) - interaction.set_comment(key, value) - - if self.test_name: - logger.info("test_name(%r)", self.test_name) - interaction.test_name(self.test_name) - - if self.query: - assert isinstance( - interaction, HttpInteraction - ), "Query parameters require an HTTP interaction" - query = URL.build(query_string=self.query).query - logger.info("with_query_parameters(%r)", query.items()) - interaction.with_query_parameters(query.items()) - - if self.headers: - assert isinstance( - interaction, HttpInteraction - ), "Headers require an HTTP interaction" - logger.info("with_headers(%r)", self.headers.items()) - interaction.with_headers(self.headers.items()) - - if self.body: - self.body.add_to_interaction(interaction) - - if self.matching_rules: - logger.info("with_matching_rules(%r)", self.matching_rules) - interaction.with_matching_rules(self.matching_rules) - - if self.response: - assert isinstance( - interaction, HttpInteraction - ), "Response requires an HTTP interaction" - logger.info("will_respond_with(%r)", self.response) - interaction.will_respond_with(self.response) - - if self.response_headers: - assert isinstance( - interaction, HttpInteraction - ), "Response headers require an HTTP interaction" - logger.info("with_headers(%r)", self.response_headers) - interaction.with_headers(self.response_headers.items()) - - if self.response_body: - assert isinstance( - interaction, HttpInteraction - ), "Response body requires an HTTP interaction" - self.response_body.add_to_interaction(interaction) - - if self.response_matching_rules: - logger.info("with_matching_rules(%r)", self.response_matching_rules) - interaction.with_matching_rules(self.response_matching_rules) - - if self.metadata: - for key, value in self.metadata.items(): - if isinstance(value, str): - interaction.with_metadata({key: value}) - else: - interaction.with_metadata({key: json.dumps(value)}) - - def add_to_flask(self, app: flask.Flask) -> None: - """ - Add an interaction to a Flask app. - - Args: - app: - The Flask app to add the interaction to. - """ - logger.debug("Adding %s interaction to Flask app", self.type) - if self.type == "HTTP": - self._add_http_to_flask(app) - elif self.type == "Sync": - self._add_sync_to_flask(app) - elif self.type == "Async": - self._add_async_to_flask(app) - else: - msg = f"Unknown interaction type: {self.type}" - raise ValueError(msg) - - def _add_http_to_flask(self, app: flask.Flask) -> None: - """ - Add a HTTP interaction to a Flask app. - - Ths function works by defining a new function to handle the request and - produce the response. This function is then added to the Flask app as a - route. - - Args: - app: - The Flask app to add the interaction to. - """ - assert isinstance(self.method, str), "Method must be a string" - assert isinstance(self.path, str), "Path must be a string" - - logger.info( - "Adding HTTP '%s %s' interaction to Flask app", - self.method, - self.path, - ) - logger.debug("-> Query: %s", self.query) - logger.debug("-> Headers: %s", self.headers) - logger.debug("-> Body: %s", self.body) - logger.debug("-> Response Status: %s", self.response) - logger.debug("-> Response Headers: %s", self.response_headers) - logger.debug("-> Response Body: %s", self.response_body) - - def route_fn() -> flask.Response: - if self.query: - query = URL.build(query_string=self.query).query - # Perform a two-way check to ensure that the query parameters - # are present in the request, and that the request contains no - # unexpected query parameters. - for k, v in query.items(): - assert request.args[k] == v - for k, v in request.args.items(): - assert query[k] == v - - if self.headers: - # Perform a one-way check to ensure that the expected headers - # are present in the request, but don't check for any unexpected - # headers. - for k, v in self.headers.items(): - assert k in request.headers - assert request.headers[k] == v - - if self.body: - assert request.data == self.body.bytes - - return flask.Response( - response=self.response_body.bytes or self.response_body.string or None - if self.response_body - else None, - status=self.response, - headers=dict(**self.response_headers), - content_type=self.response_body.mime_type - if self.response_body - else None, - direct_passthrough=True, - ) - - # The route function needs to have a unique name - clean_name = self.path.replace("/", "_").replace("__", "_") - route_fn.__name__ = f"{self.method.lower()}_{clean_name}" - - app.add_url_rule( - self.path, - view_func=route_fn, - methods=[self.method], - ) - - def _add_sync_to_flask(self, app: flask.Flask) -> None: - """ - Add a synchronous message interaction to a Flask app. - - Args: - app: - The Flask app to add the interaction to. - """ - raise NotImplementedError - - def _add_async_to_flask(self, app: flask.Flask) -> None: - """ - Add a synchronous message interaction to a Flask app. - - Args: - app: - The Flask app to add the interaction to. - """ - assert self.description, "Description must be set for async messages" - if hasattr(app, "pact_messages"): - app.pact_messages[self.description] = self - else: - app.pact_messages = {self.description: self} - - # All messages are handled by the same route. So we just need to check - # whether the route has been defined, and if not, define it. - for rule in app.url_map.iter_rules(): - if rule.rule == "/_pact/message": - sys.stderr.write("Async message route already defined\n") - return - - sys.stderr.write("Adding async message route\n") - - @app.post("/_pact/message") - def post_message() -> flask.Response: - sys.stderr.write("Received POST request for message\n") - assert hasattr(app, "pact_messages"), "No messages defined" - assert isinstance(app.pact_messages, dict), "Messages must be a dictionary" - - body: dict[str, Any] = json.loads(request.data) - description: str = body["description"] - - if description not in app.pact_messages: - return flask.Response( - response=json.dumps({ - "error": f"Message {description} not found", - }), - status=404, - headers={"Content-Type": "application/json"}, - content_type="application/json", - ) - - interaction: InteractionDefinition = app.pact_messages[description] - return interaction.create_async_message_response() - - def create_async_message_response(self) -> flask.Response: - """ - Convert the interaction to a Flask response. - - When an async message needs to be produced, Pact expects the response - from the special `/_pact/message` endppoint to generate the expected - message. - - Whilst this is a Response from Flask's perspective, the attributes - returned - """ - assert self.type == "Async", "Only async messages are supported" - - if self.metadata: - self.headers["Pact-Message-Metadata"] = base64.b64encode( - json.dumps(self.metadata).encode("utf-8") - ).decode("utf-8") - - return flask.Response( - response=self.body.bytes or self.body.string or None if self.body else None, - headers=((k, v) for k, v in self.headers.items()), - content_type=self.body.mime_type if self.body else None, - direct_passthrough=True, - ) diff --git a/tests/v3/compatibility_suite/util/consumer.py b/tests/v3/compatibility_suite/util/consumer.py index 1126ed5c3..76887b636 100644 --- a/tests/v3/compatibility_suite/util/consumer.py +++ b/tests/v3/compatibility_suite/util/consumer.py @@ -29,7 +29,9 @@ from pact.v3.interaction._async_message_interaction import AsyncMessageInteraction from pact.v3.pact import PactServer - from tests.v3.compatibility_suite.util import InteractionDefinition + from tests.v3.compatibility_suite.util.interaction_definition import ( + InteractionDefinition, + ) logger = logging.getLogger(__name__) @@ -121,7 +123,7 @@ def _( pact = Pact("consumer", "provider") pact.with_specification(version) definition = interaction_definitions[iid] - definition.update(**content[0]) + definition.update(**content[0]) # type: ignore[arg-type] logger.info("Adding modified interaction %s", iid) definition.add_to_pact(pact, f"interaction {iid}") @@ -154,6 +156,9 @@ def _( ): definition.headers.add("Content-Type", definition.body.mime_type) + assert definition.method is not None, "Method not defined" + assert definition.path is not None, "Path not defined" + return requests.request( definition.method, str(srv.url.with_path(definition.path)), @@ -195,7 +200,7 @@ def _( """ definition = interaction_definitions[request_id] assert len(content) == 1, "Expected exactly one row in the table" - definition.update(**content[0]) + definition.update(**content[0]) # type: ignore[arg-type] if ( definition.body @@ -204,6 +209,9 @@ def _( ): definition.headers.add("Content-Type", definition.body.mime_type) + assert definition.method is not None, "Method not defined" + assert definition.path is not None, "Path not defined" + return requests.request( definition.method, str(srv.url.with_path(definition.path)), diff --git a/tests/v3/compatibility_suite/util/interaction_definition.py b/tests/v3/compatibility_suite/util/interaction_definition.py new file mode 100644 index 000000000..19fd5ce05 --- /dev/null +++ b/tests/v3/compatibility_suite/util/interaction_definition.py @@ -0,0 +1,759 @@ +""" +Interaction definition. + +This module defines the `InteractionDefinition` class, which is used to help +parse the interaction definitions from the compatibility suite, and interact +with the `Pact` and `Interaction` classes. +""" + +from __future__ import annotations + +import base64 +import contextlib +import json +import logging +import sys +import typing +from typing import Any, Literal +from xml.etree import ElementTree + +import flask +from flask import request +from multidict import MultiDict +from typing_extensions import Self +from yarl import URL + +from pact.v3.interaction import HttpInteraction, Interaction +from tests.v3.compatibility_suite.util import ( + FIXTURES_ROOT, + parse_headers, + parse_matching_rules, + truncate, +) + +if typing.TYPE_CHECKING: + from pathlib import Path + + from pact.v3.interaction import Interaction + from pact.v3.pact import Pact + from tests.v3.compatibility_suite.util.provider import Provider + +logger = logging.getLogger(__name__) + + +class InteractionBody: + """ + Interaction body. + + The interaction body can be one of: + + - A file + - An arbitrary string + - A JSON document + - An XML document + """ + + def __init__(self, data: str) -> None: + """ + Instantiate the interaction body. + """ + self.string: str | None = None + self.bytes: bytes | None = None + self.mime_type: str | None = None + + if data.startswith("file: ") and data.endswith("-body.xml"): + self.parse_fixture(FIXTURES_ROOT / data[6:]) + return + + if data.startswith("file: "): + self.parse_file(FIXTURES_ROOT / data[6:]) + return + + if data.startswith("JSON: "): + self.string = data[6:] + self.bytes = self.string.encode("utf-8") + self.mime_type = "application/json" + return + + if data.startswith("XML: "): + self.string = data[5:] + self.bytes = self.string.encode("utf-8") + self.mime_type = "application/xml" + return + + self.bytes = data.encode("utf-8") + self.string = data + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return "".format( + ", ".join(str(k) + "=" + truncate(repr(v)) for k, v in vars(self).items()), + ) + + def parse_fixture(self, fixture: Path) -> None: + """ + Parse a fixture file. + + This is used to parse the fixture files that contain additional + metadata about the body (such as the content type). + """ + etree = ElementTree.parse(fixture) # noqa: S314 + root = etree.getroot() + if not root or root.tag != "body": + msg = "Invalid XML fixture document" + raise ValueError(msg) + + contents = root.find("contents") + content_type = root.find("contentType") + if contents is None: + msg = "Invalid XML fixture document: no contents" + raise ValueError(msg) + if content_type is None: + msg = "Invalid XML fixture document: no contentType" + raise ValueError(msg) + self.string = typing.cast(str, contents.text) + + if eol := contents.attrib.get("eol", None): + if eol == "CRLF": + self.string = self.string.replace("\r\n", "\n") + self.string = self.string.replace("\n", "\r\n") + elif eol == "LF": + self.string = self.string.replace("\r\n", "\n") + + self.bytes = self.string.encode("utf-8") + self.mime_type = content_type.text + + def parse_file(self, file: Path) -> None: + """ + Load the contents of a file. + + The mime type is inferred from the file extension, and the contents + are loaded as a byte array, and optionally as a string. + """ + self.bytes = file.read_bytes() + with contextlib.suppress(UnicodeDecodeError): + self.string = file.read_text() + + if file.suffix == ".xml": + self.mime_type = "application/xml" + elif file.suffix == ".json": + self.mime_type = "application/json" + elif file.suffix == ".jpg": + self.mime_type = "image/jpeg" + elif file.suffix == ".pdf": + self.mime_type = "application/pdf" + else: + msg = "Unknown file type" + raise ValueError(msg) + + def add_to_interaction( + self, + interaction: Interaction, + ) -> None: + """ + Add a body to the interaction. + + This is a helper method that adds the body to the interaction. This + relies on Pact's intelligent understanding of whether it is dealing with + a request or response (which is determined through the use of + `will_respond_with`). + + Args: + body: + The body to add to the interaction. + + interaction: + The interaction to add the body to. + + """ + if self.string: + logger.info( + "with_body(%r, %r)", + truncate(self.string), + self.mime_type, + ) + interaction.with_body( + self.string, + self.mime_type, + ) + elif self.bytes: + logger.info( + "with_binary_body(%r, %r)", + truncate(self.bytes), + self.mime_type, + ) + interaction.with_binary_body( + self.bytes, + self.mime_type, + ) + else: + msg = "Unexpected body definition" + raise RuntimeError(msg) + + if self.mime_type and isinstance(interaction, HttpInteraction): + logger.info('set_header("Content-Type", %r)', self.mime_type) + interaction.set_header("Content-Type", self.mime_type) + + +class InteractionState: + """ + Provider state. + """ + + def __init__( + self, + name: str, + parameters: str | dict[str, Any] | None = None, + ) -> None: + """ + Instantiate the provider state. + """ + self.name = name + self.parameters: dict[str, Any] + if isinstance(parameters, str): + self.parameters = json.loads(parameters) + else: + self.parameters = parameters or {} + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return "".format( + ", ".join(str(k) + "=" + truncate(repr(v)) for k, v in vars(self).items()), + ) + + def as_dict(self) -> dict[str, str | dict[str, Any]]: + """ + Convert the provider state to a dictionary. + """ + return {"name": self.name, "parameters": self.parameters} + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Self: + """ + Convert a dictionary to a provider state. + """ + return cls(**data) + + +class InteractionDefinition: + """ + Interaction definition. + + This is a dictionary that represents a single interaction. It is used to + parse the HTTP interactions table into a more useful format. + """ + + def __init__( + self, + *, + metadata: dict[str, Any] | None = None, + **kwargs: str, + ) -> None: + """ + Initialise the interaction definition. + + As the interaction definitions are parsed from a Markdown table, + values are expected to be strings and must be converted to the correct + type. + + The _only_ exception to this is the `metadata` key, which expects + a dictionary. + """ + # A common pattern used in the tests is to have a table with the 'base' + # definitions, and have tests modify these definitions as need be. As a + # result, the `__init__` method is designed to set all the values to + # defaults, and the `update` method is used to update the values. + if type_ := kwargs.pop("type", "HTTP"): + if type_ not in ("HTTP", "Sync", "Async"): + msg = f"Invalid value for 'type': {type_}" + raise ValueError(msg) + self.type: Literal["HTTP", "Sync", "Async"] = type_ # type: ignore[assignment] + + # General properties shared by all interaction types + self.id: int | None = None + self.description: str | None = None + self.states: list[InteractionState] = [] + self.metadata: dict[str, Any] | None = None + self.pending: bool = False + self.is_pending: bool = False + self.test_name: str | None = None + self.text_comments: list[str] = [] + self.comments: dict[str, str] = {} + + # Request properties + self.method: str | None = None + self.path: str | None = None + self.response: int | None = None + self.query: str | None = None + self.headers: MultiDict[str] = MultiDict() + self.body: InteractionBody | None = None + self.matching_rules: str | None = None + + # Response properties + self.response_headers: MultiDict[str] = MultiDict() + self.response_body: InteractionBody | None = None + self.response_matching_rules: str | None = None + + self.update(metadata=metadata, **kwargs) + + def update(self, *, metadata: dict[str, Any] | None = None, **kwargs: str) -> None: + """ + Update the interaction definition. + + This is a convenience method that allows the interaction definition to + be updated with new values. + """ + kwargs = self._update_shared(metadata, **kwargs) + kwargs = self._update_request(**kwargs) + kwargs = self._update_response(**kwargs) + + if len(kwargs) > 0: + msg = f"Unexpected arguments: {kwargs.keys()}" + raise TypeError(msg) + + def _update_shared( + self, + metadata: dict[str, Any] | None = None, + **kwargs: str, + ) -> dict[str, str]: + """ + Update the shared properties of the interaction. + + Note that the following properties are not supported and must be + modified directly: + + - `states` + - `text_comments` + - `comments` + + Args: + metadata: + Metadata for the interaction. + + kwargs: + Remaining keyword arguments, which are: + + - `No`: Interaction ID. Used purely for debugging purposes. + - `description`: Description of the interaction (used by + asynchronous messages) + - `pending`: Whether the interaction is pending. + - `test_name`: Test name for the interaction. + + Returns: + The remaining keyword arguments. + """ + if interaction_id := kwargs.pop("No", None): + self.id = int(interaction_id) + + if description := kwargs.pop("description", None): + self.description = description + + if "states" in kwargs: + msg = "Unsupported. Modify the 'states' property directly." + raise ValueError(msg) + + if metadata: + self.metadata = metadata + + if "pending" in kwargs: + self.pending = kwargs.pop("pending") == "true" + + if test_name := kwargs.pop("test_name", None): + self.test_name = test_name + + if "text_comments" in kwargs: + msg = "Unsupported. Modify the 'text_comments' property directly." + raise ValueError(msg) + + if "comments" in kwargs: + msg = "Unsupported. Modify the 'comments' property directly." + raise ValueError(msg) + + return kwargs + + def _update_request(self, **kwargs: str) -> dict[str, str]: + """ + Update the request properties of the interaction. + + Args: + kwargs: + Remaining keyword arguments, which are: + + - `method`: Request method. + - `path`: Request path. + - `query`: Query parameters. + - `headers`: Request headers. + - `raw_headers`: Request headers. + - `body`: Request body. + - `content_type`: Request content type. + - `matching_rules`: Request matching rules. + + """ + if method := kwargs.pop("method", None): + self.method = method + + if path := kwargs.pop("path", None): + self.path = path + + if query := kwargs.pop("query", None): + self.query = query + + if headers := kwargs.pop("headers", None): + self.headers = parse_headers(headers) + + if headers := ( + kwargs.pop("raw headers", None) or kwargs.pop("raw_headers", None) + ): + self.headers = parse_headers(headers) + + if body := kwargs.pop("body", None): + # When updating the body, we _only_ update the body content, not + # the content type. + orig_content_type = self.body.mime_type if self.body else None + self.body = InteractionBody(body) + self.body.mime_type = self.body.mime_type or orig_content_type + + if content_type := ( + kwargs.pop("content_type", None) or kwargs.pop("content type", None) + ): + if self.body is None: + self.body = InteractionBody("") + self.body.mime_type = content_type + + if matching_rules := ( + kwargs.pop("matching_rules", None) or kwargs.pop("matching rules", None) + ): + self.matching_rules = parse_matching_rules(matching_rules) + + return kwargs + + def _update_response(self, **kwargs: str) -> dict[str, str]: + """ + Update the response properties of the interaction. + + Args: + kwargs: + Remaining keyword arguments, which are: + + - `response`: Response status code. + - `response_headers`: Response headers. + - `response_content`: Response content type. + - `response_body`: Response body. + - `response_matching_rules`: Response matching rules. + + Returns: + The remaining keyword arguments. + """ + if response := kwargs.pop("response", None) or kwargs.pop("status", None): + self.response = int(response) + + if response_headers := ( + kwargs.pop("response_headers", None) or kwargs.pop("response headers", None) + ): + self.response_headers = parse_headers(response_headers) + + if response_content := ( + kwargs.pop("response_content", None) or kwargs.pop("response content", None) + ): + if self.response_body is None: + self.response_body = InteractionBody("") + self.response_body.mime_type = response_content + + if response_body := ( + kwargs.pop("response_body", None) or kwargs.pop("response body", None) + ): + orig_content_type = ( + self.response_body.mime_type if self.response_body else None + ) + self.response_body = InteractionBody(response_body) + self.response_body.mime_type = ( + self.response_body.mime_type or orig_content_type + ) + + if matching_rules := ( + kwargs.pop("response_matching_rules", None) + or kwargs.pop("response matching rules", None) + ): + self.response_matching_rules = parse_matching_rules(matching_rules) + + return kwargs + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return "".format( + ", ".join(f"{k}={v!r}" for k, v in vars(self).items()), + ) + + def add_to_pact( # noqa: C901, PLR0912, PLR0915 + self, + pact: Pact, + name: str, + ) -> None: + """ + Add the interaction to the pact. + + This is a convenience method that allows the interaction definition to + be added to the pact, defining the "upon receiving ... with ... will + respond with ...". + + Args: + pact: + The pact being defined. + + name: + Name for this interaction. Must be unique for the pact. + """ + interaction = pact.upon_receiving(name, self.type) + if isinstance(interaction, HttpInteraction): + assert self.method, "Method must be defined" + assert self.path, "Path must be defined" + + logger.info("with_request(%r, %r)", self.method, self.path) + interaction.with_request(self.method, self.path) + + for state in self.states or []: + if state.parameters: + logger.info("given(%r, parameters=%r)", state.name, state.parameters) + interaction.given(state.name, parameters=state.parameters) + else: + logger.info("given(%r)", state.name) + interaction.given(state.name) + + if self.pending: + logger.info("set_pending(True)") + interaction.set_pending(pending=True) + + if self.text_comments: + for comment in self.text_comments: + logger.info("add_text_comment(%r)", comment) + interaction.add_text_comment(comment) + + for key, value in self.comments.items(): + logger.info("set_comment(%r, %r)", key, value) + interaction.set_comment(key, value) + + if self.test_name: + logger.info("test_name(%r)", self.test_name) + interaction.test_name(self.test_name) + + if self.query: + assert isinstance( + interaction, HttpInteraction + ), "Query parameters require an HTTP interaction" + query = URL.build(query_string=self.query).query + logger.info("with_query_parameters(%r)", query.items()) + interaction.with_query_parameters(query.items()) + + if self.headers: + assert isinstance( + interaction, HttpInteraction + ), "Headers require an HTTP interaction" + logger.info("with_headers(%r)", self.headers.items()) + interaction.with_headers(self.headers.items()) + + if self.body: + self.body.add_to_interaction(interaction) + + if self.matching_rules: + logger.info("with_matching_rules(%r)", self.matching_rules) + interaction.with_matching_rules(self.matching_rules) + + if self.response: + assert isinstance( + interaction, HttpInteraction + ), "Response requires an HTTP interaction" + logger.info("will_respond_with(%r)", self.response) + interaction.will_respond_with(self.response) + + if self.response_headers: + assert isinstance( + interaction, HttpInteraction + ), "Response headers require an HTTP interaction" + logger.info("with_headers(%r)", self.response_headers) + interaction.with_headers(self.response_headers.items()) + + if self.response_body: + assert isinstance( + interaction, HttpInteraction + ), "Response body requires an HTTP interaction" + self.response_body.add_to_interaction(interaction) + + if self.response_matching_rules: + logger.info("with_matching_rules(%r)", self.response_matching_rules) + interaction.with_matching_rules(self.response_matching_rules) + + if self.metadata: + for key, value in self.metadata.items(): + if isinstance(value, str): + interaction.with_metadata({key: value}) + else: + interaction.with_metadata({key: json.dumps(value)}) + + def add_to_provider(self, provider: Provider) -> None: + """ + Add an interaction to a Flask app. + + Args: + provider: + The test provider to add the interaction to. + """ + logger.debug("Adding %s interaction to Flask app", self.type) + if self.type == "HTTP": + self._add_http_to_provider(provider) + elif self.type == "Sync": + self._add_sync_to_provider(provider) + elif self.type == "Async": + self._add_async_to_provider(provider) + else: + msg = f"Unknown interaction type: {self.type}" + raise ValueError(msg) + + def _add_http_to_provider(self, provider: Provider) -> None: + """ + Add a HTTP interaction to a Flask app. + + Ths function works by defining a new function to handle the request and + produce the response. This function is then added to the Flask app as a + route. + + Args: + provider: + The test provider to add the interaction to. + """ + assert isinstance(self.method, str), "Method must be a string" + assert isinstance(self.path, str), "Path must be a string" + + logger.info( + "Adding HTTP '%s %s' interaction to Flask app", + self.method, + self.path, + ) + logger.debug("-> Query: %s", self.query) + logger.debug("-> Headers: %s", self.headers) + logger.debug("-> Body: %s", self.body) + logger.debug("-> Response Status: %s", self.response) + logger.debug("-> Response Headers: %s", self.response_headers) + logger.debug("-> Response Body: %s", self.response_body) + + def route_fn() -> flask.Response: + if self.query: + query = URL.build(query_string=self.query).query + # Perform a two-way check to ensure that the query parameters + # are present in the request, and that the request contains no + # unexpected query parameters. + for k, v in query.items(): + assert request.args[k] == v + for k, v in request.args.items(): + assert query[k] == v + + if self.headers: + # Perform a one-way check to ensure that the expected headers + # are present in the request, but don't check for any unexpected + # headers. + for k, v in self.headers.items(): + assert k in request.headers + assert request.headers[k] == v + + if self.body: + assert request.data == self.body.bytes + + return flask.Response( + response=self.response_body.bytes or self.response_body.string or None + if self.response_body + else None, + status=self.response, + headers=dict(**self.response_headers), + content_type=self.response_body.mime_type + if self.response_body + else None, + direct_passthrough=True, + ) + + # The route function needs to have a unique name + clean_name = self.path.replace("/", "_").replace("__", "_") + route_fn.__name__ = f"{self.method.lower()}_{clean_name}" + + provider.app.add_url_rule( + self.path, + view_func=route_fn, + methods=[self.method], + ) + + def _add_sync_to_provider(self, provider: Provider) -> None: + """ + Add a synchronous message interaction to a Flask app. + + Args: + provider: + The test provider to add the interaction to. + """ + raise NotImplementedError + + def _add_async_to_provider(self, provider: Provider) -> None: + """ + Add a synchronous message interaction to a Flask app. + + Args: + provider: + The test provider to add the interaction to. + """ + assert self.description, "Description must be set for async messages" + provider.messages[self.description] = self + + # All messages are handled by the same route. So we just need to check + # whether the route has been defined, and if not, define it. + for rule in provider.app.url_map.iter_rules(): + if rule.rule == "/_pact/message": + sys.stderr.write("Async message route already defined\n") + return + + sys.stderr.write("Adding async message route\n") + + @provider.app.post("/_pact/message") + def post_message() -> flask.Response: + body: dict[str, Any] = json.loads(request.data) + description: str = body["description"] + + if description not in provider.messages: + return flask.Response( + response=json.dumps({ + "error": f"Message {description} not found", + }), + status=404, + headers={"Content-Type": "application/json"}, + content_type="application/json", + ) + + interaction: InteractionDefinition = provider.messages[description] + return interaction.create_async_message_response() + + def create_async_message_response(self) -> flask.Response: + """ + Convert the interaction to a Flask response. + + When an async message needs to be produced, Pact expects the response + from the special `/_pact/message` endppoint to generate the expected + message. + + Whilst this is a Response from Flask's perspective, the attributes + returned + """ + assert self.type == "Async", "Only async messages are supported" + + if self.metadata: + self.headers["Pact-Message-Metadata"] = base64.b64encode( + json.dumps(self.metadata).encode("utf-8") + ).decode("utf-8") + + return flask.Response( + response=self.body.bytes or self.body.string or None if self.body else None, + headers=((k, v) for k, v in self.headers.items()), + content_type=self.body.mime_type if self.body else None, + direct_passthrough=True, + ) diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 4288230b8..53ff8e40a 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -49,12 +49,15 @@ import pact.constants # type: ignore[import-untyped] from pact.v3.pact import Pact from tests.v3.compatibility_suite.util import ( - InteractionDefinition, parse_headers, parse_markdown_table, serialize, truncate, ) +from tests.v3.compatibility_suite.util.interaction_definition import ( + InteractionDefinition, + InteractionState, +) if TYPE_CHECKING: from collections.abc import Generator @@ -179,24 +182,24 @@ def __init__(self, provider_dir: Path | str, log_level: int) -> None: raise ValueError(msg) self.app: flask.Flask = flask.Flask("provider") - self._add_ping(self.app) - self._add_callback(self.app) - self._add_after_request(self.app) - self._add_interactions(self.app) + self._add_ping() + self._add_callback() + self._add_after_request() + self._add_interactions() - def _add_ping(self, app: flask.Flask) -> None: + def _add_ping(self) -> None: """ Add a ping endpoint to the provider. This is used to check that the provider is running. """ - @app.get("/_test/ping") + @self.app.get("/_test/ping") def ping() -> str: """Simple ping endpoint for testing.""" return "pong" - def _add_callback(self, app: flask.Flask) -> None: + def _add_callback(self) -> None: """ Add a callback endpoint to the provider. @@ -213,7 +216,7 @@ def _add_callback(self, app: flask.Flask) -> None: contents of the file. """ - @app.route("/_pact/callback", methods=["GET", "POST"]) + @self.app.route("/_pact/callback", methods=["GET", "POST"]) def callback() -> tuple[str, int] | str: if (self.provider_dir / "fail_callback").exists(): return "Provider state not found", 404 @@ -222,7 +225,7 @@ def callback() -> tuple[str, int] | str: if provider_states_path.exists(): logger.debug("Provider states file found") with provider_states_path.open() as f: - states = [InteractionDefinition.State(**s) for s in json.load(f)] + states = [InteractionState(**s) for s in json.load(f)] logger.debug("Provider states: %s", states) for state in states: if request.args["state"] == state.name: @@ -257,7 +260,7 @@ def callback() -> tuple[str, int] | str: return "" - def _add_after_request(self, app: flask.Flask) -> None: + def _add_after_request(self) -> None: """ Add a handler to log requests and responses. @@ -265,7 +268,7 @@ def _add_after_request(self, app: flask.Flask) -> None: application (both to the logger as well as to files). """ - @app.after_request + @self.app.after_request def log_request(response: flask.Response) -> flask.Response: logger.debug("Received request: %s %s", request.method, request.path) logger.debug("-> Query string: %s", request.query_string.decode("utf-8")) @@ -292,7 +295,7 @@ def log_request(response: flask.Response) -> flask.Response: ) return response - @app.after_request + @self.app.after_request def log_response(response: flask.Response) -> flask.Response: logger.debug("Returning response: %d", response.status_code) logger.debug("-> Headers: %s", serialize(response.headers)) @@ -320,7 +323,7 @@ def log_response(response: flask.Response) -> flask.Response: ) return response - def _add_interactions(self, app: flask.Flask) -> None: + def _add_interactions(self) -> None: """ Add the interactions to the provider. """ @@ -328,7 +331,7 @@ def _add_interactions(self, app: flask.Flask) -> None: interactions: list[InteractionDefinition] = pickle.load(f) # noqa: S301 for interaction in interactions: - interaction.add_to_flask(app) + interaction.add_to_provider(self) def run(self) -> None: """ @@ -537,7 +540,7 @@ def latest_verification_results(self) -> requests.Response | None: sys.stderr.write(f"Usage: {sys.argv[0]} \n") sys.exit(1) - Provider(sys.argv[1], sys.argv[2]).run() + Provider(sys.argv[1], int(sys.argv[2])).run() ################################################################################ @@ -608,7 +611,7 @@ def _( defns: list[InteractionDefinition] = [] for interaction in interactions: defn = copy.deepcopy(interaction_definitions[interaction]) - defn.update(**changes[0]) + defn.update(**changes[0]) # type: ignore[arg-type] defns.append(defn) logger.debug( "Updated interaction %d: %s", @@ -950,7 +953,7 @@ def _( ) defn = interaction_definitions[interaction] - defn.states = [InteractionDefinition.State(state)] + defn.states = [InteractionState(state)] pact = Pact("consumer", "provider") pact.with_specification(version) @@ -996,8 +999,7 @@ def _( defn = interaction_definitions[interaction] defn.states = [ - InteractionDefinition.State(s["State Name"], s.get("Parameters", None)) - for s in states + InteractionState(s["State Name"], s.get("Parameters", None)) for s in states ] pact = Pact("consumer", "provider") From 058c9ac02afdfad20570ca007c29a2d475ca1aa7 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 18 Jul 2024 17:03:52 +1000 Subject: [PATCH 0430/1376] feat(ffi): upgrade ffi to 0.4.22 Signed-off-by: JP-Ellis --- hatch_build.py | 2 +- src/pact/v3/ffi.py | 545 ++++++++++--------- src/pact/v3/interaction/_http_interaction.py | 4 +- tests/v3/test_verifier.py | 2 +- 4 files changed, 296 insertions(+), 257 deletions(-) diff --git a/hatch_build.py b/hatch_build.py index ab5e84e0f..dcb65ec1c 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -36,7 +36,7 @@ # Latest version available at: # https://github.com/pact-foundation/pact-reference/releases -PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.21") +PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.22") PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{machine}.{ext}" diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 4d38f17b3..0231ac4d9 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -417,7 +417,7 @@ class InteractionHandle: Handle to a HTTP Interaction. [Rust - `InteractionHandle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/handles/struct.InteractionHandle.html) + `InteractionHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/struct.InteractionHandle.html) """ def __init__(self, ref: int) -> None: @@ -879,7 +879,7 @@ class PactHandle: Handle to a Pact. [Rust - `PactHandle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/handles/struct.PactHandle.html) + `PactHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/struct.PactHandle.html) """ def __init__(self, ref: int) -> None: @@ -1517,7 +1517,7 @@ class VerifierHandle: """ Handle to a Verifier. - [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/verifier/handle/struct.VerifierHandle.html) + [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/verifier/handle/struct.VerifierHandle.html) """ def __init__(self, ref: cffi.FFI.CData) -> None: @@ -1553,7 +1553,7 @@ class ExpressionValueType(Enum): """ Expression Value Type. - [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/expressions/enum.ExpressionValueType.html) + [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/expressions/enum.ExpressionValueType.html) """ UNKNOWN = lib.ExpressionValueType_Unknown @@ -1580,7 +1580,7 @@ class GeneratorCategory(Enum): """ Generator Category. - [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/generators/enum.GeneratorCategory.html) + [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/generators/enum.GeneratorCategory.html) """ METHOD = lib.GeneratorCategory_METHOD @@ -1608,7 +1608,7 @@ class InteractionPart(Enum): """ Interaction Part. - [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/handles/enum.InteractionPart.html) + [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/enum.InteractionPart.html) """ REQUEST = lib.InteractionPart_Request @@ -1654,7 +1654,7 @@ class MatchingRuleCategory(Enum): """ Matching Rule Category. - [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) + [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) """ METHOD = lib.MatchingRuleCategory_METHOD @@ -1683,7 +1683,7 @@ class PactSpecification(Enum): """ Pact Specification. - [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/pact_specification/enum.PactSpecification.html) + [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/pact_specification/enum.PactSpecification.html) """ UNKNOWN = lib.PactSpecification_Unknown @@ -1736,7 +1736,7 @@ class _StringResult(Enum): """ Internal enum from Pact FFI. - [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/enum.StringResult.html) + [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/enum.StringResult.html) """ FAILED = lib.StringResult_Failed @@ -1892,7 +1892,7 @@ def version() -> str: """ Return the version of the pact_ffi library. - [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_version) + [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_version) Returns: The version of the pact_ffi library as a string, in the form of `x.y.z`. @@ -1912,7 +1912,7 @@ def init(log_env_var: str) -> None: tracing subscriber. [Rust - `pactffi_init`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_init) + `pactffi_init`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_init) # Safety @@ -1929,7 +1929,7 @@ def init_with_log_level(level: str = "INFO") -> None: tracing subscriber. [Rust - `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_init_with_log_level) + `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_init_with_log_level) Args: level: @@ -1949,7 +1949,7 @@ def enable_ansi_support() -> None: On non-Windows platforms, this function is a no-op. [Rust - `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_enable_ansi_support) + `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_enable_ansi_support) # Safety @@ -1967,7 +1967,7 @@ def log_message( Log using the shared core logging facility. [Rust - `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_message) + `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_message) This is useful for callers to have a single set of logs. @@ -1999,7 +1999,7 @@ def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: Get an iterator over mismatches. [Rust - `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_get_iter) + `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_get_iter) """ raise NotImplementedError @@ -2008,7 +2008,7 @@ def mismatches_delete(mismatches: Mismatches) -> None: """ Delete mismatches. - [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_delete) + [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_delete) """ raise NotImplementedError @@ -2017,7 +2017,7 @@ def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: """ Get the next mismatch from a mismatches iterator. - [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_iter_next) + [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_iter_next) Returns a null pointer if no mismatches remain. """ @@ -2028,7 +2028,7 @@ def mismatches_iter_delete(iter: MismatchesIterator) -> None: """ Delete a mismatches iterator when you're done with it. - [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_iter_delete) + [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_iter_delete) """ raise NotImplementedError @@ -2037,7 +2037,7 @@ def mismatch_to_json(mismatch: Mismatch) -> str: """ Get a JSON representation of the mismatch. - [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_to_json) + [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_to_json) """ raise NotImplementedError @@ -2046,7 +2046,7 @@ def mismatch_type(mismatch: Mismatch) -> str: """ Get the type of a mismatch. - [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_type) + [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_type) """ raise NotImplementedError @@ -2055,7 +2055,7 @@ def mismatch_summary(mismatch: Mismatch) -> str: """ Get a summary of a mismatch. - [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_summary) + [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_summary) """ raise NotImplementedError @@ -2064,7 +2064,7 @@ def mismatch_description(mismatch: Mismatch) -> str: """ Get a description of a mismatch. - [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_description) + [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_description) """ raise NotImplementedError @@ -2073,7 +2073,7 @@ def mismatch_ansi_description(mismatch: Mismatch) -> str: """ Get an ANSI-compatible description of a mismatch. - [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_ansi_description) + [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_ansi_description) """ raise NotImplementedError @@ -2083,7 +2083,7 @@ def get_error_message(length: int = 1024) -> str | None: Provide the error message from `LAST_ERROR` to the calling C code. [Rust - `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_get_error_message) + `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_get_error_message) This function should be called after any other function in the pact_matching FFI indicates a failure with its own error message, if the caller wants to @@ -2137,7 +2137,7 @@ def log_to_stdout(level_filter: LevelFilter) -> int: """ Convenience function to direct all logging to stdout. - [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_stdout) + [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_stdout) """ raise NotImplementedError @@ -2147,7 +2147,7 @@ def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: Convenience function to direct all logging to stderr. [Rust - `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_stderr) + `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_stderr) Args: level_filter: @@ -2172,7 +2172,7 @@ def log_to_file(file_name: str, level_filter: LevelFilter) -> int: Convenience function to direct all logging to a file. [Rust - `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_file) + `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_file) # Safety @@ -2186,7 +2186,7 @@ def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: """ Convenience function to direct all logging to a task local memory buffer. - [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_buffer) + [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_buffer) Raises: RuntimeError: @@ -2204,7 +2204,7 @@ def logger_init() -> None: """ Initialize the FFI logger with no sinks. - [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_logger_init) + [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_init) This initialized logger does nothing until `pactffi_logger_apply` has been called. @@ -2226,7 +2226,7 @@ def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: Attach an additional sink to the thread-local logger. [Rust - `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_logger_attach_sink) + `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_attach_sink) This logger does nothing until `pactffi_logger_apply` has been called. @@ -2273,7 +2273,7 @@ def logger_apply() -> int: Apply the previously configured sinks and levels to the program. [Rust - `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_logger_apply) + `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_apply) If no sinks have been setup, will set the log level to info and the target to standard out. @@ -2289,7 +2289,7 @@ def fetch_log_buffer(log_id: str) -> str: Fetch the in-memory logger buffer contents. [Rust - `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_fetch_log_buffer) + `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_fetch_log_buffer) This will only have any contents if the `buffer` sink has been configured to log to. The contents will be allocated on the heap and will need to be freed @@ -2315,7 +2315,7 @@ def parse_pact_json(json: str) -> Pact: Parses the provided JSON into a Pact model. [Rust - `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_parse_pact_json) + `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_parse_pact_json) The returned Pact model must be freed with the `pactffi_pact_model_delete` function when no longer needed. @@ -2332,7 +2332,7 @@ def pact_model_delete(pact: Pact) -> None: """ Frees the memory used by the Pact model. - [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_model_delete) + [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_model_delete) """ raise NotImplementedError @@ -2342,7 +2342,7 @@ def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: Returns an iterator over all the interactions in the Pact. [Rust - `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_model_interaction_iterator) + `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_model_interaction_iterator) The iterator will have to be deleted using the `pactffi_pact_interaction_iter_delete` function. The iterator will contain a @@ -2361,7 +2361,7 @@ def pact_spec_version(pact: Pact) -> PactSpecification: """ Returns the Pact specification enum that the Pact is for. - [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_spec_version) + [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_spec_version) """ raise NotImplementedError @@ -2370,7 +2370,7 @@ def pact_interaction_delete(interaction: PactInteraction) -> None: """ Frees the memory used by the Pact interaction model. - [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_delete) + [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_delete) """ raise NotImplementedError @@ -2379,7 +2379,7 @@ def async_message_new() -> AsynchronousMessage: """ Get a mutable pointer to a newly-created default message on the heap. - [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_new) + [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_new) # Safety @@ -2396,7 +2396,7 @@ def async_message_delete(message: AsynchronousMessage) -> None: """ Destroy the `AsynchronousMessage` being pointed to. - [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_delete) + [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_delete) """ lib.pactffi_async_message_delete(message._ptr) @@ -2406,7 +2406,7 @@ def async_message_get_contents(message: AsynchronousMessage) -> MessageContents Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. [Rust - `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents) + `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents) If the message contents are missing, this function will return `None`. """ @@ -2425,7 +2425,7 @@ def async_message_generate_contents( contents as would be received by the consumer. [Rust - `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_generate_contents) + `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_generate_contents) If the message contents are missing, this function will return `None`. """ @@ -2439,7 +2439,7 @@ def async_message_get_contents_str(message: AsynchronousMessage) -> str: """ Get the message contents of an `AsynchronousMessage` in string form. - [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents_str) + [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_str) # Safety @@ -2466,7 +2466,7 @@ def async_message_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_set_contents_str) + `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_contents_str) - `message` - the message to set the contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -2494,7 +2494,7 @@ def async_message_get_contents_length(message: AsynchronousMessage) -> int: Get the length of the contents of a `AsynchronousMessage`. [Rust - `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents_length) + `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_length) # Safety @@ -2513,7 +2513,7 @@ def async_message_get_contents_bin(message: AsynchronousMessage) -> str: Get the contents of an `AsynchronousMessage` as bytes. [Rust - `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents_bin) + `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_bin) # Safety @@ -2540,7 +2540,7 @@ def async_message_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_set_contents_bin) + `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_contents_bin) * `message` - the message to set the contents for * `contents` - pointer to contents to copy from @@ -2567,7 +2567,7 @@ def async_message_get_description(message: AsynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_description) + `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_description) Raises: RuntimeError: @@ -2587,7 +2587,7 @@ def async_message_set_description( """ Write the `description` field on the `AsynchronousMessage`. - [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_set_description) + [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_description) # Safety @@ -2612,7 +2612,7 @@ def async_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_provider_state) + `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_provider_state) Raises: RuntimeError: @@ -2631,7 +2631,7 @@ def async_message_get_provider_state_iter( """ Get an iterator over provider states. - [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) + [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) # Safety @@ -2646,7 +2646,7 @@ def consumer_get_name(consumer: Consumer) -> str: r""" Get a copy of this consumer's name. - [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_consumer_get_name) + [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_consumer_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -2692,7 +2692,7 @@ def pact_get_consumer(pact: Pact) -> Consumer: `pactffi_pact_consumer_delete` when no longer required. [Rust - `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_get_consumer) + `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_get_consumer) # Errors @@ -2706,7 +2706,7 @@ def pact_consumer_delete(consumer: Consumer) -> None: """ Frees the memory used by the Pact consumer. - [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_consumer_delete) + [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_consumer_delete) """ raise NotImplementedError @@ -2722,7 +2722,7 @@ def message_contents_delete(contents: MessageContents) -> None: Deleting a message content which is associated with an interaction will result in undefined behaviour. - [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_delete) + [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_delete) """ lib.pactffi_message_contents_delete(contents._ptr) @@ -2731,7 +2731,7 @@ def message_contents_get_contents_str(contents: MessageContents) -> str | None: """ Get the message contents in string form. - [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_contents_str) + [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_str) If the message has no contents or contain invalid UTF-8 characters, this function will return `None`. @@ -2751,7 +2751,7 @@ def message_contents_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_set_contents_str) + `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_set_contents_str) * `contents` - the message contents to set the contents for * `contents_str` - pointer to contents to copy from. Must be a valid @@ -2778,7 +2778,7 @@ def message_contents_get_contents_length(contents: MessageContents) -> int: """ Get the length of the message contents. - [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_contents_length) + [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_length) If the message has not contents, this function will return 0. """ @@ -2790,7 +2790,7 @@ def message_contents_get_contents_bin(contents: MessageContents) -> bytes | None Get the contents of a message as a pointer to an array of bytes. [Rust - `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_contents_bin) + `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_bin) If the message has no contents, this function will return `None`. """ @@ -2813,7 +2813,7 @@ def message_contents_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_set_contents_bin) + `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_set_contents_bin) * `message` - the message contents to set the contents for * `contents_bin` - pointer to contents to copy from @@ -2842,7 +2842,7 @@ def message_contents_get_metadata_iter( Get an iterator over the metadata of a message. [Rust - `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) + `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) # Safety @@ -2871,7 +2871,7 @@ def message_contents_get_matching_rule_iter( Get an iterator over the matching rules for a category of a message. [Rust - `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) + `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -2913,7 +2913,7 @@ def request_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP request. - [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) + [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -2950,7 +2950,7 @@ def response_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP response. - [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) + [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -2988,7 +2988,7 @@ def message_contents_get_generators_iter( Get an iterator over the generators for a category of a message. [Rust - `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_generators_iter) + `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_generators_iter) # Safety @@ -3014,7 +3014,7 @@ def request_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP request. [Rust - `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_request_contents_get_generators_iter) + `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_request_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -3039,7 +3039,7 @@ def response_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP response. [Rust - `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_contents_get_generators_iter) + `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -3064,7 +3064,7 @@ def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: any generator. [Rust - `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_parse_matcher_definition) + `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_parse_matcher_definition) The following are examples of matching rule definitions: @@ -3100,7 +3100,7 @@ def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: using the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_error) + `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_error) """ raise NotImplementedError @@ -3114,7 +3114,7 @@ def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_value) + `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_value) Note that different expressions values can have types other than a string. Use `pactffi_matcher_definition_value_type` to get the actual type of the @@ -3128,7 +3128,7 @@ def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: """ Frees the memory used by the result of parsing the matching definition expression. - [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_delete) + [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_delete) """ raise NotImplementedError @@ -3141,7 +3141,7 @@ def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Ge NULL pointer, otherwise returns the generator as a pointer. [Rust - `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_generator) + `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_generator) The generator pointer will be a valid pointer as long as `pactffi_matcher_definition_delete` has not been called on the definition. @@ -3160,7 +3160,7 @@ def matcher_definition_value_type( If there was an error parsing the expression, it will return Unknown. [Rust - `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_value_type) + `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_value_type) """ raise NotImplementedError @@ -3169,7 +3169,7 @@ def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: """ Free the iterator when you're done using it. - [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_iter_delete) + [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_iter_delete) """ raise NotImplementedError @@ -3184,7 +3184,7 @@ def matcher_definition_iter( `pactffi_matching_rule_iter_delete` function once done with it. [Rust - `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_iter) + `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_iter) If there was an error parsing the expression, this function will return a NULL pointer. @@ -3200,7 +3200,7 @@ def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: deleted but will be cleaned up when the iterator is deleted. [Rust - `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_iter_next) + `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_iter_next) Will return a NULL pointer when the iterator has advanced past the end of the list. @@ -3222,7 +3222,7 @@ def matching_rule_id(rule_result: MatchingRuleResult) -> int: Return the ID of the matching rule. [Rust - `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_id) + `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_id) The ID corresponds to the following rules: @@ -3268,7 +3268,7 @@ def matching_rule_value(rule_result: MatchingRuleResult) -> str: pointer. [Rust - `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_value) + `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_value) The associated values for the rules are: @@ -3316,7 +3316,7 @@ def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: Will return a NULL pointer if the matching rule result was a reference. [Rust - `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_pointer) + `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_pointer) # Safety @@ -3334,7 +3334,7 @@ def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: structure. I.e., [Rust - `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_reference_name) + `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_reference_name) ```json { @@ -3361,7 +3361,7 @@ def validate_datetime(value: str, format: str) -> None: Validates the date/time value against the date/time format string. [Rust - `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_validate_datetime) + `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_validate_datetime) Raises: ValueError: @@ -3388,7 +3388,7 @@ def generator_to_json(generator: Generator) -> str: Get the JSON form of the generator. [Rust - `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generator_to_json) + `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -3411,7 +3411,7 @@ def generator_generate_string(generator: Generator, context_json: str) -> str: function). [Rust - `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generator_generate_string) + `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_generate_string) If anything goes wrong, it will return a NULL pointer. """ @@ -3434,7 +3434,7 @@ def generator_generate_integer(generator: Generator, context_json: str) -> int: should be the values returned from the Provider State callback function). [Rust - `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generator_generate_integer) + `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_generate_integer) If anything goes wrong or the generator is not a type that can generate an integer value, it will return a zero value. @@ -3450,7 +3450,7 @@ def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generators_iter_delete) + `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_delete) """ lib.pactffi_generators_iter_delete(iter._ptr) @@ -3460,7 +3460,7 @@ def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePa Get the next path and generator out of the iterator, if possible. [Rust - `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generators_iter_next) + `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_next) The returned pointer must be deleted with `pactffi_generator_iter_pair_delete`. @@ -3480,7 +3480,7 @@ def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: Free a pair of key and value returned from `pactffi_generators_iter_next`. [Rust - `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generators_iter_pair_delete) + `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_pair_delete) """ lib.pactffi_generators_iter_pair_delete(pair._ptr) @@ -3489,7 +3489,7 @@ def sync_http_new() -> SynchronousHttp: """ Get a mutable pointer to a newly-created default interaction on the heap. - [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_new) + [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_new) # Safety @@ -3507,7 +3507,7 @@ def sync_http_delete(interaction: SynchronousHttp) -> None: Destroy the `SynchronousHttp` interaction being pointed to. [Rust - `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_delete) + `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_delete) """ lib.pactffi_sync_http_delete(interaction) @@ -3517,7 +3517,7 @@ def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: Get the request of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request) + `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request) # Safety @@ -3537,7 +3537,7 @@ def sync_http_get_request_contents(interaction: SynchronousHttp) -> str | None: Get the request contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request_contents) + `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents) Note that this function will return `None` if either the body is missing or is `null`. @@ -3557,7 +3557,7 @@ def sync_http_set_request_contents( Sets the request contents of the interaction. [Rust - `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_request_contents) + `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_request_contents) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -3585,7 +3585,7 @@ def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: Get the length of the request contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) + `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) This function will return 0 if the body is missing. """ @@ -3597,7 +3597,7 @@ def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes | Get the request contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) + `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) Note that this function will return `None` if either the body is missing or is `null`. @@ -3621,7 +3621,7 @@ def sync_http_set_request_contents_bin( Sets the request contents of the interaction as an array of bytes. [Rust - `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) + `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from @@ -3648,7 +3648,7 @@ def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: Get the response of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response) + `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response) # Safety @@ -3668,7 +3668,7 @@ def sync_http_get_response_contents(interaction: SynchronousHttp) -> str | None: Get the response contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response_contents) + `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents) Note that this function will return `None` if either the body is missing or is `null`. @@ -3688,7 +3688,7 @@ def sync_http_set_response_contents( Sets the response contents of the interaction. [Rust - `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_response_contents) + `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_response_contents) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -3716,7 +3716,7 @@ def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: Get the length of the response contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) + `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) This function will return 0 if the body is missing. """ @@ -3728,7 +3728,7 @@ def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes | Get the response contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) + `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) Note that this function will return `None` if either the body is missing or is `null`. @@ -3752,7 +3752,7 @@ def sync_http_set_response_contents_bin( Sets the response contents of the `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) + `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from @@ -3779,7 +3779,7 @@ def sync_http_get_description(interaction: SynchronousHttp) -> str: Get a copy of the description. [Rust - `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_description) + `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_description) Raises: RuntimeError: @@ -3797,7 +3797,7 @@ def sync_http_set_description(interaction: SynchronousHttp, description: str) -> Write the `description` field on the `SynchronousHttp`. [Rust - `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_description) + `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_description) # Safety @@ -3822,7 +3822,7 @@ def sync_http_get_provider_state( Get a copy of the provider state at the given index from this interaction. [Rust - `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_provider_state) + `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_provider_state) # Safety @@ -3848,7 +3848,7 @@ def sync_http_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) + `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) # Safety @@ -3877,7 +3877,7 @@ def pact_interaction_as_synchronous_http( longer required. [Rust - `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) + `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) # Safety This function is safe as long as the interaction pointer is a valid pointer. @@ -3899,7 +3899,7 @@ def pact_interaction_as_asynchronous_message( no longer required. [Rust - `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) + `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) Note that if the interaction is a V3 `Message`, it will be converted to a V4 `AsynchronousMessage` before being returned. @@ -3924,7 +3924,7 @@ def pact_interaction_as_synchronous_message( no longer required. [Rust - `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) + `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) # Safety This function is safe as long as the interaction pointer is a valid pointer. @@ -3939,7 +3939,7 @@ def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> Asynchronous Get the next asynchronous message from the iterator. [Rust - `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_async_message_iter_next) + `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_async_message_iter_next) Raises: StopIteration: @@ -3956,7 +3956,7 @@ def pact_async_message_iter_delete(iter: PactAsyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_async_message_iter_delete) + `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_async_message_iter_delete) """ lib.pactffi_pact_async_message_iter_delete(iter._ptr) @@ -3966,7 +3966,7 @@ def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMes Get the next synchronous request/response message from the V4 pact. [Rust - `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_message_iter_next) + `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_message_iter_next) Raises: StopIteration: @@ -3983,7 +3983,7 @@ def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) + `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) """ lib.pactffi_pact_sync_message_iter_delete(iter._ptr) @@ -3993,7 +3993,7 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: Get the next synchronous HTTP request/response interaction from the V4 pact. [Rust - `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_http_iter_next) + `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_http_iter_next) Raises: StopIteration: @@ -4010,7 +4010,7 @@ def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) + `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) """ lib.pactffi_pact_sync_http_iter_delete(iter._ptr) @@ -4020,7 +4020,7 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction Get the next interaction from the pact. [Rust - `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_iter_next) + `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_iter_next) Raises: StopIteration: @@ -4038,7 +4038,7 @@ def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_iter_delete) + `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_iter_delete) """ lib.pactffi_pact_interaction_iter_delete(iter._ptr) @@ -4048,7 +4048,7 @@ def matching_rule_to_json(rule: MatchingRule) -> str: Get the JSON form of the matching rule. [Rust - `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_to_json) + `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -4065,7 +4065,7 @@ def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rules_iter_delete) + `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_delete) """ lib.pactffi_matching_rules_iter_delete(iter._ptr) @@ -4077,7 +4077,7 @@ def matching_rules_iter_next( Get the next path and matching rule out of the iterator, if possible. [Rust - `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rules_iter_next) + `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_next) The returned pointer must be deleted with `pactffi_matching_rules_iter_pair_delete`. @@ -4099,7 +4099,7 @@ def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) + `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) """ lib.pactffi_matching_rules_iter_pair_delete(pair._ptr) @@ -4109,7 +4109,7 @@ def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: Get the next value from the iterator. [Rust - `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_iter_next) + `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_iter_next) # Safety @@ -4130,7 +4130,7 @@ def provider_state_iter_delete(iter: ProviderStateIterator) -> None: Delete the iterator. [Rust - `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_iter_delete) + `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_iter_delete) """ lib.pactffi_provider_state_iter_delete(iter._ptr) @@ -4140,7 +4140,7 @@ def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadata Get the next key and value out of the iterator, if possible. [Rust - `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_metadata_iter_next) + `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_iter_next) The returned pointer must be deleted with `pactffi_message_metadata_pair_delete`. @@ -4166,7 +4166,7 @@ def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: Free the metadata iterator when you're done using it. [Rust - `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_metadata_iter_delete) + `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_iter_delete) """ lib.pactffi_message_metadata_iter_delete(iter._ptr) @@ -4176,7 +4176,7 @@ def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_metadata_pair_delete) + `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_pair_delete) """ lib.pactffi_message_metadata_pair_delete(pair._ptr) @@ -4186,7 +4186,7 @@ def provider_get_name(provider: Provider) -> str: Get a copy of this provider's name. [Rust - `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_get_name) + `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -4232,7 +4232,7 @@ def pact_get_provider(pact: Pact) -> Provider: `pactffi_pact_provider_delete` when no longer required. [Rust - `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_get_provider) + `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_get_provider) # Errors @@ -4247,7 +4247,7 @@ def pact_provider_delete(provider: Provider) -> None: Frees the memory used by the Pact provider. [Rust - `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_provider_delete) + `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_provider_delete) """ raise NotImplementedError @@ -4257,7 +4257,7 @@ def provider_state_get_name(provider_state: ProviderState) -> str | None: Get the name of the provider state as a string. [Rust - `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_get_name) + `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_get_name) Raises: RuntimeError: @@ -4277,7 +4277,7 @@ def provider_state_get_param_iter( Get an iterator over the params of a provider state. [Rust - `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_get_param_iter) + `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_get_param_iter) # Safety @@ -4305,7 +4305,7 @@ def provider_state_param_iter_next( Get the next key and value out of the iterator, if possible. [Rust - `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_param_iter_next) + `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_iter_next) # Safety @@ -4326,7 +4326,7 @@ def provider_state_delete(provider_state: ProviderState) -> None: Free the provider state when you're done using it. [Rust - `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_delete) + `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_delete) """ raise NotImplementedError @@ -4336,7 +4336,7 @@ def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: Free the provider state param iterator when you're done using it. [Rust - `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_param_iter_delete) + `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_iter_delete) """ lib.pactffi_provider_state_param_iter_delete(iter._ptr) @@ -4346,7 +4346,7 @@ def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: Free a pair of key and value. [Rust - `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_param_pair_delete) + `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_pair_delete) """ lib.pactffi_provider_state_param_pair_delete(pair._ptr) @@ -4356,7 +4356,7 @@ def sync_message_new() -> SynchronousMessage: Get a mutable pointer to a newly-created default message on the heap. [Rust - `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_new) + `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_new) # Safety @@ -4374,7 +4374,7 @@ def sync_message_delete(message: SynchronousMessage) -> None: Destroy the `Message` being pointed to. [Rust - `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_delete) + `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_delete) """ lib.pactffi_sync_message_delete(message._ptr) @@ -4384,7 +4384,7 @@ def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: Get the request contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) + `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) # Safety @@ -4411,7 +4411,7 @@ def sync_message_set_request_contents_str( Sets the request contents of the message. [Rust - `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) + `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) - `message` - the message to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -4439,7 +4439,7 @@ def sync_message_get_request_contents_length(message: SynchronousMessage) -> int Get the length of the request contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) + `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) # Safety @@ -4458,7 +4458,7 @@ def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes: Get the request contents of a `SynchronousMessage` as a bytes. [Rust - `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) + `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) # Safety @@ -4485,7 +4485,7 @@ def sync_message_set_request_contents_bin( Sets the request contents of the message as an array of bytes. [Rust - `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) + `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) * `message` - the message to set the request contents for * `contents` - pointer to contents to copy from @@ -4512,7 +4512,7 @@ def sync_message_get_request_contents(message: SynchronousMessage) -> MessageCon Get the request contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents) + `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents) # Safety @@ -4539,7 +4539,7 @@ def sync_message_generate_request_contents( contents as would be received by the consumer. [Rust - `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_generate_request_contents) + `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_generate_request_contents) Raises: RuntimeError: @@ -4557,7 +4557,7 @@ def sync_message_get_number_responses(message: SynchronousMessage) -> int: Get the number of response messages in the `SynchronousMessage`. [Rust - `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_number_responses) + `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_number_responses) If the message is null, this function will return 0. """ @@ -4572,7 +4572,7 @@ def sync_message_get_response_contents_str( Get the response contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) + `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) # Safety @@ -4605,7 +4605,7 @@ def sync_message_set_response_contents_str( with default values. [Rust - `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) + `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response. @@ -4637,7 +4637,7 @@ def sync_message_get_response_contents_length( Get the length of the response contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) + `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) # Safety @@ -4659,7 +4659,7 @@ def sync_message_get_response_contents_bin( Get the response contents of a `SynchronousMessage` as bytes. [Rust - `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) + `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) # Safety @@ -4690,7 +4690,7 @@ def sync_message_set_response_contents_bin( responses will be padded with default values. [Rust - `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) + `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response @@ -4721,7 +4721,7 @@ def sync_message_get_response_contents( Get the response contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents) + `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents) # Safety @@ -4750,7 +4750,7 @@ def sync_message_generate_response_contents( received by the consumer. [Rust - `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_generate_response_contents) + `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_generate_response_contents) Raises: RuntimeError: @@ -4768,7 +4768,7 @@ def sync_message_get_description(message: SynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_description) + `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_description) Raises: RuntimeError: @@ -4786,7 +4786,7 @@ def sync_message_set_description(message: SynchronousMessage, description: str) Write the `description` field on the `SynchronousMessage`. [Rust - `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_description) + `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_description) # Safety @@ -4811,7 +4811,7 @@ def sync_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_provider_state) + `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_provider_state) # Safety @@ -4837,7 +4837,7 @@ def sync_message_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) + `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) # Safety @@ -4859,7 +4859,7 @@ def string_delete(string: OwnedString) -> None: Delete a string previously returned by this FFI. [Rust - `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_string_delete) + `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_string_delete) """ lib.pactffi_string_delete(string._ptr) @@ -4874,7 +4874,7 @@ def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: the mock server is returned. [Rust - `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_create_mock_server) + `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server) * `pact_str` - Pact JSON * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:80) @@ -4910,7 +4910,7 @@ def get_tls_ca_certificate() -> OwnedString: Fetch the CA Certificate used to generate the self-signed certificate. [Rust - `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_get_tls_ca_certificate) + `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_get_tls_ca_certificate) **NOTE:** The string for the result is allocated on the heap, and will have to be freed by the caller using pactffi_string_delete. @@ -4931,7 +4931,7 @@ def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) - operating system. The port of the mock server is returned. [Rust - `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_create_mock_server_for_pact) + `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server_for_pact) * `pact` - Handle to a Pact model created with created with `pactffi_new_pact`. @@ -4974,7 +4974,7 @@ def create_mock_server_for_transport( Create a mock server for the provided Pact handle and transport. [Rust - `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_create_mock_server_for_transport) + `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server_for_transport) Args: pact: @@ -5036,7 +5036,7 @@ def mock_server_matched(mock_server_handle: PactServerHandle) -> bool: if any request has not been successfully matched, or the method panics. [Rust - `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mock_server_matched) + `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_matched) """ return lib.pactffi_mock_server_matched(mock_server_handle._ref) @@ -5048,7 +5048,7 @@ def mock_server_mismatches( External interface to get all the mismatches from a mock server. [Rust - `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mock_server_mismatches) + `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_mismatches) # Errors @@ -5075,7 +5075,7 @@ def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: and cleanup any memory allocated for it. [Rust - `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_cleanup_mock_server) + `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_cleanup_mock_server) Args: mock_server_handle: @@ -5104,7 +5104,7 @@ def write_pact_file( directory to write the file to is passed as the second parameter. [Rust - `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_write_pact_file) + `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_write_pact_file) Args: mock_server_handle: @@ -5156,7 +5156,7 @@ def mock_server_logs(mock_server_handle: PactServerHandle) -> str: started. [Rust - `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mock_server_logs) + `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_logs) Raises: RuntimeError: @@ -5180,7 +5180,7 @@ def generate_datetime_string(format: str) -> StringResult: string needs to be freed with the `pactffi_string_delete` function [Rust - `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generate_datetime_string) + `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generate_datetime_string) # Safety @@ -5197,7 +5197,7 @@ def check_regex(regex: str, example: str) -> bool: Checks that the example string matches the given regex. [Rust - `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_check_regex) + `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_check_regex) # Safety @@ -5216,7 +5216,7 @@ def generate_regex_value(regex: str) -> StringResult: `pactffi_string_delete` function. [Rust - `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generate_regex_value) + `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generate_regex_value) # Safety @@ -5231,7 +5231,7 @@ def free_string(s: str) -> None: [DEPRECATED] Frees the memory allocated to a string by another function. [Rust - `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_free_string) + `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_free_string) This function is deprecated. Use `pactffi_string_delete` instead. @@ -5253,7 +5253,7 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle: Creates a new Pact model and returns a handle to it. [Rust - `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_pact) + `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_pact) Args: consumer_name: @@ -5294,7 +5294,7 @@ def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: will result in that interaction being replaced with the new one. [Rust - `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_interaction) + `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_interaction) Args: pact: @@ -5322,7 +5322,7 @@ def new_message_interaction(pact: PactHandle, description: str) -> InteractionHa will result in that interaction being replaced with the new one. [Rust - `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_message_interaction) + `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_message_interaction) Args: pact: @@ -5353,7 +5353,7 @@ def new_sync_message_interaction( will result in that interaction being replaced with the new one. [Rust - `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_sync_message_interaction) + `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_sync_message_interaction) Args: pact: @@ -5378,7 +5378,7 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> None: Sets the description for the Interaction. [Rust - `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_upon_receiving) + `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_upon_receiving) This function @@ -5419,7 +5419,7 @@ def given(interaction: InteractionHandle, description: str) -> None: Adds a provider state to the Interaction. [Rust - `pactffi_given`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_given) + `pactffi_given`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given) Args: interaction: @@ -5446,7 +5446,7 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> Non used with V4 interactions. [Rust - `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_interaction_test_name) + `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_interaction_test_name) Args: interaction: @@ -5493,7 +5493,7 @@ def given_with_param( be parsed as JSON. [Rust - `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_given_with_param) + `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given_with_param) Args: interaction: @@ -5535,7 +5535,7 @@ def given_with_params( with a `value` key. [Rust - `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_given_with_params) + `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given_with_params) Args: interaction: @@ -5574,7 +5574,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None Configures the request for the Interaction. [Rust - `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_request) + `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_request) Args: interaction: @@ -5588,7 +5588,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) which allows regex patterns. For examples: ```json @@ -5623,7 +5623,7 @@ def with_query_parameter_v2( Configures a query parameter for the Interaction. [Rust - `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_query_parameter_v2) + `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_query_parameter_v2) To setup a query parameter with multiple values, you can either call this function multiple times with a different index value: @@ -5660,7 +5660,7 @@ def with_query_parameter_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) If you want the matching rules to apply to all values (and not just the one with the given index), make sure to set the value to be an array. @@ -5712,7 +5712,7 @@ def with_query_parameter_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -5734,7 +5734,7 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None: Sets the specification version for a given Pact model. [Rust - `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_specification) + `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_specification) Args: pact: @@ -5758,7 +5758,7 @@ def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: Fetches the Pact specification version for the given Pact model. [Rust - `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_handle_get_pact_spec_version) + `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_handle_get_pact_spec_version) Args: handle: @@ -5784,7 +5784,7 @@ def with_pact_metadata( mock server for it has already started) [Rust - `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_pact_metadata) + `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_pact_metadata) Args: pact: @@ -5847,7 +5847,7 @@ def with_metadata( ``` See - [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) # Note @@ -5896,7 +5896,7 @@ def with_header_v2( r""" Configures a header for the Interaction. - [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_header_v2) + [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_header_v2) To setup a header with multiple values, you can either call this function multiple times with a different index value: @@ -5934,7 +5934,7 @@ def with_header_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -5956,7 +5956,7 @@ def with_header_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -5988,7 +5988,7 @@ def set_header( and generators can not be configured with it. [Rust - `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_header) + `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_header) If matching rules are required to be set, use `pactffi_with_header_v2`. @@ -6026,7 +6026,7 @@ def response_status(interaction: InteractionHandle, status: int) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_status) + `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_status) Args: interaction: @@ -6050,7 +6050,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_status_v2) + `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_status_v2) To include matching rules for the status (only statusCode or integer really makes sense to use), include the matching rule JSON format with the value as @@ -6069,7 +6069,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -6080,7 +6080,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -6104,12 +6104,26 @@ def with_body( Adds the body for the interaction. [Rust - `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_body) + `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_body) - For HTTP and async message interactions, this will overwrite the body. With - asynchronous messages, the part parameter will be ignored. With synchronous - messages, the request contents will be overwritten, while a new response - will be appended to the message. + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) + + If the `content_type` is determined as follows, whichever is first: + + - The `content_type` argument to this function + - The `Content-Type` header for HTTP interaction, or `contentType` metadata + entry for message interactions. + - From automatic detection of the body contents. + - Defaults to `text/plain` as a last resort. + + Furthermore, the `Content-Type` header or `contentType` metadata entry will + be updated with the above determined content type, _unless_ it is already + set. + + This function will overwrite the body contents if they exist, with the + exception of the response part of synchronous message interactions, where a + new response will be appended. Args: interaction: @@ -6117,15 +6131,15 @@ def with_body( part: The part of the interaction to add the body to (Request or - Response). + Response). This is ignored for asynchronous message interactions. content_type: - The content type of the body. Will be ignored if a content type - header is already set. + The content type of the body, or `None` to use the internal logic. body: The body contents. For JSON payloads, matching rules can be embedded - in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). + in the body. See + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -6152,7 +6166,7 @@ def with_binary_body( Adds the body for the interaction. [Rust - `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_binary_body) + `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_binary_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -6207,12 +6221,20 @@ def with_binary_file( """ Adds a binary file as the body with the expected content type and contents. + !!! warning + + This function is deprecated. Use + [`with_binary_body`][pact.v3.ffi.with_binary_body] in order to set the + binary body, and use + [`with_matching_rules`][pact.v3.ffi.with_matching_rules] to set the + matching rules to ensure that only the content type is being matched. + Will use a mime type matcher to match the body. Returns false if the interaction or Pact can't be modified (i.e. the mock server for it has already started) [Rust - `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_binary_file) + `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_binary_file) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -6266,7 +6288,7 @@ def with_matching_rules( Add matching rules to the interaction. [Rust - `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_matching_rules) + `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_matching_rules) This function can be called multiple times, in which case the matching rules will be merged. @@ -6304,7 +6326,7 @@ def with_generators( Add generators to the interaction. [Rust - `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_generators) + `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_generators) This function can be called multiple times, in which case the generators will be combined (provide they don't clash). @@ -6352,7 +6374,7 @@ def with_multipart_file_v2( # noqa: PLR0913 already started) or an error occurs. [Rust - `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_multipart_file_v2) + `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_multipart_file_v2) This function can be called multiple times. In that case, each subsequent call will be appended to the existing multipart body as a new part. @@ -6406,7 +6428,7 @@ def with_multipart_file( already started) or an error occurs. [Rust - `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_multipart_file) + `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_multipart_file) * `interaction` - Interaction handle to set the body for. * `part` - Request or response part. @@ -6443,7 +6465,7 @@ def set_key(interaction: InteractionHandle, key: str | None) -> None: Sets the key attribute for the interaction. [Rust - `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_key) + `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_key) Args: interaction: @@ -6471,7 +6493,7 @@ def set_pending(interaction: InteractionHandle, *, pending: bool) -> None: Mark the interaction as pending. [Rust - `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_pending) + `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_pending) Args: interaction: @@ -6495,7 +6517,7 @@ def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> Add a comment to the interaction. [Rust - `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_comment) + `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_comment) Args: interaction: @@ -6529,7 +6551,7 @@ def add_text_comment(interaction: InteractionHandle, comment: str) -> None: Add a text comment to the interaction. [Rust - `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_add_text_comment) + `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_add_text_comment) Args: interaction: @@ -6559,7 +6581,7 @@ def pact_handle_get_async_message_iter(pact: PactHandle) -> PactAsyncMessageIter `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6585,7 +6607,7 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6611,7 +6633,7 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: `pactffi_pact_sync_http_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) + `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) # Safety @@ -6637,7 +6659,7 @@ def pact_handle_write_file( External interface to write out the pact file. [Rust - `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_write_file) + `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_write_file) This function should be called if all the consumer tests have passed. @@ -6681,7 +6703,7 @@ def free_pact_handle(pact: PactHandle) -> None: Delete a Pact handle and free the resources used by it. [Rust - `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_free_pact_handle) + `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_free_pact_handle) Raises: RuntimeError: @@ -6701,7 +6723,7 @@ def verify(args: str) -> int: """ External interface to verifier a provider. - [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verify) + [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verify) * `args` - the same as the CLI interface, except newline delimited @@ -6727,8 +6749,13 @@ def verifier_new_for_application() -> VerifierHandle: """ Get a Handle to a newly created verifier. + By default, verification results will not be published. To enable + publishing, use + [`pactffi_verifier_set_publish_options`][pact.v3.ffi.verifier_set_publish_options] + to set the required values and enable it. + [Rust - `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_new_for_application) + `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_new_for_application) """ from pact import __version__ @@ -6743,7 +6770,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None: """ Shutdown the verifier and release all resources. - [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_shutdown) + [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_shutdown) """ lib.pactffi_verifier_shutdown(handle._ref) @@ -6760,7 +6787,7 @@ def verifier_set_provider_info( # noqa: PLR0913 Set the provider details for the Pact verifier. [Rust - `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_provider_info) + `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_provider_info) Args: handle: @@ -6806,7 +6833,7 @@ def verifier_add_provider_transport( Adds a new transport for the given provider. [Rust - `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_provider_transport) + `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_provider_transport) Args: handle: @@ -6847,7 +6874,7 @@ def verifier_set_filter_info( Set the filters for the Pact verifier. [Rust - `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_filter_info) + `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_filter_info) Set filters to narrow down the interactions to verify. @@ -6883,7 +6910,7 @@ def verifier_set_provider_state( Set the provider state URL for the Pact verifier. [Rust - `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_provider_state) + `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_provider_state) Args: handle: @@ -6918,7 +6945,7 @@ def verifier_set_verification_options( Set the options used by the verifier when calling the provider. [Rust - `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_verification_options) + `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_verification_options) Args: handle: @@ -6953,7 +6980,7 @@ def verifier_set_coloured_output( Enables or disables coloured output using ANSI escape codes. [Rust - `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_coloured_output) + `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_coloured_output) By default, coloured output is enabled. @@ -6982,7 +7009,7 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> Enables or disables if no pacts are found to verify results in an error. [Rust - `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) + `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) Args: handle: @@ -7015,7 +7042,7 @@ def verifier_set_publish_options( Set the options used when publishing verification results to the Broker. [Rust - `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_publish_options) + `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_publish_options) Args: handle: @@ -7058,7 +7085,7 @@ def verifier_set_consumer_filters( Set the consumer filters for the Pact verifier. [Rust - `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_consumer_filters) + `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_consumer_filters) """ lib.pactffi_verifier_set_consumer_filters( handle._ref, @@ -7076,7 +7103,7 @@ def verifier_add_custom_header( Adds a custom header to be added to the requests made to the provider. [Rust - `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_custom_header) + `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_custom_header) """ lib.pactffi_verifier_add_custom_header( handle._ref, @@ -7090,7 +7117,7 @@ def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: Adds a Pact file as a source to verify. [Rust - `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_file_source) + `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_file_source) """ lib.pactffi_verifier_add_file_source(handle._ref, file.encode("utf-8")) @@ -7102,7 +7129,7 @@ def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> Non All pacts from the directory that match the provider name will be verified. [Rust - `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_directory_source) + `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_directory_source) # Safety @@ -7124,7 +7151,7 @@ def verifier_url_source( Adds a URL as a source to verify. [Rust - `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_url_source) + `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_url_source) Args: handle: @@ -7164,7 +7191,7 @@ def verifier_broker_source( Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_broker_source) + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_broker_source) This will fetch all the pact files from the broker that match the provider name. @@ -7212,12 +7239,16 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) + `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) This will fetch all the pact files from the broker that match the provider name and the consumer version selectors (See [Consumer Version Selectors](https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/)). + If a username and password is given, then basic authentication will be used + when fetching the pact file. If a token is provided, then bearer token + authentication will be used. + Args: handle: The verifier handle to update. @@ -7249,11 +7280,12 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 consumer_version_selectors: The consumer version selectors to use to filter the consumer pacts. + This must be passed in as a JSON string. consumer_version_tags: The tags to use to filter the consumer pacts. """ - lib.pactffi_verifier_broker_source_with_selectors( + ret: int = lib.pactffi_verifier_broker_source_with_selectors( handle._ref, url.encode("utf-8"), username.encode("utf-8") if username else ffi.NULL, @@ -7273,13 +7305,20 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 [ffi.new("char[]", t.encode("utf-8")) for t in consumer_version_tags], len(consumer_version_tags), ) + if ret == 0: + return + if ret == -1: + msg = "Invalid version selector JSON." + raise ValueError(msg) + msg = "Unknown error adding broker source with selectors." + raise RuntimeError(msg) def verifier_execute(handle: VerifierHandle) -> None: """ Runs the verification. - (https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_execute) + (https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_execute) Raises: RuntimeError: @@ -7299,7 +7338,7 @@ def verifier_cli_args() -> str: string. [Rust - `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_cli_args) + `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_cli_args) The purpose is to then be able to use in other languages which wrap the FFI library, to implement the same CLI functionality automatically without @@ -7355,7 +7394,7 @@ def verifier_logs(handle: VerifierHandle) -> OwnedString: Extracts the logs for the verification run. [Rust - `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_logs) + `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_logs) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` @@ -7377,7 +7416,7 @@ def verifier_logs_for_provider(provider_name: str) -> OwnedString: Extracts the logs for the verification run for the provider name. [Rust - `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_logs_for_provider) + `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_logs_for_provider) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` @@ -7399,7 +7438,7 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: Extracts the standard output for the verification run. [Rust - `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_output) + `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_output) Args: handle: @@ -7426,7 +7465,7 @@ def verifier_json(handle: VerifierHandle) -> OwnedString: Extracts the verification result as a JSON document. [Rust - `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_json) + `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_json) Raises: RuntimeError: @@ -7454,7 +7493,7 @@ def using_plugin( otherwise you will have plugin processes left running. [Rust - `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_using_plugin) + `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_using_plugin) Args: pact: @@ -7497,7 +7536,7 @@ def cleanup_plugins(pact: PactHandle) -> None: zero). [Rust - `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_cleanup_plugins) + `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_cleanup_plugins) """ lib.pactffi_cleanup_plugins(pact._ref) @@ -7516,7 +7555,7 @@ def interaction_contents( format of the JSON contents. [Rust - `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_interaction_contents) + `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_interaction_contents) Args: interaction: @@ -7576,7 +7615,7 @@ def matches_string_value( function once it is no longer required. [Rust - `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_string_value) + `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_string_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string @@ -7607,7 +7646,7 @@ def matches_u64_value( function once it is no longer required. [Rust - `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_u64_value) + `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_u64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7637,7 +7676,7 @@ def matches_i64_value( function once it is no longer required. [Rust - `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_i64_value) + `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_i64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7667,7 +7706,7 @@ def matches_f64_value( function once it is no longer required. [Rust - `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_f64_value) + `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_f64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7697,7 +7736,7 @@ def matches_bool_value( function once it is no longer required. [Rust - `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_bool_value) + `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_bool_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get, 0 == false and 1 == true @@ -7729,7 +7768,7 @@ def matches_binary_value( # noqa: PLR0913 function once it is no longer required. [Rust - `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_binary_value) + `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_binary_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7764,7 +7803,7 @@ def matches_json_value( function once it is no longer required. [Rust - `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_json_value) + `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_json_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string diff --git a/src/pact/v3/interaction/_http_interaction.py b/src/pact/v3/interaction/_http_interaction.py index 1f00bf194..d7b5d5f8f 100644 --- a/src/pact/v3/interaction/_http_interaction.py +++ b/src/pact/v3/interaction/_http_interaction.py @@ -149,7 +149,7 @@ def with_header( # JSON Matching Pact's matching rules are defined in the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) and support a wide range of matching rules. These can be specified using a JSON object as a strong using `json.dumps(...)`. For example, the above rule whereby the `X-Foo` header has multiple values can be @@ -391,7 +391,7 @@ def with_query_parameter(self, name: str, value: str) -> Self: ``` For more information on the format of the JSON object, see the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). Args: name: diff --git a/tests/v3/test_verifier.py b/tests/v3/test_verifier.py index cedb66511..aab32d1b9 100644 --- a/tests/v3/test_verifier.py +++ b/tests/v3/test_verifier.py @@ -157,7 +157,7 @@ def test_broker_source_selector(verifier: Verifier) -> None: verifier.broker_source("http://localhost:8080", selector=True) .consumer_tags("main", "test") .provider_tags("main", "test") - .consumer_versions("1.2.3") + .consumer_versions('{"latest": true}') .build() ) From 93a5432f1651864fb4854c6dc64ce069c1100d95 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 18 Jul 2024 18:52:50 +1000 Subject: [PATCH 0431/1376] chore(tests): add v4 message provider compatibility suite Signed-off-by: JP-Ellis --- .../test_v3_message_producer.py | 28 +----- .../test_v4_message_provider.py | 88 +++++++++++++++++++ tests/v3/compatibility_suite/util/provider.py | 85 ++++++++++++++++++ 3 files changed, 175 insertions(+), 26 deletions(-) create mode 100644 tests/v3/compatibility_suite/test_v4_message_provider.py diff --git a/tests/v3/compatibility_suite/test_v3_message_producer.py b/tests/v3/compatibility_suite/test_v3_message_producer.py index ed6fc99a3..e427335d5 100644 --- a/tests/v3/compatibility_suite/test_v3_message_producer.py +++ b/tests/v3/compatibility_suite/test_v3_message_producer.py @@ -26,6 +26,7 @@ InteractionState, ) from tests.v3.compatibility_suite.util.provider import ( + a_pact_file_for_message_is_to_be_verified, a_provider_is_started_that_can_generate_the_message, a_provider_state_callback_is_configured, start_provider, @@ -199,6 +200,7 @@ def test_verifying_multiple_pact_files() -> None: a_provider_is_started_that_can_generate_the_message() a_provider_state_callback_is_configured() +a_pact_file_for_message_is_to_be_verified("V3") @given( @@ -240,32 +242,6 @@ def a_pact_file_for_is_to_be_verified_with_the_following( verifier.add_source(temp_dir / "pacts") -@given( - parsers.re( - r'a Pact file for "(?P[^"]+)":"(?P[^"]+)" is to be verified' - ) -) -def a_pact_file_for_is_to_be_verified( - verifier: Verifier, - temp_dir: Path, - name: str, - fixture: str, -) -> None: - pact = Pact("consumer", "provider") - pact.with_specification("V3") - interaction_definition = InteractionDefinition( - type="Async", - description=name, - body=fixture, - ) - interaction_definition.add_to_pact(pact, name) - (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) - pact.write_file(temp_dir / "pacts") - with (temp_dir / "pacts" / "consumer-provider.json").open() as f: - logger.debug("Pact file contents: %s", f.read()) - verifier.add_source(temp_dir / "pacts") - - @given( parsers.re( r'a Pact file for "(?P[^"]+)":"(?P[^"]+)"' diff --git a/tests/v3/compatibility_suite/test_v4_message_provider.py b/tests/v3/compatibility_suite/test_v4_message_provider.py new file mode 100644 index 000000000..d5b337fa6 --- /dev/null +++ b/tests/v3/compatibility_suite/test_v4_message_provider.py @@ -0,0 +1,88 @@ +""" +Basic HTTP provider feature test. +""" + +from __future__ import annotations + +import logging +import sys + +import pytest +from pytest_bdd import scenario + +from tests.v3.compatibility_suite.util.provider import ( + a_pact_file_for_message_is_to_be_verified, + a_pact_file_for_message_is_to_be_verified_with_comments, + a_provider_is_started_that_can_generate_the_message, + the_comment_will_have_been_printed_to_the_console, + the_name_of_the_test_will_be_displayed_as_the_original_test_name, + the_verification_is_run, + the_verification_results_will_contain_a_error, + the_verification_will_be_successful, + there_will_be_a_pending_error, +) + +logger = logging.getLogger(__name__) + + +################################################################################ +## Scenario +################################################################################ + + +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) +@scenario( + "definition/features/V4/message_provider.feature", + "Verifying a pending message interaction", +) +def test_verifying_a_pending_message_interaction() -> None: + """ + Verifying a pending message interaction. + """ + + +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) +@scenario( + "definition/features/V4/message_provider.feature", + "Verifying a message interaction with comments", +) +def test_verifying_a_message_interaction_with_comments() -> None: + """ + Verifying a message interaction with comments. + """ + + +################################################################################ +## Given +################################################################################ + + +a_provider_is_started_that_can_generate_the_message() +a_pact_file_for_message_is_to_be_verified("V4") +a_pact_file_for_message_is_to_be_verified_with_comments("V4") + + +################################################################################ +## When +################################################################################ + + +the_verification_is_run() + + +################################################################################ +## Then +################################################################################ + + +the_comment_will_have_been_printed_to_the_console() +the_name_of_the_test_will_be_displayed_as_the_original_test_name() +the_verification_results_will_contain_a_error() +the_verification_will_be_successful() +there_will_be_a_pending_error() diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 53ff8e40a..923de54b2 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -777,6 +777,45 @@ def _( verifier.add_source(temp_dir / "pacts") +def a_pact_file_for_message_is_to_be_verified( + version: str, + stacklevel: int = 1, +) -> None: + @given( + parsers.re( + r'a Pact file for "(?P[^"]+)":"(?P[^"]+)" is to be verified' + r"(?P(, but is marked pending)?)", + ), + converters={"pending": lambda x: x != ""}, + stacklevel=stacklevel + 1, + ) + def _( + verifier: Verifier, + temp_dir: Path, + name: str, + fixture: str, + pending: bool, # noqa: FBT001 + ) -> None: + defn = InteractionDefinition( + type="Async", + description=name, + body=fixture, + ) + defn.pending = pending + logger.debug("Adding message interaction: %s", defn) + + pact = Pact("consumer", "provider") + pact.with_specification(version) + defn.add_to_pact(pact, name) + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + + with (temp_dir / "pacts" / "consumer-provider.json").open() as f: + logger.debug("Pact file contents: %s", f.read()) + + verifier.add_source(temp_dir / "pacts") + + def a_pact_file_for_interaction_is_to_be_verified_with_comments( version: str, stacklevel: int = 1, @@ -832,6 +871,52 @@ def _( verifier.add_source(temp_dir / "pacts") +def a_pact_file_for_message_is_to_be_verified_with_comments( + version: str, + stacklevel: int = 1, +) -> None: + @given( + parsers.re( + r'a Pact file for "(?P[^"]+)":"(?P[^"]+)" is to be verified' + r" with the following comments:\n(?P.+)", + re.DOTALL, + ), + converters={"comments": parse_markdown_table}, + stacklevel=stacklevel + 1, + ) + def _( + verifier: Verifier, + temp_dir: Path, + name: str, + fixture: str, + comments: list[dict[str, str]], + ) -> None: + defn = InteractionDefinition( + type="Async", + description=name, + body=fixture, + ) + for comment in comments: + if comment["type"] == "text": + defn.text_comments.append(comment["comment"]) + elif comment["type"] == "testname": + defn.test_name = comment["comment"] + else: + defn.comments[comment["type"]] = comment["comment"] + logger.info("Updated interaction: %s", defn) + + pact = Pact("consumer", "provider") + pact.with_specification(version) + defn.add_to_pact(pact, name) + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + + with (temp_dir / "pacts" / "consumer-provider.json").open() as f: + logger.debug("Pact file contents: %s", f.read()) + + verifier.add_source(temp_dir / "pacts") + + def a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker( version: str, stacklevel: int = 1, From 2c652f0f4aff47b3e1884a39e1d749c76b7e433f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 18 Jul 2024 18:53:13 +1000 Subject: [PATCH 0432/1376] chore(tests): skip windows tests See mentioned issue for details. Signed-off-by: JP-Ellis --- .../test_v3_message_producer.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/v3/compatibility_suite/test_v3_message_producer.py b/tests/v3/compatibility_suite/test_v3_message_producer.py index e427335d5..b4eab3ee5 100644 --- a/tests/v3/compatibility_suite/test_v3_message_producer.py +++ b/tests/v3/compatibility_suite/test_v3_message_producer.py @@ -6,6 +6,7 @@ import logging import pickle import re +import sys from pathlib import Path from typing import TYPE_CHECKING, Generator @@ -48,6 +49,10 @@ logger = logging.getLogger(__name__) +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Incorrect message is generated by the provider", @@ -56,6 +61,10 @@ def test_incorrect_message_is_generated_by_the_provider() -> None: """Incorrect message is generated by the provider.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Message with JSON body (negative case)", @@ -64,6 +73,10 @@ def test_message_with_json_body_negative_case() -> None: """Message with JSON body (negative case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Message with JSON body (positive case)", @@ -72,6 +85,10 @@ def test_message_with_json_body_positive_case() -> None: """Message with JSON body (positive case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Message with XML body (negative case)", @@ -80,6 +97,10 @@ def test_message_with_xml_body_negative_case() -> None: """Message with XML body (negative case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Message with XML body (positive case)", @@ -88,6 +109,10 @@ def test_message_with_xml_body_positive_case() -> None: """Message with XML body (positive case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Message with binary body (negative case)", @@ -96,6 +121,10 @@ def test_message_with_binary_body_negative_case() -> None: """Message with binary body (negative case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Message with binary body (positive case)", @@ -104,6 +133,10 @@ def test_message_with_binary_body_positive_case() -> None: """Message with binary body (positive case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Message with plain text body (negative case)", @@ -112,6 +145,10 @@ def test_message_with_plain_text_body_negative_case() -> None: """Message with plain text body (negative case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Message with plain text body (positive case)", @@ -120,6 +157,10 @@ def test_message_with_plain_text_body_positive_case() -> None: """Message with plain text body (positive case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Supports matching rules for the message body (negative case)", @@ -128,6 +169,10 @@ def test_supports_matching_rules_for_the_message_body_negative_case() -> None: """Supports matching rules for the message body (negative case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Supports matching rules for the message body (positive case)", @@ -136,6 +181,10 @@ def test_supports_matching_rules_for_the_message_body_positive_case() -> None: """Supports matching rules for the message body (positive case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Supports matching rules for the message metadata (negative case)", @@ -144,6 +193,10 @@ def test_supports_matching_rules_for_the_message_metadata_negative_case() -> Non """Supports matching rules for the message metadata (negative case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Supports matching rules for the message metadata (positive case)", @@ -152,6 +205,10 @@ def test_supports_matching_rules_for_the_message_metadata_positive_case() -> Non """Supports matching rules for the message metadata (positive case).""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @pytest.mark.skip("Currently unable to implement") @scenario( "definition/features/V3/message_provider.feature", @@ -161,6 +218,10 @@ def test_supports_messages_with_body_formatted_for_the_kafka_schema_registry() - """Supports messages with body formatted for the Kafka schema registry.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Verifies the message metadata", @@ -169,6 +230,10 @@ def test_verifies_the_message_metadata() -> None: """Verifies the message metadata.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Verifying a simple message", @@ -177,6 +242,10 @@ def test_verifying_a_simple_message() -> None: """Verifying a simple message.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Verifying an interaction with a defined provider state", @@ -185,6 +254,10 @@ def test_verifying_an_interaction_with_a_defined_provider_state() -> None: """Verifying an interaction with a defined provider state.""" +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) @scenario( "definition/features/V3/message_provider.feature", "Verifying multiple Pact files", From f135dad9a22fe68707ed7595e1aaee8f5d39f2b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:14:13 +0000 Subject: [PATCH 0433/1376] chore(deps): update dependency devel-types/mypy to v1.11.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index feb854d58..29dd1d6dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ pact-verifier = "pact.cli.verify:main" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. devel-types = [ - "mypy ==1.10.1", + "mypy ==1.11.0", "types-cffi ~=1.0", "types-requests ~=2.0", ] From 5eb84893eec798d07f3783189abb7a605eda21fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Jul 2024 22:05:30 +0000 Subject: [PATCH 0434/1376] chore(deps): update softprops/action-gh-release digest to c062e08 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db0fb09cb..a4b779165 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -233,7 +233,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@fb2d03176f42a1f0dd433ca263f314051d3edd44 # v2 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2 with: files: wheels/* body_path: ${{ runner.temp }}/changelog From be64e5052726cadd6db6de7b1429fada8e67d034 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jul 2024 22:10:40 +0000 Subject: [PATCH 0435/1376] chore(deps): update ruff to v0.5.4 Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- src/pact/v3/interaction/_base.py | 2 +- src/pact/v3/verifier.py | 4 ++-- tests/v3/compatibility_suite/util/provider.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e5e3087c..47764610a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.2 + rev: v0.5.4 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 29dd1d6dc..65e880b05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.5.2"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.5.4"] ################################################################################ ## Hatch Build Configuration diff --git a/src/pact/v3/interaction/_base.py b/src/pact/v3/interaction/_base.py index c920d1b23..d597da59f 100644 --- a/src/pact/v3/interaction/_base.py +++ b/src/pact/v3/interaction/_base.py @@ -375,7 +375,7 @@ def with_metadata( ) return self - def with_multipart_file( # noqa: PLR0913 + def with_multipart_file( self, part_name: str, path: Path | None, diff --git a/src/pact/v3/verifier.py b/src/pact/v3/verifier.py index ea6f95ed5..a51e3c910 100644 --- a/src/pact/v3/verifier.py +++ b/src/pact/v3/verifier.py @@ -693,7 +693,7 @@ def broker_source( selector: Literal[True], ) -> BrokerSelectorBuilder: ... - def broker_source( # noqa: PLR0913 + def broker_source( self, url: str | URL, *, @@ -804,7 +804,7 @@ class BrokerSelectorBuilder: This class encapsulates the logic for selecting Pacts from a Pact broker. """ - def __init__( # noqa: PLR0913 + def __init__( self, verifier: Verifier, url: str, diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 923de54b2..c70a27297 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -354,7 +354,7 @@ class PactBroker: Interface to the Pact Broker. """ - def __init__( # noqa: PLR0913 + def __init__( self, broker_url: URL, *, From 6f60bc3317a3f6d3e16ce5d5ad25465ee05e7633 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 03:26:46 +0000 Subject: [PATCH 0436/1376] chore(deps): update dependency devel-test/pytest to ~=8.0 Signed-off-by: JP-Ellis --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 65e880b05..906eab751 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,9 +81,7 @@ devel-test = [ "flask[async] ~=3.0", "httpx ~=0.0", "mock ~=5.0", - # TODO: Upgrade to PyTest 8.1 - # Pending on https://github.com/pytest-dev/pytest-bdd/issues/673 - "pytest ~=8.2.2", + "pytest ~=8.0", "pytest-asyncio ~=0.0", "pytest-bdd ~=7.0", "pytest-cov ~=5.0", From 8a6b454f3704bd4d81f3c985bce5a60b42e00b8a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 22 Jul 2024 13:41:04 +1000 Subject: [PATCH 0437/1376] chore(ci): disable windows arm wheels Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4b779165..866a43842 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,9 +138,11 @@ jobs: - os: macos-14 archs: arm64 build: "" - - os: windows-2019 - archs: ARM64 - build: "" + # TODO: Re-enable once the issues with Windows ARM64 are resolved.exclude: + # See: pypa/cibuildwheel#1942 + # - os: windows-2019 + # archs: ARM64 + # build: "" steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 From 6276f91bc1e24a046c0c7fb4d2ff3b8998567f0a Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Mon, 22 Jul 2024 05:12:07 +0000 Subject: [PATCH 0438/1376] chore: update changelog v2.2.1 --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d265bf15b..02166afb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## v2.2.1 (2024-07-22) + +### Feat + +- **ffi**: upgrade ffi to 0.4.22 +- **v3**: add async message provider +- **v3**: implement message verification +- **v3**: remove deprecated messages iterator +- **v3**: improve exception types +- **v3**: add enum type aliases +- **ffi**: upgrade ffi 0.4.21 + +### Fix + +- **ffi**: use `with_binary_body` + +### Refactor + +- **tests**: move InteractionDefinition in own module +- **tests**: make `_add_body` a method of Body +- **v3**: new interaction iterators + ## v2.2.0 (2024-04-11) ### Feat From e9b8bf9e2a18f2771618b7f38ab92c4e093e8c75 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:27:41 +0000 Subject: [PATCH 0439/1376] chore(deps): update docker/setup-qemu-action digest to 49b3bc8 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 866a43842..acb4dd1d0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -162,7 +162,7 @@ jobs: - name: Set up QEMU if: startsWith(matrix.os, 'ubuntu-') - uses: docker/setup-qemu-action@5927c834f5b4fdf503fca6f4c7eccda82949e1ee # v3 + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3 with: platforms: arm64 From d4be080a0c10455334ae8874cae81641315a52ac Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 22 Jul 2024 18:51:10 +1000 Subject: [PATCH 0440/1376] chore(ci): use pypi trusted publishing Removing the password, as it should not longer be needed now that trusted publishing is enabled. Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index acb4dd1d0..9f2bae35f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -185,7 +185,9 @@ jobs: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') runs-on: ubuntu-20.04 - environment: pypi + environment: + name: pypi + url: https://pypi.org/p/pact-python needs: - build-sdist @@ -247,7 +249,6 @@ jobs: uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # v1.9.0 with: skip-existing: true - password: ${{ secrets.PYPI_TOKEN }} packages-dir: wheels - name: Create PR for changelog update From d31ca134b081605a30b1d91afb5661388d3fa7e7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 23:17:11 +0000 Subject: [PATCH 0441/1376] chore(deps): update pactfoundation/pact-broker:latest docker digest to 18f0b8b --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 69eb579a0..f9d0f13c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:fc44f0a6e731c7de78bf44923b9ab41e5d3351388bfcf2f648e72844041cf74d + image: pactfoundation/pact-broker:latest@sha256:18f0b8b4adae7f7c0c14122a47dbb2e1d095f0cc5aeb4c4fa2d40f641410dc8a ports: - "9292:9292" env: @@ -158,7 +158,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:fc44f0a6e731c7de78bf44923b9ab41e5d3351388bfcf2f648e72844041cf74d + image: pactfoundation/pact-broker:latest@sha256:18f0b8b4adae7f7c0c14122a47dbb2e1d095f0cc5aeb4c4fa2d40f641410dc8a ports: - "9292:9292" env: From 00c555fb34ba60eca96a49c7a5559b217f639334 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 25 Jul 2024 14:45:31 +1000 Subject: [PATCH 0442/1376] docs(blog): don't use footnote numbers While footnotes are numbered when rendered, it is best to avoid using numbers in the Markdown file itself as the numbers are less descriptive, and make it difficult to refactor (or even repeat the use of a footer). Signed-off-by: JP-Ellis --- ...5-02 integrating rust ffi with pact python.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md b/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md index e48cd0f5e..5edeb4213 100644 --- a/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md +++ b/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md @@ -17,14 +17,14 @@ In this blog post, I will delve into how this is all achieved. From explaining h ## Briding Python and Binary Libraries -Python, known for its dynamic typing and automated memory management, is fundamentally an interpreted language. Despite not having innate capabilities to directly interact with binary libraries, most Python interpreters bridge this gap efficiently. For instance, CPython—the principal interpreter—enables the creation of binary extensions[^1] and similarly, PyPy—a widely-used alternative—offers comparable functionalities[^2]. +Python, known for its dynamic typing and automated memory management, is fundamentally an interpreted language. Despite not having innate capabilities to directly interact with binary libraries, most Python interpreters bridge this gap efficiently. For instance, CPython—the principal interpreter—enables the creation of binary extensions[^binary_extension] and similarly, PyPy—a widely-used alternative—offers comparable functionalities[^pypy]. -[^1]: You can find extensive documentation on building extensions for CPython [here](https://docs.python.org/3/extending/extending.html). -[^2]: PyPy extension-building documentation is available [here](https://doc.pypy.org/en/latest/extending.html). +[^binary_extension]: You can find extensive documentation on building extensions for CPython [here](https://docs.python.org/3/extending/extending.html). +[^pypy]: PyPy extension-building documentation is available [here](https://doc.pypy.org/en/latest/extending.html). -However, each interpreter has a distinct API tailored for crafting these binary extensions, which unfortunately leads to a lack of universal solutions across different environments. Furthermore, interpreters like [Jython](https://jython.org) and [Pyodide](https://pyodide.org/en/stable/), which are based on Java and WebAssembly respectively, present unique challenges that often preclude the straightforward use of such extensions due to their distinct runtime architectures.[^3] +However, each interpreter has a distinct API tailored for crafting these binary extensions, which unfortunately leads to a lack of universal solutions across different environments. Furthermore, interpreters like [Jython](https://jython.org) and [Pyodide](https://pyodide.org/en/stable/), which are based on Java and WebAssembly respectively, present unique challenges that often preclude the straightforward use of such extensions due to their distinct runtime architectures.[^pyodide] -[^3]: It would appear that Pyodide can support C extensions as explained [here](https://pyodide.org/en/stable/development/new-packages.html), though by and large Pyodide appears to be intended for pure Python packages. +[^pyodide]: It would appear that Pyodide can support C extensions as explained [here](https://pyodide.org/en/stable/development/new-packages.html), though by and large Pyodide appears to be intended for pure Python packages. While it is possible for the extension to contain all the logic, our specific requirement is merely to provide a bridge between Python and the Rust core library. This is the niche that [Python C Foreign Function Interface (CFFI)](https://cffi.readthedocs.io/en/stable/) fills. By parsing a C header file, CFFI automates the generation of extension code needed for Python to interface with the binary library. Consequently, this library can be imported into Python as if it were any standard module—streamlining development and potentially improving performance by leveraging optimized native code. @@ -175,10 +175,10 @@ class OwnedString(str): lib.pactffi_string_delete(self._ptr) ``` -The `__del__` method is called[^4] when the object is about to be deallocated[^5], allowing us to free the memory associated with the string. This ensures that memory is managed correctly and prevents potential memory leaks. +The `__del__` method is called[^del_exceptions] when the object is about to be deallocated[^del_no_guarantee], allowing us to free the memory associated with the string. This ensures that memory is managed correctly and prevents potential memory leaks. -[^4]: There are some unique circumstances where `__del__` may not be called, such as when the Python interpreter is shutting down. -[^5]: Python does not provide guarantees on when `__del__` will be called, so it is not recommended to rely on it for critical cleanup tasks. Instead, the `__enter__` and `__exit__` methods should be used to guarantee timely cleanup. +[^del_exceptions]: There are some unique circumstances where `__del__` may not be called, such as when the Python interpreter is shutting down. +[^del_no_guarantee]: Python does not provide guarantees on when `__del__` will be called, so it is not recommended to rely on it for critical cleanup tasks. Instead, the `__enter__` and `__exit__` methods should be used to guarantee timely cleanup. ## Conclusion From d67c600b4a3920e19adb6b1700c3ce0d03133a5e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 26 Jul 2024 11:49:59 +1000 Subject: [PATCH 0443/1376] docs(blog): add async message blog post Signed-off-by: JP-Ellis --- .../07-26 asynchronous message support.md | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 docs/blog/posts/2024/07-26 asynchronous message support.md diff --git a/docs/blog/posts/2024/07-26 asynchronous message support.md b/docs/blog/posts/2024/07-26 asynchronous message support.md new file mode 100644 index 000000000..f3ad93665 --- /dev/null +++ b/docs/blog/posts/2024/07-26 asynchronous message support.md @@ -0,0 +1,185 @@ +--- +authors: + - JP-Ellis +date: + created: 2024-07-26 +--- + +# Asynchronous Message Support + +We are excited to announce that support for verifying asynchronous message interactions has been added in the recent [release of Pact Python version 2.2.1](https://github.com/pact-foundation/pact-python/releases/tag/v2.2.1). To explore this feature, use the [`pact.v3`][pact.v3] module. A huge shoutout goes to [Val Kolovos](https://github.com/valkolovos) who contributed this feature across two very large PRs ([#714](https://github.com/pact-foundation/pact-python/pull/714) and [#725](https://github.com/pact-foundation/pact-python/pull/725)). This represents a significant step forward in the capabilities of Pact Python and on the road to full support for the Pact specification. + +Asynchronous messages play a crucial role in building resilient and scalable systems. They allow services to communicate with each other without blocking, which can be particularly useful when the sender and receiver are not always available at the same time. However, verifying these interactions is challenging due to the wide variety of messaging systems and protocols. + +Pact simplifies this process by focusing on the content of the messages rather than their transport mechanisms. This approach allows defining expected message exchanges and verifying their adherence independently of messaging systems and protocols. For a more comprehensive view of non-HTTP contract testing, have a look over at [docs.pact.io](https://docs.pact.io/getting_started/how_pact_works#non-http-testing-message-pact). The Pact specification provides a way to verify these interactions, but until now, Pact Python support for this feature was incomplete at best. + + + +We are thrilled about this new feature and eager to see how our community will leverage it in their projects! Please try out asynchronous message support while it's still in preview mode, as your feedback is invaluable in shaping its final release. + +Your feedback will help us refine and prefect this feature. You can provide feedback through any of these channels: + +- Report issues on our GitHub page: [Pact Python Issues](https://github.com/pact-foundation/pact-python/issues). +- Join discussions on GitHub: [Pact Python Discussions](https://github.com/pact-foundation/pact-python/discussions). +- Connect with us on Slack: [Pact Foundation Slack](https://slack.pact.io/). + +Thank you for your continued support! + +## Consumer Example + +Pact is a consumer-driven contract testing tool, and so the consumer defines the expectations of the message. Within the context of asynchronous messages, the consumer is the service that processes the message and might be referred to as the _subscriber_. + +Consider an example where a consumer service is responsible for asynchronously processing requests to delete a user from the database and delete associated files. The Python client might listen for messages from AWS SQS and process them using a function like this: + +```python +from typing import Any + +import boto3 + +QUEUE_URL = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue" + + +def delete_user(user_id: str) -> bool: + # Delete user from database + # Delete associated files + return True + + +def process_message(message: dict[str, Any]) -> bool: + if message.get("action") == "delete_user": + user_id = message["user_id"] + return delete_user(user_id) + return False + + +def main(): + sqs = boto3.client("sqs") + + response = sqs.receive_message(QueueUrl=queue_url) + for message in response.get("Messages", []): + if process_message(message): + sqs.delete_message( + QueueUrl=queue_url, + ReceiptHandle=message["ReceiptHandle"], + ) +``` + +In this example, the `process_message` function processes messages from an SQS queue and calls the `delete_user` function to delete the user from the database and associated files. The `main` function listens for messages from the SQS queue and processes them using the `process_message` function. + +Here’s an example of a Pact test for this consumer: + +```python +import json + +from pact.v3 import Pact + +from my_consumer import process_message + +def handler(body: str | bytes | None, metadata: dict[str, Any]) -> None: + message = json.loads(body) + process_message(message) + +pact = Pact( + consumer="deleteUserService", + provider="someProvider", +).with_specification("V3") # (1) + +( + pact + .upon_receiving("a request to delete a user", "Async") + .with_body( + json.dumps({ + "action": "delete_user", + "user_id": "123", + }) + ) # (2) +) + +pact.verify(handler, "Async") +``` + +1. Support for asynchronous messages starts in version 3 of the Pact specification. +2. No `will_respond_with` method exists for asynchronous messages since there’s no response expected. + +This example highlights how the verification of asynchronous messages differs from HTTP interaction. As the transport layer is abstracted away, a `handler` function is required to parse the raw message string or bytes, and pass it to the underlying function that processes the message. + +The `handler` would also typically be responsible for mocking the underlying systems that the consumer interacts with, such as the database or file system. This allows the consumer to be tested in isolation, without relying on external services. Furthermore, the mocked systems can then be inspected to verify that the consumer has performed the expected actions. + +## Provider Example + +For context of asynchronous messages, the provider is the service that sends the message and might be referred to as the _publisher_ or _producer_. Since the contract is defined by the consumer, the Pact provider test simply has to verify that the messages it sends meet the expectations of the consumer. + +As the underlying protocol is abstracted away, Pact uses a local HTTP server to receive the messages that the provider sends. The provider test for the above consumer might look something like this: + +```python +from pact.v3 import Verifier + +class Provider: + """ + A simple HTTP provider that sends messages to the consumer. + + This would typically use the same underlying functions that would generate messages, except that instead of being sent into the message queue, they are sent to the consumer's HTTP server. + """ + +provider = Provider() + +( + Verifier() + .set_info("someProvider", url=provider.url) # (1) + .set_source("/path/to/pacts") + .set_state(provider.state_url) # (2) + .add_transport( # (3) + protocol="message", + path="/_pact/message", + ) + ) +``` + +1. The provider URL is required, but is only used if the Pact being verified contains both HTTP and message interactions. It is not used for message interactions, and should the Pact not contain any HTTP interactions, the endpoint need not be active. +2. The provider state URL is required to ensure the provider is in the correct state. If the provider is entirely stateless, this can be omitted. +3. This path is used by Pact to ensure that the provider is in the correct state before sending the message. + +Those familiar with HTTP interactions will notice that the process is very similar, with the key difference of the additional `add_transport` method. This configures a simple HTTP endpoint which Pact can use to prompt the provider to send a specific message. The following sequence diagram illustrates the flow of the provider test: + +```mermaid +sequenceDiagram + participant Pact as Pact + participant T as Test + participant P as Provider + + Pact->>Pact: Read source(s) + Pact->>T: Set provider state(s) + Pact->>T: Trigger message generation + T->>+P: Call provider + P->>T: Generate message + T->>Pact: Forward message
over HTTP + Pact->>Pact: Verify message +``` + +At present, it is the responsibility of the end user to set up the provider endpoint middle-man to access the message triggers; however, future versions of Pact Python will abstract this away thereby reducing the test boilerplate required. The payloads are: + +1. Trigger from Pact to the provider to generate a message: + + ```http + POST /_pact/message HTTP/1.1 + Content-Type: application/json + + { + "description": "a request to delete a user", + } + ``` + +2. Response expected from the provider: + + ```http + HTTP/1.1 200 OK + Content-Type: application/json + Pact-Message-Metadata: + + { + "action": "delete_user", + "user_id": "123", + } + ``` + + Some queueuing systems allow for metadata to be attached to messages and may be required as part of the Pact. If that is the case, the metadata generated by the provider can be passed through the `Pact-Message-Metadata` header as a base-64 encoded string of the underlying JSON object. From 7612ffdb21c07bd2aa938b5d91810c74f40e0bf0 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 29 Jul 2024 10:22:02 +1000 Subject: [PATCH 0444/1376] chore: fix typo in previous blog post Signed-off-by: JP-Ellis --- .../posts/2024/05-02 integrating rust ffi with pact python.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md b/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md index 5edeb4213..648361548 100644 --- a/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md +++ b/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md @@ -15,7 +15,7 @@ In this blog post, I will delve into how this is all achieved. From explaining h -## Briding Python and Binary Libraries +## Bridging Python and Binary Libraries Python, known for its dynamic typing and automated memory management, is fundamentally an interpreted language. Despite not having innate capabilities to directly interact with binary libraries, most Python interpreters bridge this gap efficiently. For instance, CPython—the principal interpreter—enables the creation of binary extensions[^binary_extension] and similarly, PyPy—a widely-used alternative—offers comparable functionalities[^pypy]. From 47143aa962faee194d9e76d5acfe51ba325b7b1e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 29 Jul 2024 12:13:47 +1000 Subject: [PATCH 0445/1376] chore(ci): update docs on push to master The frequency of updating the docs was previously reduced to avoid noise in the Pact Python Slack channel; however, it means that blog posts, or minor corrections to any docstring do not get updated. I am reverting the previous change and will investigate the possibility of publishing updates to Slack, instead of Slack subscribing to all deployments. Signed-off-by: JP-Ellis --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3a93be411..d7416eb12 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -48,7 +48,7 @@ jobs: publish: name: Publish docs - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') + if: github.ref == 'refs/heads/master' needs: build runs-on: ubuntu-latest From 72ff0d5d4275bd733f65edb03d778af15945f9b5 Mon Sep 17 00:00:00 2001 From: valkolovos Date: Fri, 2 Aug 2024 14:22:02 -0600 Subject: [PATCH 0446/1376] adding http_matcher.feature v3 compatibility test --- .../test_v3_http_matching.py | 222 ++++++++++++++++++ .../util/interaction_definition.py | 2 +- 2 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 tests/v3/compatibility_suite/test_v3_http_matching.py diff --git a/tests/v3/compatibility_suite/test_v3_http_matching.py b/tests/v3/compatibility_suite/test_v3_http_matching.py new file mode 100644 index 000000000..c7116bd8a --- /dev/null +++ b/tests/v3/compatibility_suite/test_v3_http_matching.py @@ -0,0 +1,222 @@ +"""Matching HTTP parts (request or response) feature tests.""" + +import pickle +import re +import sys +from pathlib import Path +from typing import Generator + +import pytest +from pytest_bdd import ( + given, + parsers, + scenario, + then, + when, +) +from yarl import URL + +from pact.v3 import Pact +from pact.v3.verifier import Verifier +from tests.v3.compatibility_suite.util.interaction_definition import ( + InteractionDefinition, +) +from tests.v3.compatibility_suite.util.provider import start_provider + +################################################################################ +## Scenarios +################################################################################ + + +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) +@scenario( + "definition/features/V3/http_matching.feature", + "Comparing accept headers where the actual has additional parameters", +) +def test_comparing_accept_headers_where_the_actual_has_additional_parameters() -> None: + """Comparing accept headers where the actual has additional parameters.""" + + +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) +@scenario( + "definition/features/V3/http_matching.feature", + "Comparing accept headers where the actual has is missing a value", +) +def test_comparing_accept_headers_where_the_actual_has_is_missing_a_value() -> None: + """Comparing accept headers where the actual has is missing a value.""" + + +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) +@scenario( + "definition/features/V3/http_matching.feature", + "Comparing content type headers where the actual has a charset", +) +def test_comparing_content_type_headers_where_the_actual_has_a_charset() -> None: + """Comparing content type headers where the actual has a charset.""" + + +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) +@scenario( + "definition/features/V3/http_matching.feature", + "Comparing content type headers where the actual has a different charset", +) +def test_comparing_content_type_headers_where_the_actual_has_a_different_charset() -> ( + None +): + """Comparing content type headers where the actual has a different charset.""" + + +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) +@scenario( + "definition/features/V3/http_matching.feature", + "Comparing content type headers where the actual is missing a charset", +) +def test_comparing_content_type_headers_where_the_actual_is_missing_a_charset() -> None: + """Comparing content type headers where the actual is missing a charset.""" + + +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) +@scenario( + "definition/features/V3/http_matching.feature", + "Comparing content type headers where they have the same charset", +) +def test_comparing_content_type_headers_where_they_have_the_same_charset() -> None: + """Comparing content type headers where they have the same charset.""" + + +@pytest.mark.skipif( + sys.platform.startswith("win"), + reason="See pact-foundation/pact-python#639", +) +@scenario( + "definition/features/V3/http_matching.feature", + "Comparing content type headers which are equal", +) +def test_comparing_content_type_headers_which_are_equal() -> None: + """Comparing content type headers which are equal.""" + + +################################################################################ +## Given +################################################################################ + + +@given( + parsers.re( + r'a request is received with an? "(?P[^"]+)" header of "(?P[^"]+)"' + ) +) +def a_request_is_received_with_header(name: str, value: str, temp_dir: Path) -> None: + """A request is received with a "content-type" header of "application/json".""" + interaction_definition = InteractionDefinition(method="GET", path="/", type="HTTP") + interaction_definition.response_headers.update({name: value}) + with (temp_dir / "interactions.pkl").open("wb") as pkl_file: + pickle.dump([interaction_definition], pkl_file) + + +@given( + parsers.re( + r'an expected request with an? "(?P[^"]+)" header of "(?P[^"]+)"' + ), +) +def an_expected_request_with_header(name: str, value: str, temp_dir: Path) -> None: + """An expected request with a "content-type" header of "application/json".""" + pact = Pact("consumer", "provider") + pact.with_specification("V3") + interaction_definition = InteractionDefinition(method="GET", path="/", type="HTTP") + interaction_definition.response_headers.update({name: value}) + interaction_definition.add_to_pact(pact, name) + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(temp_dir / "pacts") + + +################################################################################ +## When +################################################################################ + + +@when("the request is compared to the expected one", target_fixture="provider_url") +def the_request_is_compared_to_the_expected_one( + temp_dir: Path, +) -> Generator[URL, None, None]: + """The request is compared to the expected one.""" + yield from start_provider(temp_dir) + + +################################################################################ +## Then +################################################################################ + + +@then( + parsers.re("the comparison should(?P( NOT)?) be OK"), + converters={"negated": lambda x: x == " NOT"}, + target_fixture="verifier_result", +) +def the_comparison_should_not_be_ok( + provider_url: URL, + verifier: Verifier, + temp_dir: Path, + negated: bool, # noqa: FBT001 +) -> Verifier: + """The comparison should NOT be OK.""" + verifier.set_info("provider", url=provider_url) + verifier.add_transport( + protocol="http", + port=provider_url.port, + path="/", + ) + verifier.add_source(temp_dir / "pacts") + if negated: + with pytest.raises(RuntimeError): + verifier.verify() + else: + verifier.verify() + return verifier + + +@then( + parsers.parse( + 'the mismatches will contain a mismatch with error "{mismatch_key}" ' + "-> \"Expected header '{header_name}' to have value '{expected_value}' " + "but was '{actual_value}'\"" + ) +) +def the_mismatches_will_contain_a_mismatch_with_error( + verifier_result: Verifier, + mismatch_key: str, + header_name: str, + expected_value: str, + actual_value: str, +) -> None: + """Mismatches will contain a mismatch with error.""" + expected_value_matcher = re.compile(expected_value) + actual_value_matcher = re.compile(actual_value) + expected_error_matcher = re.compile( + rf"Mismatch with header \'{mismatch_key}\': Expected header \'{header_name}\' " + rf"to have value \'{expected_value}\' but was \'{actual_value}\'" + ) + mismatch = verifier_result.results["errors"][0]["mismatch"]["mismatches"][0] + assert mismatch["key"] == mismatch_key + assert mismatch["type"] == "HeaderMismatch" + assert expected_value_matcher.match(mismatch["expected"]) is not None + assert actual_value_matcher.match(mismatch["actual"]) is not None + assert expected_error_matcher.match(mismatch["mismatch"]) is not None diff --git a/tests/v3/compatibility_suite/util/interaction_definition.py b/tests/v3/compatibility_suite/util/interaction_definition.py index 19fd5ce05..8e985d180 100644 --- a/tests/v3/compatibility_suite/util/interaction_definition.py +++ b/tests/v3/compatibility_suite/util/interaction_definition.py @@ -576,7 +576,7 @@ def add_to_pact( # noqa: C901, PLR0912, PLR0915 interaction, HttpInteraction ), "Response headers require an HTTP interaction" logger.info("with_headers(%r)", self.response_headers) - interaction.with_headers(self.response_headers.items()) + interaction.with_headers(self.response_headers.items(), "Response") if self.response_body: assert isinstance( From 48f43583129cc13f6a9d1114ab381e1d13247aee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:12:51 +0000 Subject: [PATCH 0447/1376] chore(deps): update pactfoundation/pact-broker:latest docker digest to b521072 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9d0f13c8..fd7528046 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:18f0b8b4adae7f7c0c14122a47dbb2e1d095f0cc5aeb4c4fa2d40f641410dc8a + image: pactfoundation/pact-broker:latest@sha256:b5210724579fcb70980821ebe4ccebc847067a67ac35024e84b6b0f933be1091 ports: - "9292:9292" env: @@ -158,7 +158,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:18f0b8b4adae7f7c0c14122a47dbb2e1d095f0cc5aeb4c4fa2d40f641410dc8a + image: pactfoundation/pact-broker:latest@sha256:b5210724579fcb70980821ebe4ccebc847067a67ac35024e84b6b0f933be1091 ports: - "9292:9292" env: From 4a33eca1aa39556b4f5d2b797c9fefe917782899 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 Aug 2024 15:23:12 +0000 Subject: [PATCH 0448/1376] chore(deps): update pre-commit hook commitizen-tools/commitizen to v3.29.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 47764610a..01b64d87a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,7 +53,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.28.0 + rev: v3.29.0 hooks: - id: commitizen stages: [commit-msg] From e074d07a3fdf7f2f04fd234e886e61ba289e1d94 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:55:59 +0000 Subject: [PATCH 0449/1376] chore(deps): update dependency ruff to v0.5.7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 906eab751..ff1e7b69f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.5.4"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.5.7"] ################################################################################ ## Hatch Build Configuration From 1c72020b8954123cab9a8cb16848515a6512bd73 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 4 Aug 2024 03:17:00 +0000 Subject: [PATCH 0450/1376] chore(deps): update pypa/cibuildwheel action to v2.20.0 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f2bae35f..31b0cbc38 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,7 +104,7 @@ jobs: echo "MACOSX_DEPLOYMENT_TARGET=10.12" >> "$GITHUB_ENV" - name: Create wheels - uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 + uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} @@ -167,7 +167,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@7e5a838a63ac8128d71ab2dfd99e4634dd1bca09 # v2.19.2 + uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} From f55fbfb0592b1c7116988c25487a0a70097b627c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:56:03 +0000 Subject: [PATCH 0451/1376] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.5.7 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01b64d87a..c372389bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.4 + rev: v0.5.7 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the From baaeba5d1cb882f67d98cea611dc0a4b745abc90 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 08:22:16 +0000 Subject: [PATCH 0452/1376] chore(deps): update dependency mypy to v1.11.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ff1e7b69f..d5b0b634a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ pact-verifier = "pact.cli.verify:main" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. devel-types = [ - "mypy ==1.11.0", + "mypy ==1.11.1", "types-cffi ~=1.0", "types-requests ~=2.0", ] From 14f84a92c96c531c483b9a178dcbcd67a54529dc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:38:51 +0000 Subject: [PATCH 0453/1376] chore(deps): update actions/upload-artifact digest to 834a144 --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31b0cbc38..6c064fb9c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4 with: name: wheels-sdist path: ./dist/*.tar.* @@ -110,7 +110,7 @@ jobs: CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} - name: Upload wheels - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl @@ -173,7 +173,7 @@ jobs: CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} - name: Upload wheels - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }}-${{ matrix.build }} path: ./wheelhouse/*.whl From 46ff3b23b34917a870606d4327b657f56502880f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2024 07:59:09 +0000 Subject: [PATCH 0454/1376] chore(deps): update ubuntu:24.04 docker digest to 8a37d68 --- Dockerfile.ubuntu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 92667d1b7..ba80a92f0 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ubuntu:24.04@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30 +FROM ubuntu:24.04@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab63ee ENV DEBIAN_FRONTEND=noninteractive ARG PYTHON_VERSION 3.9 From bb51f090835b55749a4db20b72d9af98d5b957e4 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 21 Aug 2024 15:11:51 +1000 Subject: [PATCH 0455/1376] chore: regroup ruff in renovate There appears to have been a change in the way renovate names packages. Signed-off-by: JP-Ellis --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index bde45ac7f..c821376d9 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -12,7 +12,7 @@ "packageRules": [ { "groupName": "Ruff", - "matchPackageNames": ["astral-sh/ruff-pre-commit", "devel/ruff"] + "matchPackageNames": ["astral-sh/ruff-pre-commit", "ruff"] } ] } From b238af1466e1b45a95b0001188a3e7e6985a69e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 05:27:04 +0000 Subject: [PATCH 0456/1376] chore(deps): update ruff to v0.6.1 Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- examples/tests/test_00_consumer.py | 2 +- examples/tests/test_01_provider_flask.py | 2 +- examples/tests/test_02_message_consumer.py | 2 +- examples/tests/test_03_message_provider.py | 1 - examples/tests/v3/provider_server.py | 3 +- examples/tests/v3/test_01_message_consumer.py | 4 +- pyproject.toml | 2 +- tests/v3/compatibility_suite/conftest.py | 2 +- .../compatibility_suite/test_v1_provider.py | 8 ++-- .../util/interaction_definition.py | 4 +- tests/v3/test_async_interaction.py | 2 +- tests/v3/test_http_interaction.py | 46 +++++++++---------- tests/v3/test_pact.py | 2 +- tests/v3/test_sync_interaction.py | 2 +- tests/v3/test_verifier.py | 2 +- 16 files changed, 43 insertions(+), 43 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c372389bb..09a7df3f7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: stages: [pre-push] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.7 + rev: v0.6.1 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/examples/tests/test_00_consumer.py b/examples/tests/test_00_consumer.py index a49f9664a..907678508 100644 --- a/examples/tests/test_00_consumer.py +++ b/examples/tests/test_00_consumer.py @@ -36,7 +36,7 @@ MOCK_URL = URL("http://localhost:8080") -@pytest.fixture() +@pytest.fixture def user_consumer() -> UserConsumer: """ Returns an instance of the UserConsumer class. diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py index b7082dabb..58cb02458 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/tests/test_01_provider_flask.py @@ -30,10 +30,10 @@ from unittest.mock import MagicMock import pytest -from flask import request from yarl import URL from examples.src.flask import app +from flask import request from pact import Verifier PROVIDER_URL = URL("http://localhost:8080") diff --git a/examples/tests/test_02_message_consumer.py b/examples/tests/test_02_message_consumer.py index 0603b4b96..f5689b839 100644 --- a/examples/tests/test_02_message_consumer.py +++ b/examples/tests/test_02_message_consumer.py @@ -85,7 +85,7 @@ def pact(broker: URL, pact_dir: Path) -> Generator[MessagePact, Any, None]: yield pact -@pytest.fixture() +@pytest.fixture def handler() -> Handler: """ Fixture for the Handler. diff --git a/examples/tests/test_03_message_provider.py b/examples/tests/test_03_message_provider.py index 581bd2391..9a9ff3f7d 100644 --- a/examples/tests/test_03_message_provider.py +++ b/examples/tests/test_03_message_provider.py @@ -29,7 +29,6 @@ from typing import TYPE_CHECKING, Dict from flask import Flask - from pact import MessageProvider if TYPE_CHECKING: diff --git a/examples/tests/v3/provider_server.py b/examples/tests/v3/provider_server.py index 0bc9aad22..2b427524b 100644 --- a/examples/tests/v3/provider_server.py +++ b/examples/tests/v3/provider_server.py @@ -21,9 +21,10 @@ sys.path.append(str(Path(__file__).parent.parent.parent.parent)) -import flask from yarl import URL +import flask + logger = logging.getLogger(__name__) diff --git a/examples/tests/v3/test_01_message_consumer.py b/examples/tests/v3/test_01_message_consumer.py index 76f58c71a..58dc405c1 100644 --- a/examples/tests/v3/test_01_message_consumer.py +++ b/examples/tests/v3/test_01_message_consumer.py @@ -72,7 +72,7 @@ def pact() -> Generator[Pact, None, None]: pact.write_file(pact_dir, overwrite=True) -@pytest.fixture() +@pytest.fixture def handler() -> Handler: """ Fixture for the Handler. @@ -87,7 +87,7 @@ def handler() -> Handler: return handler -@pytest.fixture() +@pytest.fixture def verifier( handler: Handler, ) -> Generator[Callable[[str | bytes | None, Dict[str, Any]], None], Any, None]: diff --git a/pyproject.toml b/pyproject.toml index d5b0b634a..ece85cd42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ devel-test = [ "pytest-cov ~=5.0", "testcontainers ~=3.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.5.7"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.6.1"] ################################################################################ ## Hatch Build Configuration diff --git a/tests/v3/compatibility_suite/conftest.py b/tests/v3/compatibility_suite/conftest.py index e5446e1e5..be72eebb6 100644 --- a/tests/v3/compatibility_suite/conftest.py +++ b/tests/v3/compatibility_suite/conftest.py @@ -35,7 +35,7 @@ def _submodule_init() -> None: subprocess.check_call([git_exec, "submodule", "init"]) # noqa: S603 -@pytest.fixture() +@pytest.fixture def verifier() -> Verifier: """Return a new Verifier.""" return Verifier() diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py index 60654a629..b916d21e3 100644 --- a/tests/v3/compatibility_suite/test_v1_provider.py +++ b/tests/v3/compatibility_suite/test_v1_provider.py @@ -86,7 +86,7 @@ def test_incorrect_request_is_made_to_provider() -> None: sys.platform.startswith("win"), reason="See pact-foundation/pact-python#639", ) -@pytest.mark.container() +@pytest.mark.container @scenario( "definition/features/V1/http_provider.feature", "Verifying a simple HTTP request via a Pact broker", @@ -100,7 +100,7 @@ def test_verifying_a_simple_http_request_via_a_pact_broker() -> None: sys.platform.startswith("win"), reason="See pact-foundation/pact-python#639", ) -@pytest.mark.container() +@pytest.mark.container @scenario( "definition/features/V1/http_provider.feature", "Verifying a simple HTTP request via a Pact broker with publishing results enabled", @@ -114,7 +114,7 @@ def test_verifying_a_simple_http_request_via_a_pact_broker_with_publishing() -> sys.platform.startswith("win"), reason="See pact-foundation/pact-python#639", ) -@pytest.mark.container() +@pytest.mark.container @scenario( "definition/features/V1/http_provider.feature", "Verifying multiple Pact files via a Pact broker", @@ -128,7 +128,7 @@ def test_verifying_multiple_pact_files_via_a_pact_broker() -> None: sys.platform.startswith("win"), reason="See pact-foundation/pact-python#639", ) -@pytest.mark.container() +@pytest.mark.container @scenario( "definition/features/V1/http_provider.feature", "Incorrect request is made to provider via a Pact broker", diff --git a/tests/v3/compatibility_suite/util/interaction_definition.py b/tests/v3/compatibility_suite/util/interaction_definition.py index 8e985d180..0f1971c88 100644 --- a/tests/v3/compatibility_suite/util/interaction_definition.py +++ b/tests/v3/compatibility_suite/util/interaction_definition.py @@ -15,7 +15,7 @@ import sys import typing from typing import Any, Literal -from xml.etree import ElementTree +from xml.etree import ElementTree as ET # noqa: N817 import flask from flask import request @@ -99,7 +99,7 @@ def parse_fixture(self, fixture: Path) -> None: This is used to parse the fixture files that contain additional metadata about the body (such as the content type). """ - etree = ElementTree.parse(fixture) # noqa: S314 + etree = ET.parse(fixture) # noqa: S314 root = etree.getroot() if not root or root.tag != "body": msg = "Invalid XML fixture document" diff --git a/tests/v3/test_async_interaction.py b/tests/v3/test_async_interaction.py index 3dc9a0b32..abafddb9a 100644 --- a/tests/v3/test_async_interaction.py +++ b/tests/v3/test_async_interaction.py @@ -11,7 +11,7 @@ from pact.v3 import Pact -@pytest.fixture() +@pytest.fixture def pact() -> Pact: """ Fixture for a Pact instance. diff --git a/tests/v3/test_http_interaction.py b/tests/v3/test_http_interaction.py index 6e56ecd79..b235cfbb2 100644 --- a/tests/v3/test_http_interaction.py +++ b/tests/v3/test_http_interaction.py @@ -31,7 +31,7 @@ ] -@pytest.fixture() +@pytest.fixture def pact() -> Pact: """ Fixture for a Pact instance. @@ -56,7 +56,7 @@ def test_repr(pact: Pact) -> None: "method", ALL_HTTP_METHODS, ) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_basic_request_method(pact: Pact, method: str) -> None: ( pact.upon_receiving(f"a basic {method} request") @@ -78,7 +78,7 @@ async def test_basic_request_method(pact: Pact, method: str) -> None: "status", list(range(200, 600, 13)), ) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_basic_response_status(pact: Pact, status: int) -> None: ( pact.upon_receiving(f"a basic request producing status {status}") @@ -99,7 +99,7 @@ async def test_basic_response_status(pact: Pact, status: int) -> None: [("X-Test", "1"), ("X-Test", "2")], ], ) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_with_header_request( pact: Pact, headers: list[tuple[str, str]], @@ -124,7 +124,7 @@ async def test_with_header_request( [("X-Test", "1"), ("X-Test", "2")], ], ) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_with_header_response( pact: Pact, headers: list[tuple[str, str]], @@ -144,7 +144,7 @@ async def test_with_header_response( assert (header.lower(), value) in response_headers -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_with_header_dict(pact: Pact) -> None: ( pact.upon_receiving("a basic request with a headers from a dict") @@ -169,7 +169,7 @@ async def test_with_header_dict(pact: Pact) -> None: [("X-Foo", "true"), ("X-Bar", "true")], ], ) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_set_header_request( pact: Pact, headers: list[tuple[str, str]], @@ -186,7 +186,7 @@ async def test_set_header_request( assert resp.status == 200 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_set_header_request_repeat( pact: Pact, ) -> None: @@ -218,7 +218,7 @@ async def test_set_header_request_repeat( [("X-Foo", "true"), ("X-Bar", "true")], ], ) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_set_header_response( pact: Pact, headers: list[tuple[str, str]], @@ -238,7 +238,7 @@ async def test_set_header_response( assert (header.lower(), value) in response_headers -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_set_header_response_repeat( pact: Pact, ) -> None: @@ -260,7 +260,7 @@ async def test_set_header_response_repeat( assert ("x-test", "1") not in response_headers -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_set_header_dict(pact: Pact) -> None: ( pact.upon_receiving("a basic request with a headers from a dict") @@ -286,7 +286,7 @@ async def test_set_header_dict(pact: Pact) -> None: [("test", "1"), ("test", "2")], ], ) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_with_query_parameter_request( pact: Pact, query: list[tuple[str, str]], @@ -304,7 +304,7 @@ async def test_with_query_parameter_request( assert resp.status == 200 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_with_query_parameter_dict(pact: Pact) -> None: ( pact.upon_receiving("a basic request with a query parameter from a dict") @@ -323,7 +323,7 @@ async def test_with_query_parameter_dict(pact: Pact) -> None: "method", ["GET", "POST", "PUT"], ) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_with_body_request(pact: Pact, method: str) -> None: ( pact.upon_receiving(f"a basic {method} request with a body") @@ -345,7 +345,7 @@ async def test_with_body_request(pact: Pact, method: str) -> None: "method", ["GET", "POST", "PUT"], ) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_with_body_response(pact: Pact, method: str) -> None: ( pact.upon_receiving( @@ -366,7 +366,7 @@ async def test_with_body_response(pact: Pact, method: str) -> None: assert json.loads(await resp.content.read()) == {"test": True} -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_with_body_explicit(pact: Pact) -> None: ( pact.upon_receiving("") @@ -400,7 +400,7 @@ def test_with_body_invalid(pact: Pact) -> None: ) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_given(pact: Pact) -> None: ( pact.upon_receiving("a basic request given state 1") @@ -432,7 +432,7 @@ async def test_given(pact: Pact) -> None: assert resp.status == 202 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_binary_file_request(pact: Pact) -> None: payload = bytes(range(8)) ( @@ -452,7 +452,7 @@ async def test_binary_file_request(pact: Pact) -> None: assert resp.status == 200 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_binary_file_response(pact: Pact) -> None: payload = bytes(range(5)) ( @@ -471,7 +471,7 @@ async def test_binary_file_response(pact: Pact) -> None: @pytest.mark.skip(reason="Not working yet") -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_multipart_file_request(pact: Pact, temp_dir: Path) -> None: fpy = temp_dir / "test.py" fpng = temp_dir / "test.png" @@ -512,7 +512,7 @@ async def test_multipart_file_request(pact: Pact, temp_dir: Path) -> None: assert await resp.read() == b"" -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_name(pact: Pact) -> None: ( pact.upon_receiving("a basic request with a test name") @@ -526,7 +526,7 @@ async def test_name(pact: Pact) -> None: assert await resp.read() == b"" -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_with_plugin(pact: Pact) -> None: ( pact.upon_receiving("a basic request with a plugin") @@ -540,7 +540,7 @@ async def test_with_plugin(pact: Pact) -> None: assert await resp.read() == b"" -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_pact_server_verbose( pact: Pact, caplog: pytest.LogCaptureFixture, diff --git a/tests/v3/test_pact.py b/tests/v3/test_pact.py index 2264346d0..5928e2bb5 100644 --- a/tests/v3/test_pact.py +++ b/tests/v3/test_pact.py @@ -16,7 +16,7 @@ from pathlib import Path -@pytest.fixture() +@pytest.fixture def pact() -> Pact: """ Fixture for a Pact instance. diff --git a/tests/v3/test_sync_interaction.py b/tests/v3/test_sync_interaction.py index 3dc9a0b32..abafddb9a 100644 --- a/tests/v3/test_sync_interaction.py +++ b/tests/v3/test_sync_interaction.py @@ -11,7 +11,7 @@ from pact.v3 import Pact -@pytest.fixture() +@pytest.fixture def pact() -> Pact: """ Fixture for a Pact instance. diff --git a/tests/v3/test_verifier.py b/tests/v3/test_verifier.py index aab32d1b9..933e1bd2c 100644 --- a/tests/v3/test_verifier.py +++ b/tests/v3/test_verifier.py @@ -16,7 +16,7 @@ ASSETS_DIR = Path(__file__).parent / "assets" -@pytest.fixture() +@pytest.fixture def verifier() -> Verifier: return Verifier() From 4236be3762de8332c5ce39b96f9a56446cde9a42 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 21 Aug 2024 16:24:42 +1000 Subject: [PATCH 0457/1376] chore: add extra checks Add a few extra checks: - Typos - All `pre-commit` checks Also refactor the test CI workflow Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 20 +++- .github/workflows/test.yml | 108 +++++++++++++++--- .pre-commit-config.yaml | 40 ++++--- CHANGELOG.md | 40 +++---- CONTRIBUTING.md | 2 +- biome.json | 16 +++ committed.toml | 17 +++ ...2 integrating rust ffi with pact python.md | 2 +- docs/provider.md | 2 +- docs/scripts/python.py | 2 +- pyproject.toml | 9 ++ src/pact/matchers.py | 2 +- src/pact/message_consumer.py | 4 +- src/pact/message_provider.py | 2 +- src/pact/v3/ffi.py | 6 +- src/pact/v3/interaction/_http_interaction.py | 2 +- tests/test_verifier.py | 2 +- .../test_v3_message_consumer.py | 2 +- .../util/interaction_definition.py | 2 +- 19 files changed, 207 insertions(+), 73 deletions(-) create mode 100644 biome.json create mode 100644 committed.toml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c064fb9c..e33ad9145 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,24 @@ env: CIBW_BUILD_FRONTEND: build jobs: + complete: + name: Build completion check + if: always() + + permissions: + contents: none + + runs-on: ubuntu-latest + needs: + - build-sdist + - build-x86_64 + - build-arm64 + + steps: + - name: Failed + run: exit 1 + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + build-sdist: name: Build source distribution @@ -123,7 +141,7 @@ jobs: # As this requires emulation, it's not worth running on PRs or master if: >- github.event_name == 'push' && - startsWith(github.event.ref, 'refs/tags') + startsWith(github.event.ref, 'refs/tags/v*') runs-on: ${{ matrix.os }} strategy: fail-fast: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd7528046..05f260225 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,9 +19,33 @@ env: FORCE_COLOR: "1" jobs: - test-container: + complete: + name: Test completion check + if: always() + + permissions: + contents: none + + runs-on: ubuntu-latest + needs: + - test-linux + - test-other + - example + - format + - lint + - typecheck + - spelling + - pre-commit + + steps: + - name: Failed + run: exit 1 + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') + + test-linux: name: >- - Tests py${{ matrix.python-version }} on ${{ matrix.os }} + Test Python ${{ matrix.python-version }} + on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} @@ -93,9 +117,10 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} - test-no-container: + test-other: name: >- - Tests py${{ matrix.python-version }} on ${{ matrix.os }} + Test Python ${{ matrix.python-version }} + on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} @@ -139,17 +164,6 @@ jobs: - name: Run tests run: hatch run test - test-conlusion: - name: Test matrix complete - - runs-on: ubuntu-latest - needs: - - test-container - - test-no-container - - steps: - - run: echo "Test matrix completed successfully." - example: name: Example @@ -197,6 +211,26 @@ jobs: run: > hatch run example --broker-url=http://pactbroker:pactbroker@localhost:9292 + format: + name: Format + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + + - name: Set up Python + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5 + with: + python-version: ${{ env.STABLE_PYTHON_VERSION }} + cache: pip + + - name: Install Hatch + run: pip install --upgrade hatch + + - name: Format + run: hatch run format + lint: name: Lint @@ -214,11 +248,47 @@ jobs: - name: Install Hatch run: pip install --upgrade hatch - - name: Lint + - name: Format run: hatch run lint - - name: Typecheck - run: hatch run typecheck + typecheck: + name: Typecheck + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + + - name: Set up Python + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5 + with: + python-version: ${{ env.STABLE_PYTHON_VERSION }} + cache: pip + + - name: Install Hatch + run: pip install --upgrade hatch - name: Format - run: hatch run format + run: hatch run typecheck + + spelling: + name: Spell check + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Spell Check Repo + uses: crate-ci/typos@master + + pre-commit: + name: Pre-commit + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Run pre-commit + uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09a7df3f7..ce06edea5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,11 +35,11 @@ repos: # allows for comments within JSON files. - id: check-json5 - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 + - repo: https://github.com/biomejs/pre-commit + rev: "v0.4.0" # Use the sha / tag you want to point at hooks: - - id: prettier - stages: [pre-push] + - id: biome-check + additional_dependencies: ["@biomejs/biome@1.8.2"] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.1 @@ -52,11 +52,25 @@ repos: - id: ruff-format exclude: ^(pact|tests)/(?!v3/).*\.py$ - - repo: https://github.com/commitizen-tools/commitizen - rev: v3.29.0 + - repo: https://github.com/crate-ci/committed + rev: v1.0.20 + hooks: + - id: committed + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.41.0 hooks: - - id: commitizen - stages: [commit-msg] + - id: markdownlint + exclude: | + (?x)^( + .github/PULL_REQUEST_TEMPLATE\.md + | CHANGELOG.md + ) + + - repo: https://github.com/crate-ci/typos + rev: v1.23.6 + hooks: + - id: typos - repo: local hooks: @@ -69,13 +83,3 @@ repos: types: [python] exclude: ^(src/pact|tests)/(?!v3/).*\.py$ stages: [pre-push] - - - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.41.0 - hooks: - - id: markdownlint - exclude: | - (?x)^( - .github/PULL_REQUEST_TEMPLATE\.md - | CHANGELOG.md - ) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02166afb4..27152f718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -293,7 +293,7 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. - e097153 - docs: update readme (William Infante, Mon Jan 25 17:18:35 2021 +1100) - fc0d91c - feat: address PR comments (William Infante, Mon Jan 25 10:30:33 2021 +1100) - 5448d8c - test: remove mock and check generated json file (William Infante, Wed Jan 20 21:07:39 2021 +1100) -- abd3574 - fix: few more tests to improve coverage (Tuan Pham, Wed Jan 20 09:23:13 2021 +1100) +- abd3574 - fix: few more tests to improve coverage (Tuan Pham, Wed Jan 20 09:23:13 2021 +1100) - 0ef971f - fix: improve test coverage (Tuan Pham, Tue Jan 19 15:11:11 2021 +1100) - e543f04 - chore: add missing import (William Infante, Tue Jan 19 13:58:17 2021 +1100) - bc7ff78 - chore: pydocstyle (Tuan Pham, Tue Jan 19 10:40:12 2021 +1100) @@ -301,9 +301,9 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. - 19827d0 - chore: remove test param for provider (Tuan Pham, Mon Jan 18 16:06:42 2021 +1100) - 912b477 - chore: flake8 revert (Tuan Pham, Mon Jan 18 16:04:00 2021 +1100) - 4a730ae - fix: revert changes to quotes (Tuan Pham, Mon Jan 18 15:57:14 2021 +1100) -- cfe35cc - feat: update message hander to be independent of pact (William Infante, Mon Jan 18 13:12:17 2021 +1100) +- cfe35cc - feat: update message handler to be independent of pact (William Infante, Mon Jan 18 13:12:17 2021 +1100) - 12b4a50 - fix: flake8 warning (Tuan Pham, Mon Jan 18 12:50:50 2021 +1100) -- 7afe693 - test: update message handler condition based on content (William Infante, Mon Jan 18 12:37:50 2021 +1100) +- 7afe693 - test: update message handler condition based on content (William Infante, Mon Jan 18 12:37:50 2021 +1100) - 79106bb - feat: move publish function to broker class (Tuan Pham, Mon Jan 18 11:30:35 2021 +1100) - a04b954 - docs: add readme for message consumer (William Infante, Mon Jan 18 09:48:01 2021 +1100) - 8672e2f - feat: update handler to handle error exceptions (William Infante, Fri Jan 15 16:24:38 2021 +1100) @@ -322,7 +322,7 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. - 8546a26 - fix: linting (Tuan Pham, Thu Jan 14 10:38:52 2021 +1100) - 65b69d7 - fix: remove publish fn for now (Tuan Pham, Thu Jan 14 10:38:10 2021 +1100) - e31dd45 - feat: add constants test (William Infante, Wed Jan 13 17:28:45 2021 +1100) -- 955dbe1 - feat: update MessageConsumer and tests (William Infante, Wed Jan 13 16:38:30 2021 +1100) +- 955dbe1 - feat: update MessageConsumer and tests (William Infante, Wed Jan 13 16:38:30 2021 +1100) - af5c9fb - feat: create basic tests for single pact message (William Infante, Wed Jan 13 15:52:07 2021 +1100) - fea27c8 - feat: single message flow (William Infante, Wed Jan 13 15:03:41 2021 +1100) - 9047855 - feat: add MessageConsumer (William Infante, Wed Jan 13 12:45:19 2021 +1100) @@ -378,7 +378,7 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. - e097153 - docs: update readme (William Infante, Mon Jan 25 17:18:35 2021 +1100) - fc0d91c - feat: address PR comments (William Infante, Mon Jan 25 10:30:33 2021 +1100) - 5448d8c - test: remove mock and check generated json file (William Infante, Wed Jan 20 21:07:39 2021 +1100) -- abd3574 - fix: few more tests to improve coverage (Tuan Pham, Wed Jan 20 09:23:13 2021 +1100) +- abd3574 - fix: few more tests to improve coverage (Tuan Pham, Wed Jan 20 09:23:13 2021 +1100) - 0ef971f - fix: improve test coverage (Tuan Pham, Tue Jan 19 15:11:11 2021 +1100) - e543f04 - chore: add missing import (William Infante, Tue Jan 19 13:58:17 2021 +1100) - bc7ff78 - chore: pydocstyle (Tuan Pham, Tue Jan 19 10:40:12 2021 +1100) @@ -386,9 +386,9 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. - 19827d0 - chore: remove test param for provider (Tuan Pham, Mon Jan 18 16:06:42 2021 +1100) - 912b477 - chore: flake8 revert (Tuan Pham, Mon Jan 18 16:04:00 2021 +1100) - 4a730ae - fix: revert changes to quotes (Tuan Pham, Mon Jan 18 15:57:14 2021 +1100) -- cfe35cc - feat: update message hander to be independent of pact (William Infante, Mon Jan 18 13:12:17 2021 +1100) +- cfe35cc - feat: update message handler to be independent of pact (William Infante, Mon Jan 18 13:12:17 2021 +1100) - 12b4a50 - fix: flake8 warning (Tuan Pham, Mon Jan 18 12:50:50 2021 +1100) -- 7afe693 - test: update message handler condition based on content (William Infante, Mon Jan 18 12:37:50 2021 +1100) +- 7afe693 - test: update message handler condition based on content (William Infante, Mon Jan 18 12:37:50 2021 +1100) - 79106bb - feat: move publish function to broker class (Tuan Pham, Mon Jan 18 11:30:35 2021 +1100) - a04b954 - docs: add readme for message consumer (William Infante, Mon Jan 18 09:48:01 2021 +1100) - 8672e2f - feat: update handler to handle error exceptions (William Infante, Fri Jan 15 16:24:38 2021 +1100) @@ -407,7 +407,7 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. - 8546a26 - fix: linting (Tuan Pham, Thu Jan 14 10:38:52 2021 +1100) - 65b69d7 - fix: remove publish fn for now (Tuan Pham, Thu Jan 14 10:38:10 2021 +1100) - e31dd45 - feat: add constants test (William Infante, Wed Jan 13 17:28:45 2021 +1100) -- 955dbe1 - feat: update MessageConsumer and tests (William Infante, Wed Jan 13 16:38:30 2021 +1100) +- 955dbe1 - feat: update MessageConsumer and tests (William Infante, Wed Jan 13 16:38:30 2021 +1100) - af5c9fb - feat: create basic tests for single pact message (William Infante, Wed Jan 13 15:52:07 2021 +1100) - fea27c8 - feat: single message flow (William Infante, Wed Jan 13 15:03:41 2021 +1100) - 9047855 - feat: add MessageConsumer (William Infante, Wed Jan 13 12:45:19 2021 +1100) @@ -422,7 +422,7 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. ## 1.2.11 -- ba10318 - Merge pull request #192 from pact-foundation/fix/deploy_wheel_fix (Elliott Murray, Tue Dec 29 20:05:52 2020 +0000) +- ba10318 - Merge pull request #192 from pact-foundation/fix/deploy_wheel_fix (Elliott Murray, Tue Dec 29 20:05:52 2020 +0000) - 289e784 - fix: not creating wheel (Elliott Murray, Tue Dec 29 20:00:19 2020 +0000) - d217e67 - chore: Releasing version 1.2.10 (Elliott Murray, Sat Dec 19 12:41:02 2020 +0000) - 9438449 - Merge pull request #191 from pact-foundation/build-and-test-with-github-actions (Elliott Murray, Sat Dec 19 12:38:23 2020 +0000) @@ -438,7 +438,7 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. - 48a2a21 - Merge pull request #186 from jstoebel/patch-2 (Elliott Murray, Sat Nov 21 10:39:48 2020 +0000) - 74f9a4f - docs: fix small typo in `with_request` doc string (Jacob Stoebel, Wed Nov 18 14:51:03 2020 -0500) - 4e4ed26 - chore: added run test to travis (Elliott Murray, Sun Nov 1 11:49:38 2020 +0000) -- 37e2f3a - chore: wqshell script to run flask in exmaples (Elliott Murray, Sun Nov 1 11:41:59 2020 +0000) +- 37e2f3a - chore: wqshell script to run flask in examples (Elliott Murray, Sun Nov 1 11:41:59 2020 +0000) - b5d9d7b - chore(upgrade): upgrade python version to 3.8 (Elliott Murray, Sun Nov 1 11:12:10 2020 +0000) ## 1.2.10 @@ -456,7 +456,7 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. - 48a2a21 - Merge pull request #186 from jstoebel/patch-2 (Elliott Murray, Sat Nov 21 10:39:48 2020 +0000) - 74f9a4f - docs: fix small typo in `with_request` doc string (Jacob Stoebel, Wed Nov 18 14:51:03 2020 -0500) - 4e4ed26 - chore: added run test to travis (Elliott Murray, Sun Nov 1 11:49:38 2020 +0000) -- 37e2f3a - chore: wqshell script to run flask in exmaples (Elliott Murray, Sun Nov 1 11:41:59 2020 +0000) +- 37e2f3a - chore: wqshell script to run flask in examples (Elliott Murray, Sun Nov 1 11:41:59 2020 +0000) - b5d9d7b - chore(upgrade): upgrade python version to 3.8 (Elliott Murray, Sun Nov 1 11:12:10 2020 +0000) ## 1.2.9 @@ -482,9 +482,9 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. ## 1.2.7 - 90b71d2 - Merge pull request #178 from pact-foundation/fix/custom_header_typo (Elliott Murray, Fri Oct 9 12:47:37 2020 +0100) -- b07ef69 - fix(verifier): headers not propogated properly (Elliott Murray, Fri Oct 9 12:24:25 2020 +0100) +- b07ef69 - fix(verifier): headers not propagated properly (Elliott Murray, Fri Oct 9 12:24:25 2020 +0100) - 0e9b71c - Merge pull request #177 from pact-foundation/docs/remove_handcrafted_broker (Elliott Murray, Fri Oct 9 12:01:24 2020 +0100) -- 2db7008 - docs(examples): removed manaul publish to broker (Elliott Murray, Fri Oct 9 11:54:30 2020 +0100) +- 2db7008 - docs(examples): removed manual publish to broker (Elliott Murray, Fri Oct 9 11:54:30 2020 +0100) ## 1.2.6 @@ -514,7 +514,7 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. - 8188d88 - chore: fix release script (Elliott Murray, Wed Aug 26 12:46:10 2020 +0100) - e0e5106 - Merge pull request #169 from pact-foundation/chore/update_pr_scripts (Elliott Murray, Wed Aug 26 10:24:47 2020 +0100) -- 81fd653 - chore: release script updates version automaitcally now (Elliott Murray, Wed Aug 26 10:16:14 2020 +0100) +- 81fd653 - chore: release script updates version automatically now (Elliott Murray, Wed Aug 26 10:16:14 2020 +0100) - 773d3f9 - chore: script now uses gh over hub (Elliott Murray, Wed Aug 26 10:03:06 2020 +0100) - 468e4ad - Merge pull request #168 from pact-foundation/chore/upgrade-to-pact-ruby-standalone-1-88-3 (Elliott Murray, Wed Aug 26 09:49:33 2020 +0100) - ce944fe - feat: update standalone to 1.88.3 (Elliott Murray, Wed Aug 26 09:08:27 2020 +0100) @@ -607,7 +607,7 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. - 48ad173 - Merge pull request #135 from m-aciek/master (Elliott Murray, Sat May 9 17:21:52 2020 +0100) - 6948482 - Merge pull request #136 from pact-foundation/chore/upgrade-to-pact-ruby-standalone-1-84-0 (Elliott Murray, Sat May 9 15:13:07 2020 +0100) - 14603ac - feat: update standalone to 1.84.0 (Beth Skurrie, Sat May 2 09:43:30 2020 +1000) -- 410caf1 - chore: add script to create a PR to update the pact-ruby-standalone version (Beth Skurrie, Sat May 2 09:42:55 2020 +1000) +- 410caf1 - chore: add script to create a PR to update the pact-ruby-standalone version (Beth Skurrie, Sat May 2 09:42:55 2020 +1000) - b5af1fc - Fix missing normalization of consumer name while publishing pact (Maciej Olko, Thu Apr 30 08:50:17 2020 +0200) - 5785782 - Move tests to standard tests dir (Peter Yasi, Fri Apr 17 14:03:04 2020 -0400) - 88ea23d - docs: update RELEASING.md (Beth Skurrie, Tue Feb 18 10:46:11 2020 +1100) @@ -624,11 +624,11 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. ## 0.20.0 - 978d9f3 - fix typo (Jingjing Duan, Wed May 24 15:48:43 2017 -0700) -- 4ede7d5 - Merge pull request #117 from dlmiddlecote/feature/expose-more-options (Matt Fellows, Fri Jan 17 10:00:56 2020 +1100) +- 4ede7d5 - Merge pull request #117 from dlmiddlecote/feature/expose-more-options (Matt Fellows, Fri Jan 17 10:00:56 2020 +1100) - 73ae8d2 - Update docs (Daniel Middlecote, Tue Jan 14 22:11:40 2020 +0000) - 2bffe5e - Simple test case (Daniel Middlecote, Tue Jan 14 22:11:25 2020 +0000) -- 3ba51b5 - Add --broker-token support (Daniel Middlecote, Tue Jan 14 22:04:39 2020 +0000) -- d3a8ba6 - Update pact-ruby-standalone (Daniel Middlecote, Tue Jan 14 21:45:01 2020 +0000) +- 3ba51b5 - Add --broker-token support (Daniel Middlecote, Tue Jan 14 22:04:39 2020 +0000) +- d3a8ba6 - Update pact-ruby-standalone (Daniel Middlecote, Tue Jan 14 21:45:01 2020 +0000) - 0cbb9d4 - Merge pull request #115 from ejrb/patch-1 (Matthew Balvanz, Sat Dec 14 20:49:56 2019 -0600) - 0c85502 - match platforms like 'macOS-\*' to osx suffix (ejrb, Mon Dec 9 11:13:19 2019 +0000) - 9a0eaa7 - Merge pull request #109 from pact-foundation/dependabot/pip/flask-1.0 (Matthew Balvanz, Mon Sep 30 21:35:20 2019 -0500) @@ -700,7 +700,7 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. - 149dfc7 - Merge pull request #67 from jawu/enable-possibility-to-use-mathers-in-path (Matthew Balvanz, Sun Dec 17 10:32:34 2017 -0600) - fb97d2f - fixed doc string of Request (Janneck Wullschleger, Sat Dec 16 20:44:11 2017 +0100) -- c2c24cc - adjusted doc string of Request calss to allow str and Matcher as path parameter (Janneck Wullschleger, Sat Dec 16 20:40:35 2017 +0100) +- c2c24cc - adjusted doc string of Request class to allow str and Matcher as path parameter (Janneck Wullschleger, Sat Dec 16 20:40:35 2017 +0100) - 697a6a2 - fixed port parameter in e2e test for python 2.7 (Janneck Wullschleger, Thu Dec 14 15:08:05 2017 +0100) - ca2eb92 - added from_term call in Request constructor to process path property for json transport (Janneck Wullschleger, Thu Dec 14 14:45:11 2017 +0100) @@ -826,5 +826,5 @@ Identical to 2.1.0, but with a fix to the publication process to PyPI. - 189c647 - Merge pull request #3 from pact-foundation/travis-ci (Jose Salvatierra, Fri Apr 7 21:40:02 2017 +0100) - 559efb8 - Add Travis CI build (Matthew Balvanz, Fri Apr 7 11:12:01 2017 -0500) - 8f074a0 - Merge pull request #1 from pact-foundation/initial-framework (Matthew Balvanz, Fri Apr 7 09:55:34 2017 -0500) -- f5caf9c - Initial pact-python implementation (Matthew Balvanz, Mon Apr 3 09:16:59 2017 -0500) +- f5caf9c - Initial pact-python implementation (Matthew Balvanz, Mon Apr 3 09:16:59 2017 -0500) - bfb8380 - Initial pact-python implementation (Matthew Balvanz, Thu Mar 30 20:41:05 2017 -0500) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db794c92f..2123f55f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,7 +99,7 @@ You can also try using the new [github.dev](https://github.dev/pact-foundation/p pipx install hatch ``` -3. After cloning the repository, run `hatch shell` in the root of the repository. This will install all dependencies in a Python virtual environment and then ensure that the virtual environment is activated. You will also need to run `git submodule init` if you want to run tests, as Pact Python makes use of the Pact Compability Suite. +3. After cloning the repository, run `hatch shell` in the root of the repository. This will install all dependencies in a Python virtual environment and then ensure that the virtual environment is activated. You will also need to run `git submodule init` if you want to run tests, as Pact Python makes use of the Pact Compatibility Suite. 4. Patch the compatibility suite by running `cd tests/v3/compatibility_suite && patch -p1 -d definition < definition-update.diff && cd -` in the root of the repository. diff --git a/biome.json b/biome.json new file mode 100644 index 000000000..76cd05459 --- /dev/null +++ b/biome.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.2/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "indentWidth": 2, + "indentStyle": "space" + } +} diff --git a/committed.toml b/committed.toml new file mode 100644 index 000000000..0899f2f51 --- /dev/null +++ b/committed.toml @@ -0,0 +1,17 @@ +## Configuration for committed +## +## See +style = "conventional" +allowed_types = [ + "fix", + "feat", + "chore", + "docs", + "style", + "refactor", + "perf", + "test", + "release", +] +merge_commit = false +subject_capitalized = false diff --git a/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md b/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md index 648361548..54515dad3 100644 --- a/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md +++ b/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md @@ -83,7 +83,7 @@ if isinstance(version, bytes): # (3) 2. Convert the pointer to a Python string, or bytes if necessary, using the `ffi.string` method. 3. Decode the bytes to a string if needed. -While the process is reasonably straightforward, it does require some boilerplate code to handle the type conversions. To simplify this, we've wrapped each function from the Rust core library in a simple Python function that performs these conversion autoamtically. You can find these wrapper functions in the [`ffi` module](https://github.com/pact-foundation/pact-python/blob/d6869797b52429252b5d0da4d0fc0079f9d3671c/src/pact/v3/ffi.py). For example, the `version` function is implemented as follows: +While the process is reasonably straightforward, it does require some boilerplate code to handle the type conversions. To simplify this, we've wrapped each function from the Rust core library in a simple Python function that performs these conversion automatically. You can find these wrapper functions in the [`ffi` module](https://github.com/pact-foundation/pact-python/blob/d6869797b52429252b5d0da4d0fc0079f9d3671c/src/pact/v3/ffi.py). For example, the `version` function is implemented as follows: ```python def version() -> str: diff --git a/docs/provider.md b/docs/provider.md index f4dded900..363d0b721 100644 --- a/docs/provider.md +++ b/docs/provider.md @@ -1,6 +1,6 @@ # Provider Testing -Pact is a consumer-driven contract testng tool. This means that the consumer specifies the expected interactions with the provider, and these interactions are used to create a contract. This contract is then used to verify that the provider behaves as expected. +Pact is a consumer-driven contract testing tool. This means that the consumer specifies the expected interactions with the provider, and these interactions are used to create a contract. This contract is then used to verify that the provider behaves as expected. The provider verification process works by replaying the interactions from the consumer against the provider and checking that the responses match what was expected. This is done by using the Pact files created by the consumer tests, either by reading them from a local filesystem, or by fetching them from a Pact Broker. diff --git a/docs/scripts/python.py b/docs/scripts/python.py index 21e024fee..f5335c00f 100644 --- a/docs/scripts/python.py +++ b/docs/scripts/python.py @@ -155,7 +155,7 @@ def process_python( List of tuples containing the source and destination Python identifiers to map. Note that the list is processed in order, with later mappings applied after earlier mappings. This is applied - idependently of the `destination_mapping` argument. + independently of the `destination_mapping` argument. ignore_private: Whether to ignore private modules (those starting with an underscore diff --git a/pyproject.toml b/pyproject.toml index ece85cd42..c3580a91c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -344,3 +344,12 @@ before-build = [ 'IF EXIST src\pact\v3\data\ RMDIR /S /Q src\pact\v3\data', 'IF EXIST src\pact\v3\lib\ RMDIR /S /Q src\pact\v3\lib', ] + +################################################################################ +## Typos +################################################################################ + +[tool.typos.default] +extend-ignore-re = [ + "(?Rm)^.*(#|//| ## :airplane: Pre-flight checklist -- [ ] I have read the [Contributing Guidelines on pull requests](https://github.com/pact-foundation/pact-python/blob/master/CONTRIBUTING.md#pull-requests). +- [ ] I have read the [Contributing Guidelines on pull requests](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md#pull-requests). - [ ] **If this is a code change**: I have written unit tests and/or added dogfooding pages to fully verify the new behavior. - [ ] **If this is a new API or substantial change**: the PR has an accompanying issue (closes #0000) and the maintainers have approved on my working plan. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f499bd3e7..cb185eb92 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,10 +6,10 @@ on: tags: - v* branches: - - master + - main pull_request: branches: - - master + - main concurrency: group: ${{ github.workflow }}-${{ github.ref || github.run_id }} @@ -142,7 +142,7 @@ jobs: build-arm64: name: Build wheels on ${{ matrix.os }} (arm64) - # As this requires emulation, it's not worth running on PRs or master + # As this requires emulation, it's not worth running on PRs or main if: >- github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') @@ -293,4 +293,4 @@ jobs: body: | This PR updates the changelog for ${{ github.ref_name }}. branch: chore/update-changelog - base: master + base: main diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4fd5e24c4..bd3f20825 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,10 +4,10 @@ name: docs on: push: branches: - - master + - main pull_request: branches: - - master + - main env: STABLE_PYTHON_VERSION: '3.13' @@ -52,7 +52,7 @@ jobs: publish: name: Publish docs - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/main' needs: build runs-on: ubuntu-latest diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 312961594..de1e9eae2 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -9,7 +9,7 @@ on: - cron: 20 0 * * 0 push: branches: - - master + - main paths: - .github/labels.yml @@ -27,5 +27,5 @@ jobs: uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2 with: config-file: |- - https://raw.githubusercontent.com/pact-foundation/.github/master/.github/labels.yml + https://raw.githubusercontent.com/pact-foundation/.github/main/.github/labels.yml .github/labels.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a07f8d090..f5505c6af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,10 +4,10 @@ name: test on: push: branches: - - master + - main pull_request: branches: - - master + - main concurrency: group: ${{ github.workflow }}-${{ github.ref || github.run_id }} @@ -328,7 +328,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Spell Check Repo - uses: crate-ci/typos@master + uses: crate-ci/typos@main pre-commit: name: Pre-commit diff --git a/.github/workflows/trigger_pact_docs_update.yml b/.github/workflows/trigger_pact_docs_update.yml index b57442259..046143f23 100644 --- a/.github/workflows/trigger_pact_docs_update.yml +++ b/.github/workflows/trigger_pact_docs_update.yml @@ -4,7 +4,7 @@ name: Trigger update to docs.pact.io on: push: branches: - - master + - main paths: - '**.md' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2123f55f5..aac933bf1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,7 @@ All pull requests will be checked by the continuous integration system, GitHub a ### Branch Organization -Pact Python has one primary branch `master` and we use feature branches to deliver new features with pull requests. Typically, we scope the branch according to the [conventional commit](#conventional-commit-messages) categories. The more common ones are: +Pact Python has one primary branch `main` and we use feature branches to deliver new features with pull requests. Typically, we scope the branch according to the [conventional commit](#conventional-commit-messages) categories. The more common ones are: - `feature/` or `feat/` for new features - `fix/` for bug fixes @@ -137,7 +137,7 @@ Please make sure the following is done when submitting a pull request: 2. **Use descriptive titles.** It is recommended to follow this [commit message style](#conventional-commit-messages). 3. **Test your changes.** Describe your [**test plan**](#test-plan) in your pull request description. -All pull requests should be opened against the `master` branch. +All pull requests should be opened against the `main` branch. We have a lot of integration systems that run automated tests to guard against mistakes. The maintainers will also review your code and may fix obvious issues for you. These systems' duty is to make you worry as little about the chores as possible. Your code contributions are more important than sticking to any procedures, although completing the checklist will surely save everyone's time. diff --git a/README.md b/README.md index 50ea83809..efebabdc1 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,15 @@
diff --git a/docs/provider.md b/docs/provider.md index 363d0b721..6145f9a47 100644 --- a/docs/provider.md +++ b/docs/provider.md @@ -141,9 +141,9 @@ The CLI options are available as keyword arguments to the various methods of the You can see more details in the examples -- [Message Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_03_message_provider.py) -- [Flask Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_01_provider_flask.py) -- [FastAPI Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/master/examples/tests/test_01_provider_fastapi.py) +- [Message Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/main/examples/tests/test_03_message_provider.py) +- [Flask Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/main/examples/tests/test_01_provider_flask.py) +- [FastAPI Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/main/examples/tests/test_01_provider_fastapi.py) ## Provider States diff --git a/docs/releases.md b/docs/releases.md index e5b919042..aac85519d 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,7 +2,7 @@ Pact Python is made available through both GitHub releases and PyPI. The GitHub releases also come with a summary of changes and contributions since the last release. -The entire process is automated through the [build](https://github.com/pact-foundation/pact-python/actions/workflows/build.yml?query=branch%3Amaster) GitHub Action. A description of the process is provided [below](#build-pipeline). +The entire process is automated through the [build](https://github.com/pact-foundation/pact-python/actions/workflows/build.yml?query=branch%3Amain) GitHub Action. A description of the process is provided [below](#build-pipeline). ## Versioning @@ -23,7 +23,7 @@ The version is stored in `pact/__version__.py`. This file is automatically gener ## Build Pipeline -The build pipeline is defined in `.github/workflows/build.yml`. It is triggered on PRs targeting `master`, pushes to the `master` branch, and on every new tag. The pipeline is responsible for building the package (both as source distribution, and compiled wheels), creating the GitHub release, and uploading artifacts to PyPI. +The build pipeline is defined in `.github/workflows/build.yml`. It is triggered on PRs targeting `main`, pushes to the `main` branch, and on every new tag. The pipeline is responsible for building the package (both as source distribution, and compiled wheels), creating the GitHub release, and uploading artifacts to PyPI. ### Build Steps @@ -34,7 +34,7 @@ In order to reduce the build time, the pipeline builds different sets of wheels | Trigger | Platforms | Wheels | | ------------ | ----------------- | --------- | | Tag | `x86_64`, `arm64` | all | -| `master` | `x86_64` | all | +| `main` | `x86_64` | all | | Pull Request | `x86_64` | `cp312-*` | ### Publish Step diff --git a/docs/scripts/markdown.py b/docs/scripts/markdown.py index c27b8b73b..ffb6b1194 100644 --- a/docs/scripts/markdown.py +++ b/docs/scripts/markdown.py @@ -116,7 +116,7 @@ def process_markdown( mkdocs_gen_files.set_edit_path( destination, - f"https://github.com/pact-foundation/pact-python/edit/master/{file}", + f"https://github.com/pact-foundation/pact-python/edit/main/{file}", ) diff --git a/docs/scripts/python.py b/docs/scripts/python.py index f5335c00f..5dd8f8190 100644 --- a/docs/scripts/python.py +++ b/docs/scripts/python.py @@ -203,7 +203,7 @@ def process_python( mkdocs_gen_files.set_edit_path( destination, - f"https://github.com/pact-foundation/pact-python/edit/master/{file}", + f"https://github.com/pact-foundation/pact-python/edit/main/{file}", ) diff --git a/pyproject.toml b/pyproject.toml index af2d5fa43..f109b142f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ dependencies = [ "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" +"Changelog" = "https://github.com/pact-foundation/pact-python/blob/main/CHANGELOG.md" [project.scripts] pact-verifier = "pact.cli.verify:main" From 464637c449f27e4442d8b14c0c9e19206ab116fa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:15:35 +0000 Subject: [PATCH 0612/1376] chore(deps): update ruff to v0.8.0 Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- pyproject.toml | 4 +-- src/pact/v3/generate/__init__.py | 24 ++++++------- src/pact/v3/interaction/__init__.py | 4 +-- src/pact/v3/match/__init__.py | 34 +++++++++---------- tests/v3/compatibility_suite/util/consumer.py | 4 +-- 6 files changed, 35 insertions(+), 37 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a2731823..478362f79 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: - '@biomejs/biome@1.9.4' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.4 + rev: v0.8.0 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index f109b142f..97cd26c40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ devel-test = [ "pytest-cov ~=6.0", "testcontainers ~=4.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.7.4"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.0"] ################################################################################ ## Hatch Build Configuration @@ -276,8 +276,6 @@ ignore = [ "D200", # Require single line docstrings to be on one line. "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 "FIX002", # Forbid TODO in comments "TD002", # Assign someone to 'TODO' comments diff --git a/src/pact/v3/generate/__init__.py b/src/pact/v3/generate/__init__.py index ba94ca5e5..f961ac261 100644 --- a/src/pact/v3/generate/__init__.py +++ b/src/pact/v3/generate/__init__.py @@ -33,25 +33,25 @@ # # __all__ = [ - "int", - "integer", - "float", + "Generator", + "bool", + "boolean", + "date", + "datetime", "decimal", + "float", "hex", "hexadecimal", + "int", + "integer", + "mock_server_url", + "provider_state", + "regex", "str", "string", - "regex", - "uuid", - "date", "time", - "datetime", "timestamp", - "bool", - "boolean", - "provider_state", - "mock_server_url", - "Generator", + "uuid", ] # We prevent users from importing from this module to avoid shadowing built-ins. diff --git a/src/pact/v3/interaction/__init__.py b/src/pact/v3/interaction/__init__.py index 661fed172..4116277d2 100644 --- a/src/pact/v3/interaction/__init__.py +++ b/src/pact/v3/interaction/__init__.py @@ -76,8 +76,8 @@ from pact.v3.interaction._sync_message_interaction import SyncMessageInteraction __all__ = [ - "Interaction", - "HttpInteraction", "AsyncMessageInteraction", + "HttpInteraction", + "Interaction", "SyncMessageInteraction", ] diff --git a/src/pact/v3/match/__init__.py b/src/pact/v3/match/__init__.py index 247dbff62..2e1cca35b 100644 --- a/src/pact/v3/match/__init__.py +++ b/src/pact/v3/match/__init__.py @@ -82,31 +82,31 @@ # # __all__ = [ + "Matcher", + "array_containing", + "bool", + "boolean", + "date", + "datetime", + "decimal", + "each_key_matches", + "each_like", + "each_value_matches", + "float", + "includes", "int", "integer", - "float", - "decimal", + "like", + "none", + "null", "number", + "regex", "str", "string", - "regex", - "uuid", - "bool", - "boolean", - "date", "time", - "datetime", "timestamp", - "none", - "null", "type", - "like", - "each_like", - "includes", - "array_containing", - "each_key_matches", - "each_value_matches", - "Matcher", + "uuid", ] _T = TypeVar("_T") diff --git a/tests/v3/compatibility_suite/util/consumer.py b/tests/v3/compatibility_suite/util/consumer.py index f880d6f56..811a1124e 100644 --- a/tests/v3/compatibility_suite/util/consumer.py +++ b/tests/v3/compatibility_suite/util/consumer.py @@ -722,8 +722,8 @@ def _( expected = {key: value} actual = pact_file["interactions"][n - 1]["request"]["headers"] assert expected.keys() == actual.keys() - for k in expected: - assert expected[k] == actual[k] or [expected[k]] == actual[k] + for k, v in expected.items(): + assert v == actual[k] or [v] == actual[k] def the_nth_interaction_request_content_type_will_be(stacklevel: int = 1) -> None: From 3c8a92f331ec9d3616dd70595360837b3c3c31ae Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 25 Nov 2024 13:03:37 +1100 Subject: [PATCH 0613/1376] chore(ci): pin typos to version Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f5505c6af..cc5e8be7a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -327,8 +327,8 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Spell Check Repo - uses: crate-ci/typos@main + - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ + uses: crate-ci/typos@b74202f74b4346efdbce7801d187ec57b266bac8 # v1.27.3 pre-commit: name: Pre-commit From 8278f39840ddadcd032c1a31d82c4b9d0b7749f9 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 25 Nov 2024 13:04:27 +1100 Subject: [PATCH 0614/1376] chore(ci): pin minor version of checkout action Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 8 ++++---- .github/workflows/docs.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/test.yml | 16 ++++++++-------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb185eb92..eab2c5833 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Fetch all tags fetch-depth: 0 @@ -93,7 +93,7 @@ jobs: archs: AMD64 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Fetch all tags fetch-depth: 0 @@ -167,7 +167,7 @@ jobs: # build: "" steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Fetch all tags fetch-depth: 0 @@ -226,7 +226,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Fetch all tags fetch-depth: 0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bd3f20825..de4498775 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index de1e9eae2..8c79ef5ac 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Synchronize labels uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cc5e8be7a..fe4d8653e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -85,7 +85,7 @@ jobs: experimental: true steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: true @@ -159,7 +159,7 @@ jobs: python-version: '3.9' steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: true @@ -206,7 +206,7 @@ jobs: PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 @@ -250,7 +250,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 @@ -275,7 +275,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 @@ -300,7 +300,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 @@ -325,7 +325,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ uses: crate-ci/typos@b74202f74b4346efdbce7801d187ec57b266bac8 # v1.27.3 @@ -336,7 +336,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Run pre-commit uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 From f97feb8cf9839477ebc05168950f990604d468a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:15:32 +0000 Subject: [PATCH 0615/1376] chore(deps): update pre-commit hook igorshubovych/markdownlint-cli to v0.43.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 478362f79..3def9372f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,7 +62,7 @@ repos: - id: committed - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.42.0 + rev: v0.43.0 hooks: - id: markdownlint exclude: | From ca65b766aa593065187a86c4a5544130da40fc7a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:15:02 +0000 Subject: [PATCH 0616/1376] chore(deps): update pypa/cibuildwheel action to v2.22.0 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eab2c5833..63384d592 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,7 +126,7 @@ jobs: echo "MACOSX_DEPLOYMENT_TARGET=10.12" >> "$GITHUB_ENV" - name: Create wheels - uses: pypa/cibuildwheel@7940a4c0e76eb2030e473a5f864f291f63ee879b # v2.21.3 + uses: pypa/cibuildwheel@ee63bf16da6cddfb925f542f2c7b59ad50e93969 # v2.22.0 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} @@ -189,7 +189,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@7940a4c0e76eb2030e473a5f864f291f63ee879b # v2.21.3 + uses: pypa/cibuildwheel@ee63bf16da6cddfb925f542f2c7b59ad50e93969 # v2.22.0 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} From 957431194248ea7fafc457d2e8394685cec7a202 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:15:06 +0000 Subject: [PATCH 0617/1376] chore(deps): update astral-sh/setup-uv action to v4 --- .github/workflows/build.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 63384d592..12be451c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | @@ -238,7 +238,7 @@ jobs: merge-multiple: true - name: Set up uv - uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index de4498775..88097eb5b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe4d8653e..8ff46c687 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up uv - uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | @@ -170,7 +170,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up uv - uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | @@ -209,7 +209,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | @@ -253,7 +253,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | @@ -278,7 +278,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | @@ -303,7 +303,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@e779db74266a80753577425b0f4ee823649f251d # v3.2.3 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | From 7bc467c8dfc3bc6d8b0c817343f665b4ef43966f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 25 Nov 2024 11:02:09 +1100 Subject: [PATCH 0618/1376] chore: silence unset default fixture loop scope Asyncio requires that the fixture loop scope be explicitly set. Signed-off-by: JP-Ellis --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 97cd26c40..235e98762 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,6 +180,7 @@ python = ["3.9", "3.10", "3.11", "3.12", "3.13"] [tool.pytest.ini_options] pythonpath = "." +asyncio_default_fixture_loop_scope = "session" addopts = [ "--import-mode=importlib", "--cov-config=pyproject.toml", From ba3d58022d8508bd036bbfa60a2011931a255dfd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 22:14:06 +0000 Subject: [PATCH 0619/1376] chore(deps): update dependency pytest-bdd to v8 --- pyproject.toml | 2 +- .../compatibility_suite/test_v1_consumer.py | 16 +++--- .../compatibility_suite/test_v1_provider.py | 14 ++--- .../compatibility_suite/test_v2_consumer.py | 15 ++--- .../compatibility_suite/test_v2_provider.py | 14 ++--- .../compatibility_suite/test_v3_consumer.py | 21 ++++--- .../test_v3_message_consumer.py | 57 ++++++++++++------- .../test_v3_message_producer.py | 27 ++++----- .../compatibility_suite/test_v3_provider.py | 14 ++--- .../compatibility_suite/test_v4_provider.py | 14 ++--- tests/v3/compatibility_suite/util/__init__.py | 56 ++++++++++-------- tests/v3/compatibility_suite/util/consumer.py | 22 +++---- tests/v3/compatibility_suite/util/provider.py | 47 ++++++++------- 13 files changed, 172 insertions(+), 147 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 235e98762..1c63c9c7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ devel-test = [ "mock ~=5.0", "pytest ~=8.0", "pytest-asyncio ~=0.0", - "pytest-bdd ~=7.0", + "pytest-bdd ~=8.0", "pytest-cov ~=6.0", "testcontainers ~=4.0", ] diff --git a/tests/v3/compatibility_suite/test_v1_consumer.py b/tests/v3/compatibility_suite/test_v1_consumer.py index 8f222cbf5..3f60c310b 100644 --- a/tests/v3/compatibility_suite/test_v1_consumer.py +++ b/tests/v3/compatibility_suite/test_v1_consumer.py @@ -6,7 +6,7 @@ from pytest_bdd import given, parsers, scenario -from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util import parse_horizontal_table from tests.v3.compatibility_suite.util.consumer import ( a_response_is_returned, request_n_is_made_to_the_mock_server, @@ -288,12 +288,11 @@ def test_request_with_a_multipart_body_negative_case() -> None: @given( - parsers.parse("the following HTTP interactions have been defined:\n{content}"), + parsers.parse("the following HTTP interactions have been defined:"), target_fixture="interaction_definitions", - converters={"content": parse_markdown_table}, ) def the_following_http_interactions_have_been_defined( - content: list[dict[str, str]], + datatable: list[list[str]], ) -> dict[int, InteractionDefinition]: """ Parse the HTTP interactions table into a dictionary. @@ -313,13 +312,16 @@ def the_following_http_interactions_have_been_defined( The first row is ignored, as it is assumed to be the column headers. The order of the columns is similarly ignored. """ + logger.debug("Parsing interaction definitions") + # Check that the table is well-formed - assert len(content[0]) == 9, f"Expected 9 columns, got {len(content[0])}" - assert "No" in content[0], "'No' column not found" + definitions = parse_horizontal_table(datatable) + assert len(definitions[0]) == 9, f"Expected 9 columns, got {len(definitions[0])}" + assert "No" in definitions[0], "'No' column not found" # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} - for row in content: + for row in definitions: interactions[int(row["No"])] = InteractionDefinition(**row) # type: ignore[arg-type] return interactions diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py index b916d21e3..c8ec326fa 100644 --- a/tests/v3/compatibility_suite/test_v1_provider.py +++ b/tests/v3/compatibility_suite/test_v1_provider.py @@ -10,7 +10,7 @@ import pytest from pytest_bdd import given, parsers, scenario -from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util import parse_horizontal_table from tests.v3.compatibility_suite.util.interaction_definition import ( InteractionDefinition, ) @@ -372,12 +372,11 @@ def test_response_with_multipart_body_negative_case() -> None: @given( - parsers.parse("the following HTTP interactions have been defined:\n{content}"), + parsers.parse("the following HTTP interactions have been defined:"), target_fixture="interaction_definitions", - converters={"content": parse_markdown_table}, ) def the_following_http_interactions_have_been_defined( - content: list[dict[str, str]], + datatable: list[list[str]], ) -> dict[int, InteractionDefinition]: """ Parse the HTTP interactions table into a dictionary. @@ -401,12 +400,13 @@ def the_following_http_interactions_have_been_defined( logger.debug("Parsing interaction definitions") # Check that the table is well-formed - assert len(content[0]) == 10, f"Expected 10 columns, got {len(content[0])}" - assert "No" in content[0], "'No' column not found" + definitions = parse_horizontal_table(datatable) + assert len(definitions[0]) == 10, f"Expected 10 columns, got {len(definitions[0])}" + assert "No" in definitions[0], "'No' column not found" # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} - for row in content: + for row in definitions: interactions[int(row["No"])] = InteractionDefinition(**row) # type: ignore[arg-type] return interactions diff --git a/tests/v3/compatibility_suite/test_v2_consumer.py b/tests/v3/compatibility_suite/test_v2_consumer.py index b2b6e3101..5cfdf110d 100644 --- a/tests/v3/compatibility_suite/test_v2_consumer.py +++ b/tests/v3/compatibility_suite/test_v2_consumer.py @@ -6,7 +6,7 @@ from pytest_bdd import given, parsers, scenario -from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util import parse_horizontal_table from tests.v3.compatibility_suite.util.consumer import ( a_response_is_returned, request_n_is_made_to_the_mock_server, @@ -165,11 +165,11 @@ def test_supports_a_matcher_for_request_paths() -> None: @given( - parsers.parse("the following HTTP interactions have been defined:\n{content}"), + parsers.parse("the following HTTP interactions have been defined:"), target_fixture="interaction_definitions", ) def the_following_http_interactions_have_been_defined( - content: str, + datatable: list[list[str]], ) -> dict[int, InteractionDefinition]: """ Parse the HTTP interactions table into a dictionary. @@ -187,15 +187,16 @@ def the_following_http_interactions_have_been_defined( The first row is ignored, as it is assumed to be the column headers. The order of the columns is similarly ignored. """ - table = parse_markdown_table(content) + logger.debug("Parsing interaction definitions") # Check that the table is well-formed - assert len(table[0]) == 7, f"Expected 7 columns, got {len(table[0])}" - assert "No" in table[0], "'No' column not found" + definitions = parse_horizontal_table(datatable) + assert len(definitions[0]) == 7, f"Expected 7 columns, got {len(definitions[0])}" + assert "No" in definitions[0], "'No' column not found" # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} - for row in table: + for row in definitions: interactions[int(row["No"])] = InteractionDefinition(**row) # type: ignore[arg-type] return interactions diff --git a/tests/v3/compatibility_suite/test_v2_provider.py b/tests/v3/compatibility_suite/test_v2_provider.py index f891b60d1..1c5992c12 100644 --- a/tests/v3/compatibility_suite/test_v2_provider.py +++ b/tests/v3/compatibility_suite/test_v2_provider.py @@ -10,7 +10,7 @@ import pytest from pytest_bdd import given, parsers, scenario -from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util import parse_horizontal_table from tests.v3.compatibility_suite.util.interaction_definition import ( InteractionDefinition, ) @@ -92,12 +92,11 @@ def test_verifies_the_response_body_negative_case() -> None: @given( - parsers.parse("the following HTTP interactions have been defined:\n{content}"), + parsers.parse("the following HTTP interactions have been defined:"), target_fixture="interaction_definitions", - converters={"content": parse_markdown_table}, ) def the_following_http_interactions_have_been_defined( - content: list[dict[str, str]], + datatable: list[list[str]], ) -> dict[int, InteractionDefinition]: """ Parse the HTTP interactions table into a dictionary. @@ -119,12 +118,13 @@ def the_following_http_interactions_have_been_defined( logger.debug("Parsing interaction definitions") # Check that the table is well-formed - assert len(content[0]) == 8, f"Expected 8 columns, got {len(content[0])}" - assert "No" in content[0], "'No' column not found" + definitions = parse_horizontal_table(datatable) + assert len(definitions[0]) == 8, f"Expected 8 columns, got {len(definitions[0])}" + assert "No" in definitions[0], "'No' column not found" # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} - for row in content: + for row in definitions: interactions[int(row["No"])] = InteractionDefinition(**row) # type: ignore[arg-type] return interactions diff --git a/tests/v3/compatibility_suite/test_v3_consumer.py b/tests/v3/compatibility_suite/test_v3_consumer.py index 0b1e061ff..bfeebb99c 100644 --- a/tests/v3/compatibility_suite/test_v3_consumer.py +++ b/tests/v3/compatibility_suite/test_v3_consumer.py @@ -10,7 +10,10 @@ from pytest_bdd import given, parsers, scenario, then from pact.v3.pact import HttpInteraction, Pact -from tests.v3.compatibility_suite.util import PactInteractionTuple, parse_markdown_table +from tests.v3.compatibility_suite.util import ( + PactInteractionTuple, + parse_horizontal_table, +) from tests.v3.compatibility_suite.util.consumer import ( the_pact_file_for_the_test_is_generated, ) @@ -71,18 +74,18 @@ def a_provider_state_is_specified( @given( parsers.re( r'a provider state "(?P[^"]+)" is specified' - r" with the following data:\n(?P
Test Status Build Status Build Status
.+)", + r" with the following data:", re.DOTALL, ), - converters={"table": parse_markdown_table}, ) def a_provider_state_is_specified_with_the_following_data( pact_interaction: PactInteractionTuple[HttpInteraction], state: str, - table: list[dict[str, Any]], + datatable: list[list[str]], ) -> None: """A provider state is specified.""" - for row in table: + data: list[dict[str, Any]] = parse_horizontal_table(datatable) + for row in data: for key, value in row.items(): if value.startswith('"') and value.endswith('"'): row[key] = value[1:-1] @@ -96,7 +99,7 @@ def a_provider_state_is_specified_with_the_following_data( elif value.replace(".", "", 1).isdigit(): row[key] = float(value) - pact_interaction.interaction.given(state, parameters=table[0]) + pact_interaction.interaction.given(state, parameters=data[0]) ################################################################################ @@ -154,20 +157,20 @@ def the_interaction_in_the_pact_file_will_container_provider_state( @then( parsers.re( r'the provider state "(?P[^"]+)" in the Pact file' - r" will contain the following parameters:\n(?P
.+)", + r" will contain the following parameters:", re.DOTALL, ), - converters={"table": parse_markdown_table}, ) def the_provider_state_in_the_pact_file_will_contain_the_following_parameters( state: str, - table: list[dict[str, Any]], pact_data: dict[str, Any], + datatable: list[list[str]], ) -> None: """The provider state in the Pact file will contain the following parameters.""" assert "interactions" in pact_data assert len(pact_data["interactions"]) == 1 assert "providerStates" in pact_data["interactions"][0] + table = parse_horizontal_table(datatable) parameters: dict[str, Any] = json.loads(table[0]["parameters"]) provider_state = next( diff --git a/tests/v3/compatibility_suite/test_v3_message_consumer.py b/tests/v3/compatibility_suite/test_v3_message_consumer.py index fb39a3c9e..f0808461a 100644 --- a/tests/v3/compatibility_suite/test_v3_message_consumer.py +++ b/tests/v3/compatibility_suite/test_v3_message_consumer.py @@ -19,7 +19,7 @@ from tests.v3.compatibility_suite.util import ( FIXTURES_ROOT, PactInteractionTuple, - parse_markdown_table, + parse_horizontal_table, ) from tests.v3.compatibility_suite.util.consumer import ( a_message_integration_is_being_defined_for_a_consumer_test, @@ -133,24 +133,35 @@ def test_supports_the_use_of_generators_with_message_metadata() -> None: @given( parsers.re( - r'a provider state "(?P[^"]+)" for the message is specified' - r"( with the following data:\n)?(?P
.*)", + r'a provider state "(?P[^"]+)" for the message is specified', + ), +) +def a_provider_state_for_the_message_is_specified( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + state: str, +) -> None: + """A provider state for the message is specified.""" + logger.debug("Specifying provider state '%s'", state) + pact_interaction.interaction.given(state) + + +@given( + parsers.re( + r'a provider state "(?P[^"]+)" for the message is specified ' + r"with the following data:", re.DOTALL, ), - converters={"table": lambda v: parse_markdown_table(v) if v else None}, ) def a_provider_state_for_the_message_is_specified_with_the_following_data( pact_interaction: PactInteractionTuple[AsyncMessageInteraction], state: str, - table: list[dict[str, str]] | None, + datatable: list[list[str]], ) -> None: """A provider state for the message is specified with the following data.""" - logger.debug("Specifying provider state '%s': %s", state, table) - if table: - parameters = {k: ast.literal_eval(v) for k, v in table[0].items()} - pact_interaction.interaction.given(state, parameters=parameters) - else: - pact_interaction.interaction.given(state) + table = parse_horizontal_table(datatable) + logger.debug("Specifying provider state '%s' with data: %s", state, table) + parameters = {k: ast.literal_eval(v) for k, v in table[0].items()} + pact_interaction.interaction.given(state, parameters=parameters) @given("a message is defined") @@ -160,18 +171,18 @@ def a_message_is_defined() -> None: @given( parsers.re( - r"the message contains the following metadata:\n(?P
.+)", + r"the message contains the following metadata:", re.DOTALL, ), - converters={"table": parse_markdown_table}, ) def the_message_contains_the_following_metadata( pact_interaction: PactInteractionTuple[AsyncMessageInteraction], - table: list[dict[str, Any]], + datatable: list[list[str]], ) -> None: """The message contains the following metadata.""" - logger.debug("Adding metadata to message: %s", table) - for metadata in table: + metadatas = parse_horizontal_table(datatable) + logger.debug("Adding metadata to message: %s", metadatas) + for metadata in metadatas: if metadata.get("value", "").startswith("JSON: "): metadata["value"] = metadata["value"].replace("JSON:", "") pact_interaction.interaction.with_metadata({metadata["key"]: metadata["value"]}) @@ -179,16 +190,16 @@ def the_message_contains_the_following_metadata( @given( parsers.re( - r"the message is configured with the following:\n(?P
.+)", + r"the message is configured with the following:", re.DOTALL, ), - converters={"table": parse_markdown_table}, ) def the_message_is_configured_with_the_following( pact_interaction: PactInteractionTuple[AsyncMessageInteraction], - table: list[dict[str, Any]], + datatable: list[list[str]], ) -> None: """The message is configured with the following.""" + table = parse_horizontal_table(datatable) assert len(table) == 1, "Only one row is expected" config: dict[str, str] = table[0] @@ -506,20 +517,21 @@ def the_pact_file_will_contain_message_interaction( @then( parsers.re( r'the provider state "(?P[^"]+)" for the message ' - r"will contain the following parameters:\n(?P
.+)", + r"will contain the following parameters:", re.DOTALL, ), - converters={"table": parse_markdown_table}, ) def the_provider_state_for_the_message_will_contain_the_following_parameters( pact_interaction: PactInteractionTuple[AsyncMessageInteraction], pact_result: PactResult, state: str, - table: list[dict[str, Any]], + datatable: list[list[str]], ) -> None: """The provider state for the message will contain the following parameters.""" + table = parse_horizontal_table(datatable) assert len(table) == 1, "Only one row is expected" expected = json.loads(table[0]["parameters"]) + logger.debug("Checking provider state '%s' parameters: %s", state, expected) # It is unclear whether this test is meant to verify the `Interaction` # object, or the result as written to the Pact file. As a result, we @@ -536,6 +548,7 @@ def the_provider_state_for_the_message_will_contain_the_following_parameters( assert len(message["providerStates"]) > 0, "At least one provider state is expected" provider_states = message["providerStates"] + logger.debug("Provider states: %s", provider_states) for provider_state_dict in provider_states: if provider_state_dict["name"] == state: assert expected == provider_state_dict["params"] diff --git a/tests/v3/compatibility_suite/test_v3_message_producer.py b/tests/v3/compatibility_suite/test_v3_message_producer.py index e8087a4dc..936a9dce7 100644 --- a/tests/v3/compatibility_suite/test_v3_message_producer.py +++ b/tests/v3/compatibility_suite/test_v3_message_producer.py @@ -8,7 +8,7 @@ import re import sys from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import pytest from pytest_bdd import ( @@ -19,8 +19,8 @@ from pact.v3.pact import Pact from tests.v3.compatibility_suite.util import ( - parse_horizontal_markdown_table, - parse_markdown_table, + parse_horizontal_table, + parse_vertical_table, ) from tests.v3.compatibility_suite.util.interaction_definition import ( InteractionDefinition, @@ -280,17 +280,15 @@ def test_verifying_multiple_pact_files() -> None: @given( parsers.re( - r'a Pact file for "(?P[^"]+)" is to be verified with the following:\n' - r"(?P
.+)", + r'a Pact file for "(?P[^"]+)" is to be verified with the following:', re.DOTALL, ), - converters={"table": parse_horizontal_markdown_table}, ) def a_pact_file_for_is_to_be_verified_with_the_following( verifier: Verifier, temp_dir: Path, name: str, - table: dict[str, str | dict[str, str]], + datatable: list[list[str]], ) -> None: """ A Pact file for "basic" is to be verified with the following. @@ -298,6 +296,7 @@ def a_pact_file_for_is_to_be_verified_with_the_following( pact = Pact("consumer", "provider") pact.with_specification("V3") + table: dict[str, Any] = parse_vertical_table(datatable) if "metadata" in table: assert isinstance(table["metadata"], str) metadata = { @@ -351,20 +350,19 @@ def a_pact_file_for_is_to_be_verified_with_provider_state( @given( parsers.re( r'a Pact file for "(?P[^"]+)":"(?P[^"]+)" is ' - "to be verified with the following metadata:\n" - r"(?P.+)", + "to be verified with the following metadata:", re.DOTALL, ), - converters={"metadata": parse_markdown_table}, ) def a_pact_file_for_is_to_be_verified_with_the_following_metadata( temp_dir: Path, verifier: Verifier, name: str, fixture: str, - metadata: list[dict[str, str]], + datatable: list[list[str]], ) -> None: """A Pact file is to be verified with the following metadata.""" + metadata = parse_horizontal_table(datatable) pact = Pact("consumer", "provider") pact.with_specification("V3") interaction_definition = InteractionDefinition( @@ -387,21 +385,20 @@ def a_pact_file_for_is_to_be_verified_with_the_following_metadata( @given( parsers.re( r'a provider is started that can generate the "(?P[^"]+)" ' - r'message with "(?P[^"]+)" and the following metadata:\n' - r"(?P.+)", + r'message with "(?P[^"]+)" and the following metadata:', re.DOTALL, ), - converters={"metadata": parse_markdown_table}, target_fixture="provider_url", ) def a_provider_is_started_that_can_generate_the_message_with_the_following_metadata( temp_dir: Path, name: str, fixture: str, - metadata: list[dict[str, str]], + datatable: list[list[str]], ) -> Generator[URL, None, None]: """A provider is started that can generate the message with the following metadata.""" # noqa: E501 interaction_definitions: list[InteractionDefinition] = [] + metadata = parse_horizontal_table(datatable) if (temp_dir / "interactions.pkl").exists(): with (temp_dir / "interactions.pkl").open("rb") as pkl_file: interaction_definitions = pickle.load(pkl_file) # noqa: S301 diff --git a/tests/v3/compatibility_suite/test_v3_provider.py b/tests/v3/compatibility_suite/test_v3_provider.py index b3f6442ea..c4176a71b 100644 --- a/tests/v3/compatibility_suite/test_v3_provider.py +++ b/tests/v3/compatibility_suite/test_v3_provider.py @@ -10,7 +10,7 @@ import pytest from pytest_bdd import given, parsers, scenario -from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util import parse_horizontal_table from tests.v3.compatibility_suite.util.interaction_definition import ( InteractionDefinition, ) @@ -67,12 +67,11 @@ def test_verifying_an_interaction_with_a_provider_state_with_parameters() -> Non @given( - parsers.parse("the following HTTP interactions have been defined:\n{content}"), + parsers.parse("the following HTTP interactions have been defined:"), target_fixture="interaction_definitions", - converters={"content": parse_markdown_table}, ) def the_following_http_interactions_have_been_defined( - content: list[dict[str, str]], + datatable: list[list[str]], ) -> dict[int, InteractionDefinition]: """ Parse the HTTP interactions table into a dictionary. @@ -94,12 +93,13 @@ def the_following_http_interactions_have_been_defined( logger.debug("Parsing interaction definitions") # Check that the table is well-formed - assert len(content[0]) == 8, f"Expected 8 columns, got {len(content[0])}" - assert "No" in content[0], "'No' column not found" + definitions = parse_horizontal_table(datatable) + assert len(definitions[0]) == 8, f"Expected 8 columns, got {len(definitions[0])}" + assert "No" in definitions[0], "'No' column not found" # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} - for row in content: + for row in definitions: interactions[int(row["No"])] = InteractionDefinition(**row) # type: ignore[arg-type] return interactions diff --git a/tests/v3/compatibility_suite/test_v4_provider.py b/tests/v3/compatibility_suite/test_v4_provider.py index 5f3ba4482..118659a49 100644 --- a/tests/v3/compatibility_suite/test_v4_provider.py +++ b/tests/v3/compatibility_suite/test_v4_provider.py @@ -10,7 +10,7 @@ import pytest from pytest_bdd import given, parsers, scenario -from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util import parse_horizontal_table from tests.v3.compatibility_suite.util.interaction_definition import ( InteractionDefinition, ) @@ -69,12 +69,11 @@ def test_verifying_a_http_interaction_with_comments() -> None: @given( - parsers.parse("the following HTTP interactions have been defined:\n{content}"), + parsers.parse("the following HTTP interactions have been defined:"), target_fixture="interaction_definitions", - converters={"content": parse_markdown_table}, ) def the_following_http_interactions_have_been_defined( - content: list[dict[str, str]], + datatable: list[list[str]], ) -> dict[int, InteractionDefinition]: """ Parse the HTTP interactions table into a dictionary. @@ -98,12 +97,13 @@ def the_following_http_interactions_have_been_defined( logger.debug("Parsing interaction definitions") # Check that the table is well-formed - assert len(content[0]) == 10, f"Expected 10 columns, got {len(content[0])}" - assert "No" in content[0], "'No' column not found" + definitions = parse_horizontal_table(datatable) + assert len(definitions[0]) == 10, f"Expected 10 columns, got {len(definitions[0])}" + assert "No" in definitions[0], "'No' column not found" # Parse the table into a more useful format interactions: dict[int, InteractionDefinition] = {} - for row in content: + for row in definitions: interactions[int(row["No"])] = InteractionDefinition(**row) # type: ignore[arg-type] return interactions diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index c2ba356e3..d07bffb88 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -150,9 +150,9 @@ def truncate(data: str | bytes) -> str: ) -def parse_markdown_table(content: str) -> list[dict[str, str]]: +def parse_horizontal_table(content: list[list[str]]) -> list[dict[str, str]]: """ - Parse a Markdown table into a list of dictionaries. + Parse a table into a list of dictionaries. The table is expected to be in the following format: @@ -161,25 +161,28 @@ def parse_markdown_table(content: str) -> list[dict[str, str]]: | val1 | val2 | val3 | ``` - Note that the first row is expected to be the column headers, and the - remaining rows are the values. There is no header/body separation. + The parsing of the Markdown table into a list of lists is first done by + the `pytest-bdd` library. This function then converts this into a list of + dictionaries. + + Args: + content: + The table contents as parsed by `pytest-bdd`. + + Returns: + A list of dictionaries, where each dictionary represents a row in the + table. """ - rows = [ - list(map(str.strip, row.split("|")))[1:-1] - for row in content.split("\n") - if row.strip() - ] - - if len(rows) < 2: - msg = f"Expected at least two rows in the table, got {len(rows)}" + if len(content) < 2: + msg = f"Expected at least two rows in the table, got {len(content)}" raise ValueError(msg) - return [dict(zip(rows[0], row)) for row in rows[1:]] + return [dict(zip(content[0], row)) for row in content[1:]] -def parse_horizontal_markdown_table(content: str) -> dict[str, str]: +def parse_vertical_table(content: list[list[str]]) -> dict[str, str]: """ - Parse a Markdown table into a list of dictionaries. + Parse a table into a single dictionary. The table is expected to be in the following format: @@ -188,18 +191,23 @@ def parse_horizontal_markdown_table(content: str) -> dict[str, str]: | key2 | val2 | | key3 | val3 | ``` + + The parsing of the Markdown table into a list of lists is first done by + the `pytest-bdd` library. This function then converts this into a single + dictionary for easier access. + + Args: + content: + The table contents as parsed by `pytest-bdd`. + + Returns: + A dictionary, where each key is a column in the table """ - rows = [ - list(map(str.strip, row.split("|")))[1:-1] - for row in content.split("\n") - if row.strip() - ] - - if len(rows[0]) > 2: - msg = f"Expected at most two columns in the table, got {len(rows[0])}" + if len(content[0]) != 2: + msg = f"Expected exactly two columns in the table, got {len(content[0])}" raise ValueError(msg) - return {row[0]: row[1] for row in rows} + return {row[0]: row[1] for row in content} def serialize(obj: Any) -> Any: # noqa: ANN401, PLR0911 diff --git a/tests/v3/compatibility_suite/util/consumer.py b/tests/v3/compatibility_suite/util/consumer.py index 811a1124e..dfe73c0d8 100644 --- a/tests/v3/compatibility_suite/util/consumer.py +++ b/tests/v3/compatibility_suite/util/consumer.py @@ -32,7 +32,7 @@ from tests.v3.compatibility_suite.util import ( FIXTURES_ROOT, PactInteractionTuple, - parse_markdown_table, + parse_horizontal_table, string_to_int, truncate, ) @@ -177,23 +177,24 @@ def the_mock_server_is_started_with_interaction_n_but_with_the_following_changes parsers.re( r"the mock server is started" r" with interaction (?P\d+)" - r" but with the following changes?:\n(?P.*)", + r" but with the following changes?:", re.DOTALL, ), - converters={"iid": int, "content": parse_markdown_table}, + converters={"iid": int}, target_fixture="srv", stacklevel=stacklevel + 1, ) def _( iid: int, interaction_definitions: dict[int, InteractionDefinition], - content: list[dict[str, str]], + datatable: list[list[str]], ) -> Generator[PactServer, Any, None]: """The mock server is started with interactions.""" pact = Pact("consumer", "provider") pact.with_specification(version) definition = interaction_definitions[iid] - definition.update(**content[0]) # type: ignore[arg-type] + changes = parse_horizontal_table(datatable) + definition.update(**changes[0]) # type: ignore[arg-type] logger.info("Adding modified interaction %s", iid) definition.add_to_pact(pact, f"interaction {iid}") @@ -249,17 +250,17 @@ def request_n_is_made_to_the_mock_server_with_the_following_changes( @when( parsers.re( r"request (?P\d+) is made to the mock server" - r" with the following changes?:\n(?P.*)", + r" with the following changes?:", re.DOTALL, ), - converters={"request_id": int, "content": parse_markdown_table}, + converters={"request_id": int}, target_fixture="response", stacklevel=stacklevel + 1, ) def _( interaction_definitions: dict[int, InteractionDefinition], request_id: int, - content: list[dict[str, str]], + datatable: list[list[str]], srv: PactServer, ) -> requests.Response: """ @@ -269,8 +270,9 @@ def _( definition (as in the given step). """ definition = interaction_definitions[request_id] - assert len(content) == 1, "Expected exactly one row in the table" - definition.update(**content[0]) # type: ignore[arg-type] + changes = parse_horizontal_table(datatable) + assert len(changes) == 1, "Expected exactly one row in the table" + definition.update(**changes[0]) # type: ignore[arg-type] if ( definition.body diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index d86bcbbb8..ab9e3b437 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -50,7 +50,7 @@ from pact.v3.pact import Pact from tests.v3.compatibility_suite.util import ( parse_headers, - parse_markdown_table, + parse_horizontal_table, serialize, truncate, ) @@ -573,12 +573,11 @@ def a_provider_is_started_that_returns_the_responses_from_interactions_with_chan parsers.re( r"a provider is started that returns the responses?" r' from interactions? "?(?P[0-9, ]+)"?' - r" with the following changes:\n(?P.+)", + r" with the following changes:", re.DOTALL, ), converters={ "interactions": lambda x: [int(i) for i in x.split(",") if i], - "changes": parse_markdown_table, }, target_fixture="provider_url", stacklevel=stacklevel + 1, @@ -586,13 +585,14 @@ def a_provider_is_started_that_returns_the_responses_from_interactions_with_chan def _( interaction_definitions: dict[int, InteractionDefinition], interactions: list[int], - changes: list[dict[str, str]], + datatable: list[list[str]], temp_dir: Path, ) -> Generator[URL, None, None]: """ Start a provider that returns the responses from the given interactions. """ logger.debug("Starting provider for modified interactions %s", interactions) + changes = parse_horizontal_table(datatable) assert len(changes) == 1, "Only one set of changes is supported" defns: list[InteractionDefinition] = [] @@ -810,17 +810,17 @@ def a_pact_file_for_interaction_is_to_be_verified_with_comments( @given( parsers.re( r"a Pact file for interaction (?P\d+) is to be verified" - r" with the following comments:\n(?P.+)", + r" with the following comments:", re.DOTALL, ), - converters={"interaction": int, "comments": parse_markdown_table}, + converters={"interaction": int}, stacklevel=stacklevel + 1, ) def _( interaction_definitions: dict[int, InteractionDefinition], verifier: Verifier, interaction: int, - comments: list[dict[str, str]], + datatable: list[list[str]], temp_dir: Path, ) -> None: """ @@ -831,7 +831,7 @@ def _( interaction, interaction_definitions[interaction], ) - + comments = parse_horizontal_table(datatable) defn = interaction_definitions[interaction] for comment in comments: if comment["type"] == "text": @@ -865,10 +865,9 @@ def a_pact_file_for_message_is_to_be_verified_with_comments( @given( parsers.re( r'a Pact file for "(?P[^"]+)":"(?P[^"]+)" is to be verified' - r" with the following comments:\n(?P.+)", + r" with the following comments:", re.DOTALL, ), - converters={"comments": parse_markdown_table}, stacklevel=stacklevel + 1, ) def _( @@ -876,13 +875,14 @@ def _( temp_dir: Path, name: str, fixture: str, - comments: list[dict[str, str]], + datatable: list[list[str]], ) -> None: defn = InteractionDefinition( type="Async", description=name, body=fixture, ) + comments = parse_horizontal_table(datatable) for comment in comments: if comment["type"] == "text": defn.text_comments.append(comment["comment"]) @@ -1047,22 +1047,23 @@ def a_pact_file_for_interaction_is_to_be_verified_with_a_provider_states_defined @given( parsers.re( r"a Pact file for interaction (?P\d+) is to be verified" - r" with the following provider states defined:\n(?P.+)", + r" with the following provider states defined:", re.DOTALL, ), - converters={"interaction": int, "states": parse_markdown_table}, + converters={"interaction": int}, stacklevel=stacklevel + 1, ) def _( interaction_definitions: dict[int, InteractionDefinition], verifier: Verifier, interaction: int, - states: list[dict[str, Any]], + datatable: list[list[str]], temp_dir: Path, ) -> None: """ Verify the Pact file for the given interaction with provider states defined. """ + states = parse_horizontal_table(datatable) logger.debug( "Adding interaction %d to be verified with provider states %s", interaction, @@ -1091,14 +1092,11 @@ def a_request_filter_is_configured_to_make_the_following_changes( stacklevel: int = 1, ) -> None: @given( - parsers.parse( - "a request filter is configured to make the following changes:\n{content}" - ), - converters={"content": parse_markdown_table}, + parsers.parse("a request filter is configured to make the following changes:"), stacklevel=stacklevel + 1, ) def _( - content: list[dict[str, str]], + datatable: list[list[str]], verifier: Verifier, ) -> None: """ @@ -1106,8 +1104,9 @@ def _( """ logger.debug("Configuring request filter") - if "headers" in content[0]: - verifier.add_custom_headers(parse_headers(content[0]["headers"]).items()) + changes = parse_horizontal_table(datatable) + if "headers" in changes[0]: + verifier.add_custom_headers(parse_headers(changes[0]["headers"]).items()) else: msg = "Unsupported filter type" raise RuntimeError(msg) @@ -1376,23 +1375,23 @@ def the_provider_state_callback_will_receive_a_setup_call_with_parameters( r"the provider state callback" r" will receive a (?Psetup|teardown) call" r' (with )?"(?P[^"]*)"' - r" and the following parameters:\n(?P.+)", + r" and the following parameters:", re.DOTALL, ), - converters={"parameters": parse_markdown_table}, stacklevel=stacklevel + 1, ) def _( temp_dir: Path, action: str, state: str, - parameters: list[dict[str, str]], + datatable: list[list[str]], ) -> None: """ Check that the provider state callback received a setup call. """ logger.info("Checking provider state callback received a %s call", action) logger.info("Callback files: %s", list(temp_dir.glob("callback.*.json"))) + parameters = parse_horizontal_table(datatable) params: dict[str, str] = parameters[0] # If we have a string that looks quoted, unquote it for key, value in params.items(): From 65da2af234ddcc11ec4f9bf2c370fb7b17a66475 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 26 Nov 2024 10:53:49 +1100 Subject: [PATCH 0620/1376] style: lint Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 8 ++++---- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12be451c8..673c69ad4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | @@ -126,7 +126,7 @@ jobs: echo "MACOSX_DEPLOYMENT_TARGET=10.12" >> "$GITHUB_ENV" - name: Create wheels - uses: pypa/cibuildwheel@ee63bf16da6cddfb925f542f2c7b59ad50e93969 # v2.22.0 + uses: pypa/cibuildwheel@ee63bf16da6cddfb925f542f2c7b59ad50e93969 # v2.22.0 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} @@ -189,7 +189,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@ee63bf16da6cddfb925f542f2c7b59ad50e93969 # v2.22.0 + uses: pypa/cibuildwheel@ee63bf16da6cddfb925f542f2c7b59ad50e93969 # v2.22.0 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} @@ -238,7 +238,7 @@ jobs: merge-multiple: true - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 88097eb5b..1cb3f68bc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8ff46c687..094140157 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | @@ -170,7 +170,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | @@ -209,7 +209,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | @@ -253,7 +253,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | @@ -278,7 +278,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | @@ -303,7 +303,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 with: enable-cache: true cache-dependency-glob: | From 62ce57363e357d23dff4d91f6af93c92ca5e261c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:56:10 +0000 Subject: [PATCH 0621/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.28.0 --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 094140157..2828edf27 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -328,7 +328,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@b74202f74b4346efdbce7801d187ec57b266bac8 # v1.27.3 + uses: crate-ci/typos@78d6d2274460eb93ea511a10ce9f67d72f014f35 # v1.28.0 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3def9372f..f7321a02c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,7 +72,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.27.3 + rev: v1.28.0 hooks: - id: typos From 6f8db314bc20a961454b6d5c269a0d8149a0fa35 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 26 Nov 2024 11:41:21 +1100 Subject: [PATCH 0622/1376] style: lint --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2828edf27..3bf508116 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -328,7 +328,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@78d6d2274460eb93ea511a10ce9f67d72f014f35 # v1.28.0 + uses: crate-ci/typos@78d6d2274460eb93ea511a10ce9f67d72f014f35 # v1.28.0 pre-commit: name: Pre-commit From af5a5beb1a7d3c20bb3c2459870e22c5818a4417 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 10:59:51 +0000 Subject: [PATCH 0623/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.28.1 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: JP-Ellis --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3bf508116..ef7342b42 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -328,7 +328,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@78d6d2274460eb93ea511a10ce9f67d72f014f35 # v1.28.0 + uses: crate-ci/typos@bd36f89fcd3424dcefd442894589e6ee572a59f2 # v1.28.1 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7321a02c..7b8823b19 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,7 +72,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.28.0 + rev: v1.28.1 hooks: - id: typos From 0fa56c90376cdbdbf96f232c9078a5448fc50980 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 28 Nov 2024 10:36:55 +1100 Subject: [PATCH 0624/1376] chore(ci): replace pre-commit/action Replacing with a manual installation and run of pre-commit, and `pre-commit/action`. Ref: pre-commit/action#218 Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef7342b42..1ec121822 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -335,8 +335,30 @@ jobs: runs-on: ubuntu-latest + env: + PRE_COMMIT_HOME: ${{ github.workspace }}/.pre-commit + steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Cache pre-commit + uses: actions/cache@v4 + with: + path: | + ${{ env.PRE_COMMIT_HOME }} + key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + ${{ runner.os }}-pre-commit- + + - name: Set up uv + uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + with: + enable-cache: true + cache-suffix: pre-commit + cache-dependency-glob: '' + + - name: Install pre-commit + run: uv tool install pre-commit + - name: Run pre-commit - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 + run: pre-commit run --show-diff-on-failure --color=always --all-files From b5d5b030db3ba60107dab0f4d567964f765f04de Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 00:11:09 +0000 Subject: [PATCH 0625/1376] chore(deps): pin actions/cache action to 6849a64 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: JP-Ellis --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ec121822..190637256 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -342,7 +342,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Cache pre-commit - uses: actions/cache@v4 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: | ${{ env.PRE_COMMIT_HOME }} From 1db2de149694a6796ca8bd80fb921c61dc171880 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Nov 2024 14:25:37 +1100 Subject: [PATCH 0626/1376] chore(v3)!: remove unnecessary underscores The arguments to `__exit__` are not used. When first implemented, this used the leading underscores to make that explicit; however, this is incompatible with the expected type signature of the function. BREAKING CHANGE: The PactServer `__exit__` arguments no longer have leading underscores. This is typically handled by Python itself and therefore is unlikely to be a change for any user, unless the end user was calling the `__exit__` method explicitly _and_ using keyword arguments. Signed-off-by: JP-Ellis --- src/pact/v3/pact.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index 9e69c8298..2d11738cd 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -744,9 +744,9 @@ def __enter__(self) -> Self: def __exit__( self, - _exc_type: type[BaseException] | None, - _exc_value: BaseException | None, - _traceback: TracebackType | None, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> None: """ Stop the server. From 6d39844cbe129214a32f4001f879607f4c5aecc0 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Nov 2024 13:41:32 +1100 Subject: [PATCH 0627/1376] chore(v3): add _find_free_port utility Within the `pact.v3.util` module, a _find_free_port utility has been added. It is used within tests, and more importantly, it is used to find a free port for the Pact server. As part of this change, "find a port for me" option is denoted using `None` as opposed to `0`. The value of 0 will still allocate a random port, though this is done using the Pact Core Library as opposed to the higher leve Python library. This should _not_ be a breaking change for anyone as: 1. If someone explicitly used the default value of `0`, the behaviour remains unchanged (that is, port is allocated by the Pact Core library). 2. If the default is implicitly provided, a random port is still being allocated, albeit now by the Python library as opposed to the Rust core library. Signed-off-by: JP-Ellis --- examples/tests/v3/provider_server.py | 23 ++++--------------- src/pact/v3/pact.py | 15 ++++++------ src/pact/v3/util.py | 19 +++++++++++++++ tests/v3/compatibility_suite/util/provider.py | 21 ++--------------- tests/v3/test_pact.py | 1 + 5 files changed, 34 insertions(+), 45 deletions(-) diff --git a/examples/tests/v3/provider_server.py b/examples/tests/v3/provider_server.py index 9b42ec4e7..91a4598b2 100644 --- a/examples/tests/v3/provider_server.py +++ b/examples/tests/v3/provider_server.py @@ -7,11 +7,10 @@ import logging import re import signal -import socket import subprocess import sys import time -from contextlib import closing, contextmanager +from contextlib import contextmanager from importlib import import_module from pathlib import Path from threading import Thread @@ -19,6 +18,8 @@ import requests +from pact.v3.util import _find_free_port + sys.path.append(str(Path(__file__).parent.parent.parent.parent)) from yarl import URL @@ -115,27 +116,11 @@ def set_provider_state() -> tuple[str, int]: self.state_provider_function(flask.request.args["state"]) return "Provider state set", 200 - def _find_free_port(self) -> int: - """ - Find a free port. - - This is used to find a free port to host the API on when running locally. It - is allocated, and then released immediately so that it can be used by the - API. - - Returns: - The port number. - """ - with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: - s.bind(("", 0)) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - return s.getsockname()[1] - def run(self) -> None: """ Start the provider. """ - url = URL(f"http://localhost:{self._find_free_port()}") + url = URL(f"http://localhost:{_find_free_port()}") sys.stderr.write(f"Starting provider on {url}\n") self.app.run( diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index 2d11738cd..9ebf5555d 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -84,6 +84,7 @@ from pact.v3.interaction._async_message_interaction import AsyncMessageInteraction from pact.v3.interaction._http_interaction import HttpInteraction from pact.v3.interaction._sync_message_interaction import SyncMessageInteraction +from pact.v3.util import _find_free_port if TYPE_CHECKING: from collections.abc import Generator @@ -565,7 +566,7 @@ def __init__( # noqa: PLR0913 self, pact_handle: pact.v3.ffi.PactHandle, host: str = "localhost", - port: int = 0, + port: int | None = None, transport: str = "HTTP", transport_config: str | None = None, *, @@ -583,8 +584,8 @@ def __init__( # noqa: PLR0913 Hostname of IP for the mock server. port: - Port to bind the mock server to. The value of `0` will select a - random available port. + Port to bind the mock server to. The value of `None` will select + a random available port. transport: Transport to use for the mock server. @@ -602,7 +603,7 @@ def __init__( # noqa: PLR0913 independently of `raises`. """ self._host = host - self._port = port + self._port = port or _find_free_port() self._transport = transport self._transport_config = transport_config self._pact_handle = pact_handle @@ -611,16 +612,16 @@ def __init__( # noqa: PLR0913 self._verbose = verbose @property - def port(self) -> int: + def port(self) -> int | None: """ Port on which the server is running. - If the server is not running, then this will be `0`. + If the server is not running, then this will be `None`. """ # Unlike the other properties, this value might be different to what was # passed in to the constructor as the server can be started on a random # port. - return self._handle.port if self._handle else 0 + return self._handle.port if self._handle else None @property def host(self) -> str: diff --git a/src/pact/v3/util.py b/src/pact/v3/util.py index 64a178313..c0a023d38 100644 --- a/src/pact/v3/util.py +++ b/src/pact/v3/util.py @@ -7,7 +7,9 @@ reference. """ +import socket import warnings +from contextlib import closing _PYTHON_FORMAT_TO_JAVA_DATETIME = { "a": "EEE", @@ -140,3 +142,20 @@ def _format_code_to_java_format(code: str) -> str: msg = f"Unsupported Python format code `%{code}`" raise ValueError(msg) + + +def _find_free_port() -> int: + """ + Find a free port. + + This is used to find a free port to host the API on when running locally. It + is allocated, and then released immediately so that it can be used by the + API. + + Returns: + The port number. + """ + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + s.bind(("", 0)) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return s.getsockname()[1] diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index ab9e3b437..74f94102b 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -19,10 +19,11 @@ import pytest +from pact.v3.util import _find_free_port + sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent)) -import contextlib import copy import json import logging @@ -31,7 +32,6 @@ import re import shutil import signal -import socket import subprocess import time import warnings @@ -107,23 +107,6 @@ def next_version() -> str: return version -def _find_free_port() -> int: - """ - Find a free port. - - This is used to find a free port to host the API on when running locally. It - is allocated, and then released immediately so that it can be used by the - API. - - Returns: - The port number. - """ - with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: - s.bind(("", 0)) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - return s.getsockname()[1] - - def _setup_logging(log_level: int) -> None: """ Set up logging for the provider. diff --git a/tests/v3/test_pact.py b/tests/v3/test_pact.py index 5928e2bb5..c40de3188 100644 --- a/tests/v3/test_pact.py +++ b/tests/v3/test_pact.py @@ -44,6 +44,7 @@ def test_empty_provider() -> None: def test_serve(pact: Pact) -> None: with pact.serve() as srv: + assert srv.port is not None assert srv.port > 0 assert srv.host == "localhost" assert str(srv).startswith("http://localhost") From 5162c80af84932e8f7d3a0cb63b06e5e167d3ca1 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Nov 2024 13:43:24 +1100 Subject: [PATCH 0628/1376] chore(v3)!: make util module private The `pact.v3.util` module was created as a public module, but serves no purpose to end-users of Pact Python. This commit renames it and marks it as private. If anyone was relying on functions from this module, they are still accessible within the (now private) `pact.v3._util` module. BREAKING CHANGE: `pact.v3.util` has been renamed to `pact.v3._util` and is now private. Signed-off-by: JP-Ellis --- examples/tests/v3/provider_server.py | 4 ++-- src/pact/v3/{util.py => _util.py} | 10 +++++----- src/pact/v3/generate/__init__.py | 2 +- src/pact/v3/match/__init__.py | 2 +- src/pact/v3/pact.py | 4 ++-- tests/v3/compatibility_suite/util/provider.py | 4 ++-- tests/v3/test_util.py | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) rename src/pact/v3/{util.py => _util.py} (95%) diff --git a/examples/tests/v3/provider_server.py b/examples/tests/v3/provider_server.py index 91a4598b2..20f1637ca 100644 --- a/examples/tests/v3/provider_server.py +++ b/examples/tests/v3/provider_server.py @@ -18,7 +18,7 @@ import requests -from pact.v3.util import _find_free_port +from pact.v3._util import find_free_port sys.path.append(str(Path(__file__).parent.parent.parent.parent)) @@ -120,7 +120,7 @@ def run(self) -> None: """ Start the provider. """ - url = URL(f"http://localhost:{_find_free_port()}") + url = URL(f"http://localhost:{find_free_port()}") sys.stderr.write(f"Starting provider on {url}\n") self.app.run( diff --git a/src/pact/v3/util.py b/src/pact/v3/_util.py similarity index 95% rename from src/pact/v3/util.py rename to src/pact/v3/_util.py index c0a023d38..49bc3e791 100644 --- a/src/pact/v3/util.py +++ b/src/pact/v3/_util.py @@ -3,8 +3,8 @@ This module defines a number of utility functions that are used in specific contexts within the Pact library. These functions are not intended to be -used directly by consumers of the library, but are still made available for -reference. +used directly by consumers of the library and as such, may change without +notice. """ import socket @@ -84,7 +84,7 @@ def strftime_to_simple_date_format(python_format: str) -> str: if escaped: result += "'" escaped = False - result += _format_code_to_java_format(c) + result += format_code_to_java_format(c) # Increment another time to skip the second character of the # Python format code. idx += 1 @@ -108,7 +108,7 @@ def strftime_to_simple_date_format(python_format: str) -> str: return result -def _format_code_to_java_format(code: str) -> str: +def format_code_to_java_format(code: str) -> str: """ Convert a single Python format code to a Java SimpleDateFormat format. @@ -144,7 +144,7 @@ def _format_code_to_java_format(code: str) -> str: raise ValueError(msg) -def _find_free_port() -> int: +def find_free_port() -> int: """ Find a free port. diff --git a/src/pact/v3/generate/__init__.py b/src/pact/v3/generate/__init__.py index f961ac261..e2678f0a8 100644 --- a/src/pact/v3/generate/__init__.py +++ b/src/pact/v3/generate/__init__.py @@ -8,11 +8,11 @@ import warnings from typing import TYPE_CHECKING, Literal +from pact.v3._util import strftime_to_simple_date_format from pact.v3.generate.generator import ( Generator, GenericGenerator, ) -from pact.v3.util import strftime_to_simple_date_format if TYPE_CHECKING: from collections.abc import Mapping, Sequence diff --git a/src/pact/v3/match/__init__.py b/src/pact/v3/match/__init__.py index 2e1cca35b..afbd6507d 100644 --- a/src/pact/v3/match/__init__.py +++ b/src/pact/v3/match/__init__.py @@ -51,6 +51,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload from pact.v3 import generate +from pact.v3._util import strftime_to_simple_date_format from pact.v3.match.matcher import ( ArrayContainsMatcher, EachKeyMatcher, @@ -59,7 +60,6 @@ Matcher, ) from pact.v3.types import UNSET, Matchable, Unset -from pact.v3.util import strftime_to_simple_date_format if TYPE_CHECKING: from collections.abc import Mapping, Sequence diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index 9ebf5555d..6f9917f33 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -75,6 +75,7 @@ from yarl import URL import pact.v3.ffi +from pact.v3._util import find_free_port from pact.v3.error import ( InteractionVerificationError, Mismatch, @@ -84,7 +85,6 @@ from pact.v3.interaction._async_message_interaction import AsyncMessageInteraction from pact.v3.interaction._http_interaction import HttpInteraction from pact.v3.interaction._sync_message_interaction import SyncMessageInteraction -from pact.v3.util import _find_free_port if TYPE_CHECKING: from collections.abc import Generator @@ -603,7 +603,7 @@ def __init__( # noqa: PLR0913 independently of `raises`. """ self._host = host - self._port = port or _find_free_port() + self._port = port or find_free_port() self._transport = transport self._transport_config = transport_config self._pact_handle = pact_handle diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 74f94102b..99f389f38 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -19,7 +19,7 @@ import pytest -from pact.v3.util import _find_free_port +from pact.v3._util import find_free_port sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent)) @@ -307,7 +307,7 @@ def run(self) -> None: """ Start the provider. """ - url = URL(f"http://localhost:{_find_free_port()}") + url = URL(f"http://localhost:{find_free_port()}") sys.stderr.write(f"Starting provider on {url}\n") for endpoint in self.app.url_map.iter_rules(): sys.stderr.write(f" * {endpoint}\n") diff --git a/tests/v3/test_util.py b/tests/v3/test_util.py index 65f915f83..19f2356db 100644 --- a/tests/v3/test_util.py +++ b/tests/v3/test_util.py @@ -4,7 +4,7 @@ import pytest -from pact.v3.util import strftime_to_simple_date_format +from pact.v3._util import strftime_to_simple_date_format def test_convert_python_to_java_datetime_format_basic() -> None: From 0145f206b2fa65c3ac197aae2b517b393106f62c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 06:28:36 +0000 Subject: [PATCH 0629/1376] chore(deps): update ruff to v0.8.1 --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7b8823b19..b5f19fd8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: - '@biomejs/biome@1.9.4' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.0 + rev: v0.8.1 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 1c63c9c7a..e7173c9fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ devel-test = [ "pytest-cov ~=6.0", "testcontainers ~=4.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.0"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.1"] ################################################################################ ## Hatch Build Configuration From bdde3ead49cbae8e155a7b91f4dc2fc442741296 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:32:35 +0000 Subject: [PATCH 0630/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.28.2 --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 190637256..af5ce2dbd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -328,7 +328,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@bd36f89fcd3424dcefd442894589e6ee572a59f2 # v1.28.1 + uses: crate-ci/typos@2872c382bb9668d4baa5eade234dcbc0048ca2cf # v1.28.2 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5f19fd8c..89380ab7b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,7 +72,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.28.1 + rev: v1.28.2 hooks: - id: typos From 0f18eca82a7b87ec9583723863467dc4c0136ca1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:32:32 +0000 Subject: [PATCH 0631/1376] chore(deps): update pre-commit hook crate-ci/committed to v1.1.2 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 89380ab7b..7f6a7f5de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/crate-ci/committed - rev: v1.1.1 + rev: v1.1.2 hooks: - id: committed From d13ae3d6e0cee2b9707ed4bdadd9c269bf6677c5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 6 Dec 2024 09:29:32 +1100 Subject: [PATCH 0632/1376] chore(ci): upgrade macos-12 to macos-13 Ref: actions/runner-images#10721 Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 673c69ad4..f57f861aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,7 +87,7 @@ jobs: include: - os: ubuntu-20.04 archs: x86_64 - - os: macos-12 + - os: macos-13 archs: x86_64 - os: windows-2019 archs: AMD64 From 6f829d732222618001ef9f4880e28e8cae48fa8d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 23:30:54 +0000 Subject: [PATCH 0633/1376] chore(deps): update astral-sh/setup-uv action to v4.2.0 --- .github/workflows/build.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f57f861aa..04cc64224 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 with: enable-cache: true cache-dependency-glob: | @@ -238,7 +238,7 @@ jobs: merge-multiple: true - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1cb3f68bc..0f872527b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af5ce2dbd..99cf851d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 with: enable-cache: true cache-dependency-glob: | @@ -170,7 +170,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 with: enable-cache: true cache-dependency-glob: | @@ -209,7 +209,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 with: enable-cache: true cache-dependency-glob: | @@ -253,7 +253,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 with: enable-cache: true cache-dependency-glob: | @@ -278,7 +278,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 with: enable-cache: true cache-dependency-glob: | @@ -303,7 +303,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 with: enable-cache: true cache-dependency-glob: | @@ -351,7 +351,7 @@ jobs: ${{ runner.os }}-pre-commit- - name: Set up uv - uses: astral-sh/setup-uv@d8db0a86d3d88f3017a4e6b8a1e2b234e7a0a1b5 # v4.0.0 + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 with: enable-cache: true cache-suffix: pre-commit From e353bdf5fc8659807bf4295abaccebceece47fc9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 23:30:46 +0000 Subject: [PATCH 0634/1376] chore(deps): update ruff to v0.8.2 --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f6a7f5de..fd6325a7b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: - '@biomejs/biome@1.9.4' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.1 + rev: v0.8.2 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index e7173c9fd..637f88206 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ devel-test = [ "pytest-cov ~=6.0", "testcontainers ~=4.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.1"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.2"] ################################################################################ ## Hatch Build Configuration From e5b6bc6d9759259d8e4cb5cfddf5c37860a25b30 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 23:30:50 +0000 Subject: [PATCH 0635/1376] chore(deps): update actions/cache action to v4.2.0 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99cf851d1..ee9d105e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -342,7 +342,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Cache pre-commit - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: | ${{ env.PRE_COMMIT_HOME }} From 812a6492ae898fc76b68681a434dde8d6f9c60ca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 23:30:40 +0000 Subject: [PATCH 0636/1376] chore(deps): update actions/cache digest to 1bd1e32 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 04cc64224..471043b7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Cache pip packages - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4 with: path: ~/.cache/pip key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} @@ -173,7 +173,7 @@ jobs: fetch-depth: 0 - name: Cache pip packages - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4 with: path: ~/.cache/pip key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} From e8cccb82b65e8ab45ceea1e7c71c934d7fb82ad9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 23:30:59 +0000 Subject: [PATCH 0637/1376] chore(deps): update codecov/codecov-action action to v5.1.1 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ee9d105e1..62d4057db 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -126,7 +126,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # v5.0.7 + uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests @@ -239,7 +239,7 @@ jobs: hatch run example --broker-url=http://pactbroker:pactbroker@localhost:9292 - name: Upload coverage - uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # v5.0.7 + uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} flags: examples From e19486d6c404d2829001998c205945d7bba79976 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:28:23 +0000 Subject: [PATCH 0638/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v0.6.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fd6325a7b..9b8d73be0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v0.5.0 # Use the sha / tag you want to point at + rev: v0.6.0 # Use the sha / tag you want to point at hooks: - id: biome-check additional_dependencies: From 8f3795ecc2f82cb6125753614855d97a268b1704 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:36:45 +0000 Subject: [PATCH 0639/1376] chore(deps): update pypa/gh-action-pypi-publish action to v1.12.3 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 471043b7b..b824d61c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -279,7 +279,7 @@ jobs: generate_release_notes: true - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2 + uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3 with: skip-existing: true packages-dir: wheels From 15f61caeb9dfa829b4548895b2deb1c9dda54f82 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 03:40:29 +0000 Subject: [PATCH 0640/1376] chore(deps): update softprops/action-gh-release action to v2.2.0 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b824d61c0..832a7d699 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -270,7 +270,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 + uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0 with: files: wheels/* body_path: ${{ runner.temp }}/changelog From 37524a76f70841378c0c8e6669ceff4fef8f40cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:28:47 +0000 Subject: [PATCH 0641/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.28.3 --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 62d4057db..a3a980cb0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -328,7 +328,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@2872c382bb9668d4baa5eade234dcbc0048ca2cf # v1.28.2 + uses: crate-ci/typos@d1c850b2b5d502763520c25fb4a6a1128ad99bd9 # v1.28.3 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9b8d73be0..c0b7b8e65 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,7 +72,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.28.2 + rev: v1.28.3 hooks: - id: typos From e6371f0c2b8835ee50e749f17b12e5c4cb40e72c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:14:28 +0000 Subject: [PATCH 0642/1376] fix(deps): update ruff to v0.8.3 --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c0b7b8e65..ca61691e8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: - '@biomejs/biome@1.9.4' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.2 + rev: v0.8.3 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 637f88206..4c280421d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ devel-test = [ "pytest-cov ~=6.0", "testcontainers ~=4.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.2"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.3"] ################################################################################ ## Hatch Build Configuration From c90571dc4ee3ae0c85d2fbb8e469f87bdb9cefa8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 22:03:33 +0000 Subject: [PATCH 0643/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.28.4 --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a3a980cb0..faa8388f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -328,7 +328,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@d1c850b2b5d502763520c25fb4a6a1128ad99bd9 # v1.28.3 + uses: crate-ci/typos@9d890159570d5018df91fedfa40b4730cd4a81b1 # v1.28.4 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ca61691e8..e897bfbd1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,7 +72,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.28.3 + rev: v1.28.4 hooks: - id: typos From 6c9a8429140dd2310aaacba95f2689d755850b4d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:08:53 +0000 Subject: [PATCH 0644/1376] chore(deps): update codecov/codecov-action action to v5.1.2 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index faa8388f9..085ecd11e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -126,7 +126,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1 + uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests @@ -239,7 +239,7 @@ jobs: hatch run example --broker-url=http://pactbroker:pactbroker@localhost:9292 - name: Upload coverage - uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1 + uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2 with: token: ${{ secrets.CODECOV_TOKEN }} flags: examples From 42abbd8a410f8b6d7591027cdf3ba0d00404a012 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:05:06 +0000 Subject: [PATCH 0645/1376] chore(deps): update pre-commit hook crate-ci/committed to v1.1.4 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e897bfbd1..08d578bd0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/crate-ci/committed - rev: v1.1.2 + rev: v1.1.4 hooks: - id: committed From fc68f9a374b1729c6579be9203a20fde97e5c306 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:29:39 +0000 Subject: [PATCH 0646/1376] chore(deps): update actions/upload-artifact digest to 6f51ac0 --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 832a7d699..8426d480b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,7 +70,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4 with: name: wheels-sdist path: ./dist/*.tar.* @@ -132,7 +132,7 @@ jobs: CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} - name: Upload wheels - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl @@ -195,7 +195,7 @@ jobs: CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} - name: Upload wheels - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }}-${{ matrix.build }} path: ./wheelhouse/*.whl From c078322bbf72286c85af6a7347454ef73cb490e3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 19 Dec 2024 11:21:34 +1100 Subject: [PATCH 0647/1376] chore(c): specify full action version --- .github/workflows/build.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8426d480b..99ffed132 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,7 +70,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: wheels-sdist path: ./dist/*.tar.* @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Cache pip packages - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: ~/.cache/pip key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} @@ -132,7 +132,7 @@ jobs: CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} - name: Upload wheels - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl @@ -173,7 +173,7 @@ jobs: fetch-depth: 0 - name: Cache pip packages - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: ~/.cache/pip key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} @@ -184,7 +184,7 @@ jobs: - name: Set up QEMU if: startsWith(matrix.os, 'ubuntu-') - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3 + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 with: platforms: arm64 @@ -195,7 +195,7 @@ jobs: CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} - name: Upload wheels - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }}-${{ matrix.build }} path: ./wheelhouse/*.whl @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Download wheels and sdist - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: wheels merge-multiple: true @@ -285,7 +285,7 @@ jobs: packages-dir: wheels - name: Create PR for changelog update - uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7 + uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'chore: update changelog ${{ github.ref_name }}' From 86680950979484c476e234a414450372384ea1bb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 00:47:10 +0000 Subject: [PATCH 0648/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v0.6.1 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 08d578bd0..d14c48384 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v0.6.0 # Use the sha / tag you want to point at + rev: v0.6.1 hooks: - id: biome-check additional_dependencies: From 9366c8de6117943389233db6624efbd36bb2f991 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 04:16:31 +0000 Subject: [PATCH 0649/1376] chore(deps): update pre-commit hook crate-ci/committed to v1.1.5 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d14c48384..0fcb8960b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: exclude: ^(pact|tests)/(?!v3/).*\.py$ - repo: https://github.com/crate-ci/committed - rev: v1.1.4 + rev: v1.1.5 hooks: - id: committed From 5019a6dd22f73c87df608c185365680478e4779c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:25:33 +0000 Subject: [PATCH 0650/1376] fix(deps): update ruff to v0.8.4 --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0fcb8960b..d2369ff7f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: - '@biomejs/biome@1.9.4' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.3 + rev: v0.8.4 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 4c280421d..bd7d49432 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ devel-test = [ "pytest-cov ~=6.0", "testcontainers ~=4.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.3"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.4"] ################################################################################ ## Hatch Build Configuration From dcf0354c47f3ca5a894da2a37720d26cb5c73cc2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:27:05 +0000 Subject: [PATCH 0651/1376] fix(deps): update dependency mypy to v1.14.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd7d49432..e1026c730 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ pact-verifier = "pact.cli.verify:main" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. devel-types = [ - "mypy ==1.13.0", + "mypy ==1.14.0", "types-cffi ~=1.0", "types-requests ~=2.0", ] From 0491048ba1dae97b6d408df048edc31be58c6986 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:32:03 +0000 Subject: [PATCH 0652/1376] chore(deps): update astral-sh/setup-uv action to v5 --- .github/workflows/build.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99ffed132..8f164efd7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 + uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 with: enable-cache: true cache-dependency-glob: | @@ -238,7 +238,7 @@ jobs: merge-multiple: true - name: Set up uv - uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 + uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0f872527b..a28a3ed65 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 + uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 085ecd11e..1f1fa12da 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up uv - uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 + uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 with: enable-cache: true cache-dependency-glob: | @@ -170,7 +170,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up uv - uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 + uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 with: enable-cache: true cache-dependency-glob: | @@ -209,7 +209,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 + uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 with: enable-cache: true cache-dependency-glob: | @@ -253,7 +253,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 + uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 with: enable-cache: true cache-dependency-glob: | @@ -278,7 +278,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 + uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 with: enable-cache: true cache-dependency-glob: | @@ -303,7 +303,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 + uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 with: enable-cache: true cache-dependency-glob: | @@ -351,7 +351,7 @@ jobs: ${{ runner.os }}-pre-commit- - name: Set up uv - uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4.2.0 + uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 with: enable-cache: true cache-suffix: pre-commit From d1d46329abdef24670e702c07b560d1f281921c4 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 13 Dec 2024 12:17:24 +1100 Subject: [PATCH 0653/1376] chore: add pytest-xdist Parallelise the tests so the test suite runs faster. Signed-off-by: JP-Ellis --- examples/conftest.py | 38 ++++++++++++++++++++++++++++++++++++-- pyproject.toml | 9 +++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/examples/conftest.py b/examples/conftest.py index a9c7784f1..b715b60bf 100644 --- a/examples/conftest.py +++ b/examples/conftest.py @@ -12,6 +12,9 @@ from __future__ import annotations +import socket +import sys +import warnings from pathlib import Path from typing import TYPE_CHECKING, Any @@ -20,7 +23,9 @@ from yarl import URL if TYPE_CHECKING: - from collections.abc import Generator + from collections.abc import Generator, Sequence + + import execnet EXAMPLE_DIR = Path(__file__).parent.resolve() @@ -45,6 +50,13 @@ def broker(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: yield URL(broker_url) return + # Check whether port 9292 is already in use. If it is, we assume that the + # broker is already running and return early. + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + if s.connect_ex(("localhost", 9292)) == 0: + yield URL("http://pactbroker:pactbroker@localhost:9292") + return + with DockerCompose( EXAMPLE_DIR, compose_file_name=["docker-compose.yml"], @@ -52,10 +64,32 @@ def broker(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: wait=False, ) as _: yield URL("http://pactbroker:pactbroker@localhost:9292") - return @pytest.fixture(scope="session") def pact_dir() -> Path: """Fixture for the Pact directory.""" return EXAMPLE_DIR / "pacts" + + +def pytest_xdist_setupnodes( + config: pytest.Config, # noqa: ARG001 + specs: Sequence[execnet.XSpec], +) -> None: + """ + Hook to check if the examples are run with multiple workers. + + The examples are designed to run in a specific order, with the consumer + tests running _before_ the provider tests as the provider tests require that + the consumer-generated Pacts are published. + + If multiple xdist workers are detected, a warning is printed to the console. + """ + if len(specs) > 1: + sys.stderr.write("\n") + warnings.warn( + "Running the examples with multiple workers may cause issues. " + "Consider running the examples with a single worker by setting " + "`--numprocesses=1` or using `hatch run example`.", + stacklevel=1, + ) diff --git a/pyproject.toml b/pyproject.toml index e1026c730..16dd46529 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,10 +81,11 @@ devel-test = [ "flask[async] ~=3.0", "httpx ~=0.0", "mock ~=5.0", - "pytest ~=8.0", "pytest-asyncio ~=0.0", "pytest-bdd ~=8.0", "pytest-cov ~=6.0", + "pytest-xdist ~=3.0", + "pytest ~=8.0", "testcontainers ~=4.0", ] devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.4"] @@ -160,7 +161,7 @@ lint = "ruff check --output-format=full --show-fixes {args}" typecheck = "mypy {args:.}" format = "ruff format {args}" test = "pytest tests/ {args}" -example = "pytest examples/ {args}" +example = "pytest --numprocesses=1 examples/ {args}" all = ["format", "lint", "typecheck", "test", "example"] docs = "mkdocs serve {args}" docs-build = "mkdocs build {args}" @@ -183,9 +184,13 @@ pythonpath = "." asyncio_default_fixture_loop_scope = "session" addopts = [ "--import-mode=importlib", + # Coverage options "--cov-config=pyproject.toml", "--cov=pact", "--cov-report=xml", + # Xdist options + "--numprocesses=logical", + "--dist=worksteal", ] filterwarnings = [ "ignore::DeprecationWarning:examples", From a2dfec640e24b668b6301166b27da1f6bc134e32 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 23 Dec 2024 14:30:39 +1100 Subject: [PATCH 0654/1376] chore(ci): remove condition on examples Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1f1fa12da..1536ae213 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -190,7 +190,6 @@ jobs: name: Example runs-on: ubuntu-latest - if: github.event_name == 'push' || ! github.event.pull_request.draft services: broker: From 38d996a12ab1f7491190e761d2612d8507a52fb7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:50:07 +0000 Subject: [PATCH 0655/1376] chore(deps): update astral-sh/setup-uv action to v5.1.0 --- .github/workflows/build.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f164efd7..39dc67e10 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 + uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 with: enable-cache: true cache-dependency-glob: | @@ -238,7 +238,7 @@ jobs: merge-multiple: true - name: Set up uv - uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 + uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a28a3ed65..8a5ccc423 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 + uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1536ae213..e787eaba8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up uv - uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 + uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 with: enable-cache: true cache-dependency-glob: | @@ -170,7 +170,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up uv - uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 + uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 with: enable-cache: true cache-dependency-glob: | @@ -208,7 +208,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 + uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 with: enable-cache: true cache-dependency-glob: | @@ -252,7 +252,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 + uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 with: enable-cache: true cache-dependency-glob: | @@ -277,7 +277,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 + uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 with: enable-cache: true cache-dependency-glob: | @@ -302,7 +302,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 + uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 with: enable-cache: true cache-dependency-glob: | @@ -350,7 +350,7 @@ jobs: ${{ runner.os }}-pre-commit- - name: Set up uv - uses: astral-sh/setup-uv@180f8b44399608a850e1db031fa65c77746566d3 # v5.0.1 + uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 with: enable-cache: true cache-suffix: pre-commit From d76b18397402d6304811bb97a3d08a7807475ada Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:34:31 +0000 Subject: [PATCH 0656/1376] chore(deps): update peter-evans/create-pull-request action to v7.0.6 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39dc67e10..713f6ecf2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -285,7 +285,7 @@ jobs: packages-dir: wheels - name: Create PR for changelog update - uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5 + uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'chore: update changelog ${{ github.ref_name }}' From 3f35067c086ac3f02e6cd2d27ba5f8ec8f1da2c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:50:19 +0000 Subject: [PATCH 0657/1376] chore(deps): update pactfoundation/pact-broker:latest docker digest to a3dac71 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e787eaba8..43bb6c0b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,7 +54,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:05e813c53cbc4d5fadb34a608baaf17348a50c669dc1e3e94dad55e364609912 + image: pactfoundation/pact-broker:latest@sha256:a3dac719e93dceebcacc33323fee19941fa42595d25cc141d0a9d2f46511305c ports: - 9292:9292 env: @@ -193,7 +193,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:05e813c53cbc4d5fadb34a608baaf17348a50c669dc1e3e94dad55e364609912 + image: pactfoundation/pact-broker:latest@sha256:a3dac719e93dceebcacc33323fee19941fa42595d25cc141d0a9d2f46511305c ports: - 9292:9292 env: From 1c1df51661e7d9faba4960dd979b6f5f932016b6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 29 Nov 2024 15:49:03 +1100 Subject: [PATCH 0658/1376] feat(v3): add message relay and callback servers Pact makes use of additional HTTP endpoints to be able to communicate. In particular: - A callback endpoint is required in order to setup/teardown the provider states; and, - HTTP is used as the transport mechanism for message interactions as the specific protocol is abstracted away. This commit adds the foundational HTTP servers for both use cases. Signed-off-by: JP-Ellis --- src/pact/v3/_server.py | 518 ++++++++++++++++++++++++++++++++++++++++ tests/v3/test_server.py | 191 +++++++++++++++ 2 files changed, 709 insertions(+) create mode 100644 src/pact/v3/_server.py create mode 100644 tests/v3/test_server.py diff --git a/src/pact/v3/_server.py b/src/pact/v3/_server.py new file mode 100644 index 000000000..befeccbe0 --- /dev/null +++ b/src/pact/v3/_server.py @@ -0,0 +1,518 @@ +""" +Internal Pact server. + +Pact typically communicates directly with the client/server under test over +HTTP. When testing message interactions, however, Pact abstracts away the +transport layer and instead verifies the message payload and metadata directly. + +Internally, this verification process still requires some form of transport +layer to communication with the underlying Pact Core library. This is where the +Pact server comes in. It is a lightweight HTTP server which translates +communications from the underlying Pact Core library with direct Python function +calls. + +In order to be able to both handle incoming requests, and verify the +interactions, the server is started in a separate thread within the same Python +process. This does have some risks, as the server is not isolated from the rest +of the Python process. This also relies on the requests being made sequentially +and not in parallel, as the server (and more specifically, the verification +process), is _not_ thread-safe. +""" + +from __future__ import annotations + +import base64 +import binascii +import json +import logging +import warnings +from collections.abc import Callable +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from threading import Thread +from typing import TYPE_CHECKING, Any, Generic, Self, TypeAlias, TypeVar +from urllib.parse import parse_qs, urlparse + +from pact import __version__ +from pact.v3._util import find_free_port + +if TYPE_CHECKING: + from types import TracebackType + +logger = logging.getLogger(__name__) + + +_C = TypeVar("_C", bound=Callable[..., Any]) + + +class HandlerHttpServer(ThreadingHTTPServer, Generic[_C]): + """ + A simple HTTP server with an custom handler function. + + Both the message relay and state handler need to be instantiated with a + user-provided function which is accessed during the handling of a request. + As Python's lightweight HTTP server makes the underlying server instance + accessible while processing a request, we can use this to pass the handler + function to the request handler. + """ + + def __init__( + self, + *args: Any, # noqa: ANN401 + handler: _C, + **kwargs: Any, # noqa: ANN401 + ) -> None: + """ + Initialize the HTTP server. + + Args: + handler: + The handler function to call when a request is received. + + *args: + Additional arguments to pass to the server. These are not used + by this class and are passed to the superclass. + + **kwargs: + Additional keyword arguments to pass to the server. These are + not used by this class and are passed to the superclass. + """ + self.handler = handler + super().__init__(*args, **kwargs) + + +################################################################################ +## Message Relay +################################################################################ + + +MessageHandlerCallable: TypeAlias = Callable[ + [bytes | None, dict[str, Any] | None], bytes | None +] + + +class MessageRelay: + """ + Internal message relay server. + + The Pact server is a lightweight HTTP server which translates communications + from the underlying Pact Core library with direct Python function calls. + + The server is responsible for starting and stopping the Pact server, as well + as handling the communication between the server and the underlying Pact + Core library. + """ + + def __init__( + self, + handler: MessageHandlerCallable, + host: str = "localhost", + port: int | None = None, + ) -> None: + """ + Initialize the Pact server. + + Args: + handler: + The handler function to call when a request is received. It must + accept two positional arguments: + + - The body of the request if present as a byte string, or + `None`. + - The metadata of the request if present as a dictionary, or + `None`. + + The handler function must return a byte string response, or + `None`. + + host: + The host to run the server on. + + port: + The port to run the server on. If not provided, a free port will + be found. + """ + self._host = host + self._port = port or find_free_port() + + self._handler = handler + + self._server: HandlerHttpServer[MessageHandlerCallable] | None = None + self._thread: Thread | None = None + + @property + def host(self) -> str: + """ + Server host. + """ + return self._host + + @property + def port(self) -> int: + """ + Server port. + """ + return self._port + + @property + def url(self) -> str: + """ + Server URL. + """ + return f"http://{self.host}:{self.port}" + + def __enter__(self) -> Self: + """ + Enter the Pact message server context. + + This method starts the Pact server in a separate thread to handle the + communication between the server and the underlying Pact Core library. + """ + self._server = HandlerHttpServer( + (self.host, self.port), + MessageRelayHandler, + handler=self._handler, + ) + self._thread = Thread( + target=self._server.serve_forever, + name="Pact Message Relay Server", + ) + self._thread.start() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """ + Exit the Pact message server context. + """ + if not self._thread or not self._server: + warnings.warn( + "Exiting server context despite server not being started.", + stacklevel=2, + ) + return + + self._server.shutdown() + self._thread.join() + + +class MessageRelayHandler(SimpleHTTPRequestHandler): + """ + Request handler for the message relay server. + + The `do_GET` and `do_POST` methods allow the server to handle GET and POST + requests. A new instance of this class is created for each request and + attributes can be inspected to determine the request details and respond + accordingly. + + Specifically, the request details can be found in the following attributes: + + - `self.path` contains the HTTP path of the request. + - `self.headers` contains the HTTP headers of the request. + - `self.rfile` is an input stream containing the body of the request. + + The response can be sent using: + + - `self.send_response(code, message)` to set the response code and message. + - `self.send_header(header, value)` to set a response header. + - `self.end_headers()` to end the headers. + """ + + if TYPE_CHECKING: + server: HandlerHttpServer[MessageHandlerCallable] + + MESSAGE_PATH = "/_pact/message" + + def version_string(self) -> str: + """ + Return the server version string. + + This method is overridden to return a custom server version string. + """ + return f"Pact Python Message Relay/{__version__}" + + def _process(self) -> tuple[bytes | None, dict[str, str] | None]: + """ + Process the request. + + Read the body and headers from the request and perform some common logic + shared between GET and POST requests. + + Returns: + body: + The body of the request as a byte string, if present. + + metadata: + The metadata of the request, if present. + """ + if content_length := self.headers.get("Content-Length"): + body = self.rfile.read(int(content_length)) + else: + body = None + + if data := self.headers.get("Pact-Message-Metadata"): + try: + metadata = json.loads(base64.b64decode(data)) + except binascii.Error as err: + msg = "Unable to base64 decode Pact metadata header." + raise RuntimeError(msg) from err + except json.JSONDecodeError as err: + msg = "Unable to JSON decode Pact metadata header." + raise RuntimeError(msg) from err + else: + return body, metadata + + return body, None + + def do_POST(self) -> None: # noqa: N802 + """ + Handle a POST request. + + This method is called when a POST request is received by the server. + """ + logger.debug( + "Received POST request: %s", + self.path, + extra={"headers": self.headers}, + ) + self.close_connection = True + if self.path != self.MESSAGE_PATH: + self.send_response(404) + self.end_headers() + return + + body, metadata = self._process() + self.send_response(200, "OK") + self.end_headers() + + response = self.server.handler(body, metadata) + if response: + self.wfile.write(response) + + def do_GET(self) -> None: # noqa: N802 + """ + Handle a GET request. + + This method is called when a GET request is received by the server. + """ + logger.debug( + "Received GET request: %s", + self.path, + extra={"headers": self.headers}, + ) + self.close_connection = True + if self.path != self.MESSAGE_PATH: + self.send_response(404) + self.end_headers() + return + + body, metadata = self._process() + response = self.server.handler(body, metadata) + self.send_response(200, "OK") + self.end_headers() + + if response: + self.wfile.write(response) + + +################################################################################ +## State Handler +################################################################################ + + +StateHandlerCallable: TypeAlias = Callable[[str, str, dict[str, Any] | None], None] +""" +State handler function. + +It must accept three positional arguments: + +- The state name, for example "user exists" +- The state action, which is either "setup" or "teardown" +- The metadata of the request if present as a dictionary, or `None`. For + example, `{"user_id": 123}`. +""" + + +class StateCallback: + """ + Internal server for handlng state callbacks. + + The state handler is a lightweight HTTP server which listens for state + change requests from the underlying Pact Core library. It then calls a + user-provided function to handle the setup/teardown of the state. + """ + + def __init__( + self, + handler: StateHandlerCallable, + host: str = "localhost", + port: int | None = None, + ) -> None: + """ + Initialize the state handler. + + Args: + handler: + The handler function to call when a state callback is + received. + + The + + host: + The host to run the server on. + + port: + The port to run the server on. If not provided, a free port will + be found. + """ + self._host = host + self._port = port or find_free_port() + + self._handler = handler + + self._server: HandlerHttpServer[StateHandlerCallable] | None = None + self._thread: Thread | None = None + + @property + def host(self) -> str: + """ + Server host. + """ + return self._host + + @property + def port(self) -> int: + """ + Server port. + """ + return self._port + + @property + def url(self) -> str: + """ + Server URL. + """ + return f"http://{self.host}:{self.port}" + + def __enter__(self) -> Self: + """ + Enter the state handler context. + + This method starts the Pact server in a separate thread to handle the + communication between the server and the underlying Pact Core library. + """ + self._server = HandlerHttpServer( + (self.host, self.port), + StateCallbackHandler, + handler=self._handler, + ) + self._thread = Thread( + target=self._server.serve_forever, + name="Pact Message Relay Server", + ) + self._thread.start() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """ + Exit the state handler context. + """ + if not self._thread or not self._server: + warnings.warn( + "Exiting server context despite server not being started.", + stacklevel=2, + ) + return + + self._server.shutdown() + self._thread.join() + + +class StateCallbackHandler(SimpleHTTPRequestHandler): + """ + Request handler for the state callback server. + + See the docs of [`MessageRelayHandler`](#messagerelayhandler) for more + information on how to handle requests. + """ + + if TYPE_CHECKING: + server: HandlerHttpServer[StateHandlerCallable] + + CALLBACK_PATH = "/_pact/state" + + def version_string(self) -> str: + """ + Return the server version string. + + This method is overridden to return a custom server version string. + """ + return f"Pact Python State Callback/{__version__}" + + def do_POST(self) -> None: # noqa: N802 + """ + Handle a POST request. + + This method is called when a POST request is received by the server. + """ + logger.debug( + "Received POST request: %s", + self.path, + extra={"headers": self.headers}, + ) + self.close_connection = True + url = urlparse(self.path) + if url.path != self.CALLBACK_PATH: + self.send_response(404) + self.end_headers() + return + + if query := url.query: + data: dict[str, Any] = parse_qs(query) + # Convert single-element lists to single values + for k, v in data.items(): + if isinstance(v, list) and len(v) == 1: + data[k] = v[0] + + else: + content_length = self.headers.get("Content-Length") + if not content_length: + self.send_response(400, "Bad Request") + self.end_headers() + return + data = json.loads(self.rfile.read(int(content_length))) + + state = data.pop("state") + action = data.pop("action") + + if not state or not action: + self.send_response(400, "Bad Request") + self.end_headers() + return + + self.server.handler(state, action, data) + self.send_response(200, "OK") + self.end_headers() + + def do_GET(self) -> None: # noqa: N802 + """ + Handle a GET request. + + This method is called when a GET request is received by the server. + """ + logger.debug( + "Received GET request: %s", + self.path, + extra={"headers": self.headers}, + ) + self.close_connection = True + self.send_response(404) + self.end_headers() diff --git a/tests/v3/test_server.py b/tests/v3/test_server.py new file mode 100644 index 000000000..de6f665db --- /dev/null +++ b/tests/v3/test_server.py @@ -0,0 +1,191 @@ +""" +Tests for `pact.v3._server` module. +""" + +import base64 +import json +from unittest.mock import MagicMock + +import aiohttp +import pytest + +from pact.v3._server import MessageRelay, StateCallback + + +def test_relay_default_init() -> None: + handler = MagicMock() + server = MessageRelay(handler) + + assert server.host == "localhost" + assert server.port > 1024 # Random non-privileged port + assert server.url == f"http://{server.host}:{server.port}" + + +@pytest.mark.asyncio +async def test_relay_invalid_path_http() -> None: + handler = MagicMock(return_value="Not OK") + server = MessageRelay(handler) + + with server: + async with aiohttp.ClientSession() as session: + async with session.get(server.url) as response: + assert response.status == 404 + handler.assert_not_called() + + +@pytest.mark.asyncio +async def test_relay_get_http() -> None: + handler = MagicMock(return_value=b"Pact Python is awesome!") + server = MessageRelay(handler) + + with server: + async with aiohttp.ClientSession() as session: + async with session.get(server.url + "/_pact/message") as response: + assert response.status == 200 + assert await response.text() == "Pact Python is awesome!" + + handler.assert_called_once() + assert handler.call_args.args == (None, None) + + +@pytest.mark.asyncio +async def test_relay_post_http() -> None: + handler = MagicMock(return_value=b"Pact Python is awesome!") + server = MessageRelay(handler) + + with server: + async with aiohttp.ClientSession() as session: + async with session.post( + server.url + "/_pact/message", + data='{"hello": "world"}', + ) as response: + assert response.status == 200 + assert await response.text() == "Pact Python is awesome!" + + handler.assert_called_once() + assert handler.call_args.args == (b'{"hello": "world"}', None) + + +@pytest.mark.asyncio +async def test_relay_get_with_metadata() -> None: + handler = MagicMock(return_value=b"Pact Python is awesome!") + server = MessageRelay(handler) + metadata = base64.b64encode(json.dumps({"key": "value"}).encode()).decode() + + with server: + async with aiohttp.ClientSession() as session: + async with session.get( + server.url + "/_pact/message", + headers={"Pact-Message-Metadata": metadata}, + ) as response: + assert response.status == 200 + assert await response.text() == "Pact Python is awesome!" + + handler.assert_called_once() + assert handler.call_args.args == (None, {"key": "value"}) + + +@pytest.mark.asyncio +async def test_relay_post_with_metadata() -> None: + handler = MagicMock(return_value=b"Pact Python is awesome!") + server = MessageRelay(handler) + metadata = base64.b64encode(json.dumps({"key": "value"}).encode()).decode() + + with server: + async with aiohttp.ClientSession() as session: + async with session.post( + server.url + "/_pact/message", + data='{"hello": "world"}', + headers={"Pact-Message-Metadata": metadata}, + ) as response: + assert response.status == 200 + assert await response.text() == "Pact Python is awesome!" + + handler.assert_called_once() + assert handler.call_args.args == (b'{"hello": "world"}', {"key": "value"}) + + +def test_callback_default_init() -> None: + handler = MagicMock() + server = StateCallback(handler) + + assert server.host == "localhost" + assert server.port > 1024 # Random non-privileged port + assert server.url == f"http://{server.host}:{server.port}" + + +@pytest.mark.asyncio +async def test_callback_invalid_http() -> None: + handler = MagicMock(return_value=None) + server = StateCallback(handler) + + with server: + async with aiohttp.ClientSession() as session: + async with session.get(server.url) as response: + assert response.status == 404 + handler.assert_not_called() + + +@pytest.mark.asyncio +async def test_callback_get_http() -> None: + handler = MagicMock(return_value=None) + server = StateCallback(handler) + + with server: + async with aiohttp.ClientSession() as session: + async with session.get(server.url + "/_pact/state") as response: + assert response.status == 404 + + handler.assert_not_called() + + +@pytest.mark.asyncio +async def test_callback_post_query() -> None: + handler = MagicMock(return_value=None) + server = StateCallback(handler) + + with server: + async with aiohttp.ClientSession() as session: + async with session.post( + server.url + "/_pact/state", + params={ + "state": "user exists", + "action": "setup", + "foo": "bar", + "1": 2, + }, + ) as response: + assert response.status == 200 + + handler.assert_called_once() + assert handler.call_args.args == ( + "user exists", + "setup", + {"foo": "bar", "1": "2"}, + ) + + +@pytest.mark.asyncio +async def test_callback_post_body() -> None: + handler = MagicMock(return_value=None) + server = StateCallback(handler) + + with server: + async with aiohttp.ClientSession() as session: + async with session.post( + server.url + "/_pact/state", + json={ + "state": "user exists", + "action": "setup", + "foo": "bar", + "1": 2, + }, + ) as response: + assert response.status == 200 + + handler.assert_called_once() + assert handler.call_args.args == ( + "user exists", + "setup", + {"foo": "bar", "1": 2}, + ) From 461e4f3bfb8f421943ab951360e0d3c58a1a5efb Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 29 Nov 2024 15:50:54 +1100 Subject: [PATCH 0659/1376] feat(v3)!: integrate message relay server With the message relay server added, this commit modifies the verifier to allow for a handler function to be used to handle messages. Note that this does _not_ modify the tests yet (this will be done in another commit). BREAKING CHANGE: The provider name must be given as an argument of the `Verifier` constructor, instead of the first argument of the `set_info` method. BREAKING CHANGE: The `set_info` verifier method is removed, with `add_transport` needing to be used. Signed-off-by: JP-Ellis --- examples/tests/v3/test_01_fastapi_provider.py | 13 +- examples/tests/v3/test_03_message_provider.py | 4 +- examples/tests/v3/test_match.py | 4 +- src/pact/v3/verifier.py | 306 +++++++++++------- tests/v3/compatibility_suite/conftest.py | 2 +- .../test_v3_http_matching.py | 2 +- tests/v3/compatibility_suite/util/provider.py | 2 +- tests/v3/test_verifier.py | 26 +- 8 files changed, 212 insertions(+), 147 deletions(-) diff --git a/examples/tests/v3/test_01_fastapi_provider.py b/examples/tests/v3/test_01_fastapi_provider.py index 1e181a4bb..2f91fbabe 100644 --- a/examples/tests/v3/test_01_fastapi_provider.py +++ b/examples/tests/v3/test_01_fastapi_provider.py @@ -157,11 +157,14 @@ def test_provider() -> None: proc = Process(target=run_server, daemon=True) proc.start() time.sleep(2) - verifier = Verifier().set_info("v3_http_provider", url=PROVIDER_URL) - verifier.add_source("examples/pacts/v3_http_consumer-v3_http_provider.json") - verifier.set_state( - PROVIDER_URL / "_pact" / "callback", - teardown=True, + verifier = ( + Verifier("v3_http_provider") + .add_transport(url=PROVIDER_URL) + .add_source("examples/pacts/v3_http_consumer-v3_http_provider.json") + .set_state( + PROVIDER_URL / "_pact" / "callback", + teardown=True, + ) ) verifier.verify() diff --git a/examples/tests/v3/test_03_message_provider.py b/examples/tests/v3/test_03_message_provider.py index e3b5d6126..e9473c496 100644 --- a/examples/tests/v3/test_03_message_provider.py +++ b/examples/tests/v3/test_03_message_provider.py @@ -61,12 +61,12 @@ def test_producer() -> None: state_provider_function="state_provider_function", ) as provider_url: verifier = ( - Verifier() + Verifier("provider") + .add_transport(url=f"{provider_url}/produce_message") .set_state( provider_url / "set_provider_state", teardown=True, ) - .set_info("provider", url=f"{provider_url}/produce_message") .filter_consumers("v3_message_consumer") .add_source(PACT_DIR / "v3_message_consumer-v3_message_provider.json") ) diff --git a/examples/tests/v3/test_match.py b/examples/tests/v3/test_match.py index bb8dafeb9..02080b1f6 100644 --- a/examples/tests/v3/test_match.py +++ b/examples/tests/v3/test_match.py @@ -127,8 +127,8 @@ def test_matchers() -> None: pact.write_file(pact_dir, overwrite=True) with start_provider() as url: verifier = ( - Verifier() - .set_info("My Provider", url=url) + Verifier("My Provider") + .add_transport(url=url) .add_source(pact_dir / "consumer-provider.json") ) verifier.verify() diff --git a/src/pact/v3/verifier.py b/src/pact/v3/verifier.py index a51e3c910..824652faa 100644 --- a/src/pact/v3/verifier.py +++ b/src/pact/v3/verifier.py @@ -31,13 +31,19 @@ # In the case of local Pact files -verifier = Verifier().set_info("My Provider", url="http://localhost:8080") -verifier.add_source("pact/to/pacts/") +verifier = ( + Verifier("My Provider") + .add_transport("http", url="http://localhost:8080") + .add_source("pact/to/pacts/") +) verifier.verify() # In the case of a Pact Broker -verifier = Verifier().set_info("My Provider", url="http://localhost:8080") -verifier.broker_source("https://broker.example.com/") +verifier = ( + Verifier("My Provider") + .add_transport("http", url="http://localhost:8080") + .broker_source("https://broker.example.com/") +) verifier.verify() ``` @@ -68,19 +74,62 @@ from __future__ import annotations import json +from contextlib import nullcontext from datetime import date from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, overload +from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, overload from typing_extensions import Self from yarl import URL import pact.v3.ffi +from pact.v3._server import MessageRelay if TYPE_CHECKING: from collections.abc import Iterable +class _ProviderTransport(TypedDict): + """ + Provider transport information. + + When the verifier is set up, it needs to communicate with the Provider. This + is typically done over a single transport method (e.g., HTTP); however, Pact + _does_ support multiple transport methods. + + This dictionary is used to store information for each transport method and + is a reflection of Rust's [`ProviderTransport` + struct](https://github.com/pact-foundation/pact-reference/blob/b55407ef2be897d286af9330506219d17d2a746c/rust/pact_verifier/src/lib.rs#L168). + """ + + transport: str + """ + The transport method for payloads. + + This is typically one of `http` or `message`. Any other value is used as a + custom plugin (e.g., `grpc`). + """ + port: int | None + """ + The port on which the provider is listening. + """ + path: str | None + """ + The path under which the provider is listening. + + This is prefixed to all paths in interactions. For example, if the path is + `/api`, and the interaction path is `/users`, the request will be made to + `/api/users`. + """ + scheme: str | None + """ + The scheme to use for the provider. + + This is typically only used for the `http` transport method, where this + value can either be `http` or `https`. + """ + + class Verifier: """ A Verifier between a consumer and a provider. @@ -91,16 +140,31 @@ class Verifier: match the expectations set by the consumer. """ - def __init__(self) -> None: + def __init__(self, name: str, host: str | None = None) -> None: """ Create a new Verifier. + + Args: + name: + The name of the provider to verify. This is used to identify + which interactions the provider is involved in, and then Pact + will replay these interactions against the provider. + + host: + The host on which the Pact verifier is running. This is used to + communicate with the provider. If not specified, the default + value is `localhost`. """ - self._handle: pact.v3.ffi.VerifierHandle = ( - pact.v3.ffi.verifier_new_for_application() - ) + self._name = name + self._host = host or "localhost" + self._handle = pact.v3.ffi.verifier_new_for_application() # In order to provide a fluent interface, we remember some options which - # are set using the same FFI method. + # are set using the same FFI method. In particular, we remember + # transport methods defined, and then before verification call the + # `set_info` and `add_transport` FFI methods as needed. + self._transports: list[_ProviderTransport] = [] + self._message_relay: MessageRelay | nullcontext[None] = nullcontext() self._disable_ssl_verification = False self._request_timeout = 5000 @@ -108,107 +172,19 @@ def __str__(self) -> str: """ Informal string representation of the Verifier. """ - return "Verifier" + return f"Verifier({self._name})" def __repr__(self) -> str: """ Information-rish string representation of the Verifier. """ - return f"" - - def set_info( # noqa: PLR0913 - self, - name: str, - *, - url: str | URL | None = None, - scheme: str | None = None, - host: str | None = None, - port: int | None = None, - path: str | None = None, - ) -> Self: - """ - Set the provider information. - - This sets up information about the provider as well as the way it - communicates with the consumer. Note that for historical reasons, a - HTTP(S) transport method is always added. - - For a provider which uses other protocols (such as message queues), the - [`add_transport`][pact.v3.verifier.Verifier.add_transport] must be used. - This method can be called multiple times to add multiple transport - methods. - - Args: - name: - A user-friendly name for the provider. - - url: - The URL on which requests are made to the provider by Pact. - - It is recommended to use this parameter to set the provider URL. - If the port is not explicitly set, the default port for the - scheme will be used. - - This parameter is mutually exclusive with the individual - parameters. - - scheme: - The provider scheme. This must be one of `http` or `https`. - - host: - The provider hostname or IP address. If the provider is running - on the same machine as the verifier, `localhost` can be used. - - port: - The provider port. If not specified, the default port for the - schema will be used. - - path: - The provider context path. If not specified, the root path will - be used. - - If a non-root path is used, the path given here will be - prepended to the path in the interaction. For example, if the - path is `/api`, and the interaction path is `/users`, the - request will be made to `/api/users`. - """ - if url is not None: - if any(param is not None for param in (scheme, host, port, path)): - msg = "Cannot specify both `url` and individual parameters" - raise ValueError(msg) - - url = URL(url) - scheme = url.scheme - host = url.host - port = url.explicit_port - path = url.path - - if port is None: - msg = "Unable to determine default port for scheme {scheme}" - raise ValueError(msg) - - pact.v3.ffi.verifier_set_provider_info( - self._handle, - name, - scheme, - host, - port, - path, - ) - return self - - url = URL.build( - scheme=scheme or "http", - host=host or "localhost", - port=port, - path=path or "", - ) - return self.set_info(name, url=url) + return f"" def add_transport( self, *, - protocol: str, + url: str | URL | None = None, + protocol: str | None = None, port: int | None = None, path: str | None = None, scheme: str | None = None, @@ -222,25 +198,29 @@ def add_transport( methods. As some transport methods may not use ports, paths or schemes, these - parameters are optional. + parameters are optional. Note that while optional, these _may_ still be + used during testing as Pact uses HTTP(S) to communicate with the + provider. For example, if you are implementing your own message + verification, it needs to be exposed over HTTP and the `port` and `path` + arguments are used for this testing communication. Args: + url: + A convenient way to set the provider transport. This option + is mutually exclusive with the other options. + protocol: The protocol to use. This will typically be one of: - - `http` for communications over HTTP(S). Note that when - setting up the provider information in - [`set_info`][pact.v3.verifier.Verifier.set_info], a HTTP - transport method is always added and it is unlikely that an - additional HTTP transport method will be needed unless the - provider is running on additional ports. + - `http` for communications over HTTP(S) - - `message` for non-plugin synchronous message-based - communications. + - `message` for non-plugin message-based communications Any other protocol will be treated as a custom protocol and will be handled by a plugin. + If `url` is _not_ specified, this parameter is required. + port: The provider port. @@ -269,18 +249,82 @@ def add_transport( This is typically only used for the `http` protocol, where this value can either be `http` (the default) or `https`. """ + if url and any(x is not None for x in (protocol, port, path, scheme)): + msg = "The `url` parameter is mutually exclusive with other parameters" + raise ValueError(msg) + + if url: + url = URL(url) + if url.host != self._host: + msg = f"Host mismatch: {url.host} != {self._host}" + raise ValueError(msg) + protocol = url.scheme + if protocol == "https": + protocol = "http" + port = url.port + path = url.path + scheme = url.scheme + return self.add_transport( + protocol=protocol, + port=port, + path=path, + scheme=scheme, + ) + + if not protocol: + msg = "A protocol must be specified" + raise ValueError(msg) + if port is None and scheme: if scheme.lower() == "http": port = 80 elif scheme.lower() == "https": port = 443 - pact.v3.ffi.verifier_add_provider_transport( - self._handle, - protocol, - port or 0, - path, - scheme, + self._transports.append( + _ProviderTransport( + transport=protocol, + port=port, + path=path, + scheme=scheme, + ) + ) + + return self + + def message_handler(self, handler: Callable[[Any, Any], None]) -> Self: + """ + Set the message handler. + + This method can be used to set a custom message handler for the + verifier. The message handler is called when the verifier needs to send + a message to the provider. + + As message interactions abstract the transport layer, the message + handler is responsible for receiving (and possibly responding) to the + messages. + + ## Implementation + + Internally, Pact Python uses a lightweight HTTP server as we need to use + _some_ transport method to communicate between the Pact Core library and + the Python provider. The lightweight HTTP server receives the payloads + and then passes them to the message handler. + + It is possible to use your own HTTP server to handle messages by using + the `add_transport` method. It is not possible to use both this method + and `add_transport` to handle messages. + + Args: + handler: + The message handler. This should be a callable that takes no + arguments. + """ + self._message_relay = MessageRelay(handler) + self.add_transport( + protocol="message", + port=self._message_relay.port, + path="/_pact/message", ) return self @@ -766,7 +810,33 @@ def verify(self) -> Self: Returns: Whether the interactions were verified successfully. """ - pact.v3.ffi.verifier_execute(self._handle) + if not self._transports: + msg = "No transports have been set" + raise RuntimeError(msg) + + first, *rest = self._transports + + pact.v3.ffi.verifier_set_provider_info( + self._handle, + self._name, + first["scheme"], + self._host, + first["port"], + first["path"], + ) + + for transport in rest: + pact.v3.ffi.verifier_add_provider_transport( + self._handle, + transport["transport"], + transport["port"] or 0, + transport["path"], + transport["scheme"], + ) + + with self._message_relay: + pact.v3.ffi.verifier_execute(self._handle) + return self @property diff --git a/tests/v3/compatibility_suite/conftest.py b/tests/v3/compatibility_suite/conftest.py index bb5fdbab2..57f980289 100644 --- a/tests/v3/compatibility_suite/conftest.py +++ b/tests/v3/compatibility_suite/conftest.py @@ -39,7 +39,7 @@ def _submodule_init() -> None: @pytest.fixture def verifier() -> Verifier: """Return a new Verifier.""" - return Verifier() + return Verifier("provider") @pytest.fixture(scope="session") diff --git a/tests/v3/compatibility_suite/test_v3_http_matching.py b/tests/v3/compatibility_suite/test_v3_http_matching.py index 92b0118be..50a204f3f 100644 --- a/tests/v3/compatibility_suite/test_v3_http_matching.py +++ b/tests/v3/compatibility_suite/test_v3_http_matching.py @@ -178,7 +178,7 @@ def the_comparison_should_not_be_ok( negated: bool, # noqa: FBT001 ) -> Verifier: """The comparison should NOT be OK.""" - verifier.set_info("provider", url=provider_url) + verifier.add_transport(url=provider_url) verifier.add_transport( protocol="http", port=provider_url.port, diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 99f389f38..be2c0d2a1 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -1117,7 +1117,7 @@ def _( """ logger.debug("Running verification on %r", verifier) - verifier.set_info("provider", url=provider_url) + verifier.add_transport(url=provider_url) verifier.add_transport( protocol="message", port=provider_url.port, diff --git a/tests/v3/test_verifier.py b/tests/v3/test_verifier.py index 933e1bd2c..c31a45a37 100644 --- a/tests/v3/test_verifier.py +++ b/tests/v3/test_verifier.py @@ -18,30 +18,21 @@ @pytest.fixture def verifier() -> Verifier: - return Verifier() + return Verifier("Tester") def test_str_repr(verifier: Verifier) -> None: - assert str(verifier) == "Verifier" - assert re.match(r"", repr(verifier)) + assert str(verifier) == "Verifier(Tester)" + assert re.match( + r"", + repr(verifier), + ) def test_set_provider_info(verifier: Verifier) -> None: - name = "test_provider" url = "http://localhost:8888/api" - verifier.set_info(name, url=url) - - scheme = "http" - host = "localhost" - port = 8888 - path = "/api" - verifier.set_info( - name, - scheme=scheme, - host=host, - port=port, - path=path, - ) + verifier.add_transport(url=url) + verifier.verify() def test_add_provider_transport(verifier: Verifier) -> None: @@ -163,6 +154,7 @@ def test_broker_source_selector(verifier: Verifier) -> None: def test_verify(verifier: Verifier) -> None: + verifier.add_transport(url="http://localhost:8080") verifier.verify() From f6654dbc8b498b82d4218a4f6b1fb3bc1baf6fb6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 2 Dec 2024 11:02:10 +1100 Subject: [PATCH 0660/1376] feat(v3)!: add state handler server When testing a provider, it is very common that the provider state needs to be set up. This commit allows for functions to be provided instead of needing to configure a HTTP handler for the callback. BREAKING CHANGE: `set_state` has been renamed to `state_handler`. If using a URL still, the `body` keyword argument is now a _required_ parameter. Signed-off-by: JP-Ellis --- src/pact/v3/verifier.py | 363 +++++++++++++++++- tests/v3/compatibility_suite/util/provider.py | 3 +- tests/v3/test_verifier.py | 4 +- 3 files changed, 350 insertions(+), 20 deletions(-) diff --git a/src/pact/v3/verifier.py b/src/pact/v3/verifier.py index 824652faa..f20405fe6 100644 --- a/src/pact/v3/verifier.py +++ b/src/pact/v3/verifier.py @@ -73,22 +73,75 @@ from __future__ import annotations +import inspect import json +import typing from contextlib import nullcontext from datetime import date from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, overload +from typing import TYPE_CHECKING, Any, Callable, Literal, TypeAlias, TypedDict, overload from typing_extensions import Self from yarl import URL import pact.v3.ffi -from pact.v3._server import MessageRelay +from pact.v3._server import MessageRelay, StateCallback if TYPE_CHECKING: from collections.abc import Iterable +StateHandlerFull: TypeAlias = Callable[[str, str, dict[str, Any] | None], None] +""" +Full state handler signature. + +This is the signature for a state handler that takes three arguments: + +1. The state name, as a string. +2. The action (either `setup` or `teardown`), as a string. +3. A dictionary of parameters, or `None` if no parameters are provided. +""" +StateHandlerNoAction: TypeAlias = Callable[[str, dict[str, Any] | None], None] +""" +State handler signature without the action. + +This is the signature for a state handler that takes two arguments: + +1. The state name, as a string. +2. A dictionary of parameters, or `None` if no parameters are provided. +""" +StateHandlerNoState: TypeAlias = Callable[[str, dict[str, Any] | None], None] +""" +State handler signature without the state. + +This is the signature for a state handler that takes two arguments: + +1. The action (either `setup` or `teardown`), as a string. +2. A dictionary of parameters, or `None` if no parameters are provided. + +This function must be provided as part of a dictionary mapping state names to +functions. +""" +StateHandlerNoActionNoState: TypeAlias = Callable[[dict[str, Any] | None], None] +""" +State handler signature without the state or action. + +This is the signature for a state handler that takes one argument: + +1. A dictionary of parameters, or `None` if no parameters are provided. + +This function must be provided as part of a dictionary mapping state names to +functions. +""" +StateHandlerUrl: TypeAlias = str | URL +""" +State handler URL signature. + +Instead of providing a function to handle state changes, it is possible to +provide a URL endpoint to which the request should be made. +""" + + class _ProviderTransport(TypedDict): """ Provider transport information. @@ -165,6 +218,7 @@ def __init__(self, name: str, host: str | None = None) -> None: # `set_info` and `add_transport` FFI methods as needed. self._transports: list[_ProviderTransport] = [] self._message_relay: MessageRelay | nullcontext[None] = nullcontext() + self._state_handler: StateCallback | nullcontext[None] = nullcontext() self._disable_ssl_verification = False self._request_timeout = 5000 @@ -363,26 +417,157 @@ def filter( ) return self - def set_state( + # Cases where the handler takes the state name. + @overload + def state_handler( self, - url: str | URL, + handler: StateHandlerFull, + *, + teardown: Literal[True], + body: None = None, + ) -> Self: ... + @overload + def state_handler( + self, + handler: StateHandlerNoAction, + *, + teardown: Literal[False] = False, + body: None = None, + ) -> Self: ... + # Cases where the handler takes a dictionary of functions + @overload + def state_handler( + self, + handler: dict[str, StateHandlerNoState], + *, + teardown: Literal[True], + body: None = None, + ) -> Self: ... + @overload + def state_handler( + self, + handler: dict[str, StateHandlerNoActionNoState], + *, + teardown: Literal[False] = False, + body: None = None, + ) -> Self: ... + # Cases where the handler takes a URL + @overload + def state_handler( + self, + handler: StateHandlerUrl, *, teardown: bool = False, - body: bool = False, + body: bool, + ) -> Self: ... + + def state_handler( + self, + handler: StateHandlerFull + | StateHandlerNoAction + | dict[str, StateHandlerNoState] + | dict[str, StateHandlerNoActionNoState] + | StateHandlerUrl, + *, + teardown: bool = False, + body: bool | None = None, ) -> Self: """ - Set the provider state URL. + Set the state handler. + + In many interactions, the consumer will assume that the provider is in a + certain state. For example, a consumer requesting information about a + user with ID `123` will have specified `given("user with ID 123 + exists")`. + + The state handler is responsible for changing the provider's internal + state to match the expected state before the interaction is replayed. + + This can be done in one of three ways: + + 1. By providing a single function that will be called for all state + changes. + 2. By providing a mapping of state names to functions. + 3. By providing the URL endpoint to which the request should be made. + + The first two options are most straightforward to use. + + When providing a function, the arguments should be: - The URL is used when the provider's internal state needs to be changed. - For example, a consumer might have an interaction that requires a - specific user to be present in the database. The provider state URL is - used to change the provider's internal state to include the required - user. + 1. The state name, as a string. + 2. The action (either `setup` or `teardown`), as a string. + 3. A dictionary of parameters, or `None` if no parameters are provided. + + Note that these arguments will change in the following ways: + + 1. If a dictionary mapping is used, the state name is _not_ provided to + the function. + 2. If `teardown` is `False` thereby indicating that the function is + only called for setup, the `action` argument is not provided. + + This means that in the case of a dictionary mapping of function with + `teardown=False`, the function should take only one argument: the + dictionary of parameters (which itself may be `None`, albeit still an + argument). Args: - url: - The URL to which a `GET` request will be made to change the - provider's internal state. + handler: + The handler for the state changes. This can be one of the + following: + + - A single function that will be called for all state changes. + - A dictionary mapping state names to functions. + - A URL endpoint to which the request should be made. + + See above for more information on the function signature. + + teardown: + Whether to teardown the provider state after an interaction is + validated. + + body: + Whether to include the state change request in the body (`True`) + or in the query string (`False`). This must be left as `None` if + providing one or more handler functions; and it must be set to + a boolean if providing a URL. + """ + if isinstance(handler, StateHandlerUrl): + if body is None: + msg = "The `body` parameter must be a boolean when providing a URL" + raise ValueError(msg) + return self._state_handler_url(handler, teardown=teardown, body=body) + + if isinstance(handler, dict): + if body is not None: + msg = "The `body` parameter must be `None` when providing a dictionary" + raise ValueError(msg) + return self._state_handler_dict(handler, teardown=teardown) + + if callable(handler): + if body is not None: + msg = "The `body` parameter must be `None` when providing a function" + raise ValueError(msg) + return self._set_function_state_handler(handler, teardown=teardown) + + msg = "Invalid handler type" + raise TypeError(msg) + + def _state_handler_url( + self, + handler: StateHandlerUrl, + *, + teardown: bool, + body: bool, + ) -> Self: + """ + Set the state handler to a URL. + + This method is used to set the state handler to a URL endpoint. This + endpoint will be called to change the provider's state. + + Args: + handler: + The URL endpoint to which the request should be made. teardown: Whether to teardown the provider state after an interaction is @@ -391,15 +576,161 @@ def set_state( body: Whether to include the state change request in the body (`True`) or in the query string (`False`). + + Returns: + The verifier instance. """ pact.v3.ffi.verifier_set_provider_state( self._handle, - url if isinstance(url, str) else str(url), + str(handler), teardown=teardown, body=body, ) return self + def _state_handler_dict( + self, + handler: dict[str, StateHandlerNoState] + | dict[str, StateHandlerNoActionNoState], + *, + teardown: bool, + ) -> Self: + """ + Set the state handler to a dictionary of functions. + + This method is used to set the state handler to a dictionary of functions. + Each function is called when the provider's state needs to be changed. + + Args: + handler: + The dictionary mapping state names to functions. If `teardown` + is `True`, the functions must take two arguments: the action and + the parameters. If `teardown` is `False`, the functions must take + one argument: the parameters. + + teardown: + Whether to teardown the provider state after an interaction is + validated. + + Returns: + The verifier instance. + """ + if any(not callable(f) for f in handler.values()): + msg = "All values in the dictionary must be callable" + raise TypeError(msg) + + if teardown: + if any( + len(inspect.signature(f).parameters) != 2 # noqa: PLR2004 + for f in handler.values() + ): + msg = "All functions must take two arguments: action and parameters" + raise TypeError(msg) + + handler_map = typing.cast(dict[str, StateHandlerNoState], handler) + + def _handler( + state: str, + action: str, + parameters: dict[str, Any] | None, + ) -> None: + handler_map[state](action, parameters) + + else: + if any(len(inspect.signature(f).parameters) != 1 for f in handler.values()): + msg = "All functions must take one argument: parameters" + raise TypeError(msg) + + handler_map_no_action = typing.cast( + dict[str, StateHandlerNoActionNoState], + handler, + ) + + def _handler( + state: str, + action: str, # noqa: ARG001 + parameters: dict[str, Any] | None, + ) -> None: + handler_map_no_action[state](parameters) + + self._state_handler = StateCallback(_handler) + pact.v3.ffi.verifier_set_provider_state( + self._handle, + self._state_handler.url, + teardown=teardown, + body=True, + ) + + return self + + def _set_function_state_handler( + self, + handler: StateHandlerFull | StateHandlerNoAction, + *, + teardown: bool, + ) -> Self: + """ + Set the state handler to a single function. + + This method is used to set the state handler to a single function. This + function will be called when the provider's state needs to be changed. + + Args: + handler: + The function to call when the provider's state needs to be + changed. If `teardown` is `True`, the function must take three + arguments: the state, the action, and the parameters. If + `teardown` is `False`, the function must take two arguments: the + state and the parameters. + + teardown: + Whether to teardown the provider state after an interaction is + validated. + + Returns: + The verifier instance. + """ + if teardown: + if len(inspect.signature(handler).parameters) != 3: # noqa: PLR2004 + msg = ( + "The function must take three arguments: " + "state, action, and parameters." + ) + raise TypeError(msg) + + handler_fn_full = typing.cast(StateHandlerFull, handler) + + def _handler( + state: str, + action: str, + parameters: dict[str, Any] | None, + ) -> None: + handler_fn_full(state, action, parameters) + + else: + if len(inspect.signature(handler).parameters) != 2: # noqa: PLR2004 + msg = "The function must take two arguments: state and parameters" + raise TypeError(msg) + + handler_fn_no_action = typing.cast(StateHandlerNoAction, handler) + + def _handler( + state: str, + action: str, # noqa: ARG001 + parameters: dict[str, Any] | None, + ) -> None: + handler_fn_no_action(state, parameters) + + self._state_handler = StateCallback(_handler) + pact.v3.ffi.verifier_set_provider_state( + self._handle, + self._state_handler.url, + teardown=teardown, + body=True, + ) + + return self + def disable_ssl_verification(self) -> Self: """ Disable SSL verification. @@ -834,7 +1165,7 @@ def verify(self) -> Self: transport["scheme"], ) - with self._message_relay: + with self._message_relay, self._state_handler: pact.v3.ffi.verifier_execute(self._handle) return self diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index be2c0d2a1..d5edc83c0 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -973,9 +973,10 @@ def _( with (temp_dir / "fail_callback").open("w") as f: f.write("true") - verifier.set_state( + verifier.state_handler( provider_url / "_pact" / "callback", teardown=True, + body=False, ) diff --git a/tests/v3/test_verifier.py b/tests/v3/test_verifier.py index c31a45a37..ad1a89363 100644 --- a/tests/v3/test_verifier.py +++ b/tests/v3/test_verifier.py @@ -71,9 +71,7 @@ def test_set_filter(verifier: Verifier) -> None: def test_set_state(verifier: Verifier) -> None: - verifier.set_state("test_state") - verifier.set_state("test_state", teardown=True) - verifier.set_state("test_state", body=True) + verifier.state_handler("test_state", body=True) def test_disable_ssl_verification(verifier: Verifier) -> None: From 9928590a89f12ce040b0558587d6a7c24ebd73cd Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 4 Dec 2024 17:38:34 +1100 Subject: [PATCH 0661/1376] feat(v3)!: further simplify message interface The initial implentation required the end user to provide a function; however, this has been expanded to also allow for mapping of message names to either functions, or static message definitions. Furthermore, the function signature has been tweaked. It now expects a dictionary, with the `Message` typed dictionary being provided for convenience. As part of this commit, the MessageRelay has been renamed to MessageProducer to more correctly reflect its purpose. BREAKING CHANGE: `message_handler` signature has been changed and expanded. Signed-off-by: JP-Ellis --- src/pact/v3/_server.py | 119 +++++++++++----------- src/pact/v3/types.py | 110 +++++++++++++++++++- src/pact/v3/types.pyi | 86 +++++++++++++++- src/pact/v3/verifier.py | 215 ++++++++++++++++++++++++++-------------- 4 files changed, 393 insertions(+), 137 deletions(-) diff --git a/src/pact/v3/_server.py b/src/pact/v3/_server.py index befeccbe0..bb6301af7 100644 --- a/src/pact/v3/_server.py +++ b/src/pact/v3/_server.py @@ -29,7 +29,7 @@ from collections.abc import Callable from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from threading import Thread -from typing import TYPE_CHECKING, Any, Generic, Self, TypeAlias, TypeVar +from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar from urllib.parse import parse_qs, urlparse from pact import __version__ @@ -38,6 +38,8 @@ if TYPE_CHECKING: from types import TracebackType + from pact.v3.types import MessageProducerFull, StateHandlerFull + logger = logging.getLogger(__name__) @@ -85,14 +87,9 @@ def __init__( ################################################################################ -MessageHandlerCallable: TypeAlias = Callable[ - [bytes | None, dict[str, Any] | None], bytes | None -] - - -class MessageRelay: +class MessageProducer: """ - Internal message relay server. + Internal message producer server. The Pact server is a lightweight HTTP server which translates communications from the underlying Pact Core library with direct Python function calls. @@ -104,7 +101,7 @@ class MessageRelay: def __init__( self, - handler: MessageHandlerCallable, + handler: MessageProducerFull, host: str = "localhost", port: int | None = None, ) -> None: @@ -136,7 +133,7 @@ def __init__( self._handler = handler - self._server: HandlerHttpServer[MessageHandlerCallable] | None = None + self._server: HandlerHttpServer[MessageProducerFull] | None = None self._thread: Thread | None = None @property @@ -153,12 +150,19 @@ def port(self) -> int: """ return self._port + @property + def path(self) -> str: + """ + Server path. + """ + return MessageProducerHandler.MESSAGE_PATH + @property def url(self) -> str: """ Server URL. """ - return f"http://{self.host}:{self.port}" + return f"http://{self.host}:{self.port}{self.path}" def __enter__(self) -> Self: """ @@ -169,7 +173,7 @@ def __enter__(self) -> Self: """ self._server = HandlerHttpServer( (self.host, self.port), - MessageRelayHandler, + MessageProducerHandler, handler=self._handler, ) self._thread = Thread( @@ -199,7 +203,7 @@ def __exit__( self._thread.join() -class MessageRelayHandler(SimpleHTTPRequestHandler): +class MessageProducerHandler(SimpleHTTPRequestHandler): """ Request handler for the message relay server. @@ -222,7 +226,7 @@ class MessageRelayHandler(SimpleHTTPRequestHandler): """ if TYPE_CHECKING: - server: HandlerHttpServer[MessageHandlerCallable] + server: HandlerHttpServer[MessageProducerFull] MESSAGE_PATH = "/_pact/message" @@ -280,17 +284,39 @@ def do_POST(self) -> None: # noqa: N802 ) self.close_connection = True if self.path != self.MESSAGE_PATH: - self.send_response(404) - self.end_headers() + self.send_error(404, "Not Found") + return + + data: dict[str, Any] = json.loads( + self.rfile.read(int(self.headers.get("Content-Length", -1))) + ) + + description: str | None = data.pop("description", None) + if not description: + logger.error("No description provided in message.") + self.send_error(400, "Bad Request") return - body, metadata = self._process() self.send_response(200, "OK") - self.end_headers() - response = self.server.handler(body, metadata) - if response: - self.wfile.write(response) + message = self.server.handler(description, data) + + metadata = message.get("metadata") or {} + if content_type := message.get("content_type"): + self.send_header("Content-Type", content_type) + if "contentType" not in metadata: + metadata["contentType"] = content_type + + if metadata: + self.send_header( + "Pact-Message-Metadata", + base64.b64encode(json.dumps(metadata).encode()).decode(), + ) + + contents = message.get("contents", b"") + self.send_header("Content-Length", str(len(contents))) + self.end_headers() + self.wfile.write(contents) def do_GET(self) -> None: # noqa: N802 """ @@ -304,18 +330,7 @@ def do_GET(self) -> None: # noqa: N802 extra={"headers": self.headers}, ) self.close_connection = True - if self.path != self.MESSAGE_PATH: - self.send_response(404) - self.end_headers() - return - - body, metadata = self._process() - response = self.server.handler(body, metadata) - self.send_response(200, "OK") - self.end_headers() - - if response: - self.wfile.write(response) + self.send_error(404, "Not Found") ################################################################################ @@ -323,19 +338,6 @@ def do_GET(self) -> None: # noqa: N802 ################################################################################ -StateHandlerCallable: TypeAlias = Callable[[str, str, dict[str, Any] | None], None] -""" -State handler function. - -It must accept three positional arguments: - -- The state name, for example "user exists" -- The state action, which is either "setup" or "teardown" -- The metadata of the request if present as a dictionary, or `None`. For - example, `{"user_id": 123}`. -""" - - class StateCallback: """ Internal server for handlng state callbacks. @@ -347,7 +349,7 @@ class StateCallback: def __init__( self, - handler: StateHandlerCallable, + handler: StateHandlerFull, host: str = "localhost", port: int | None = None, ) -> None: @@ -373,7 +375,7 @@ def __init__( self._handler = handler - self._server: HandlerHttpServer[StateHandlerCallable] | None = None + self._server: HandlerHttpServer[StateHandlerFull] | None = None self._thread: Thread | None = None @property @@ -395,7 +397,7 @@ def url(self) -> str: """ Server URL. """ - return f"http://{self.host}:{self.port}" + return f"http://{self.host}:{self.port}{StateCallbackHandler.CALLBACK_PATH}" def __enter__(self) -> Self: """ @@ -445,7 +447,7 @@ class StateCallbackHandler(SimpleHTTPRequestHandler): """ if TYPE_CHECKING: - server: HandlerHttpServer[StateHandlerCallable] + server: HandlerHttpServer[StateHandlerFull] CALLBACK_PATH = "/_pact/state" @@ -471,8 +473,7 @@ def do_POST(self) -> None: # noqa: N802 self.close_connection = True url = urlparse(self.path) if url.path != self.CALLBACK_PATH: - self.send_response(404) - self.end_headers() + self.send_error(404, "Not Found") return if query := url.query: @@ -485,20 +486,19 @@ def do_POST(self) -> None: # noqa: N802 else: content_length = self.headers.get("Content-Length") if not content_length: - self.send_response(400, "Bad Request") - self.end_headers() + self.send_error(400, "Bad Request") return data = json.loads(self.rfile.read(int(content_length))) state = data.pop("state") action = data.pop("action") + params = data.pop("params") - if not state or not action: - self.send_response(400, "Bad Request") - self.end_headers() + if state is None or action is None: + self.send_error(400, "Bad Request") return - self.server.handler(state, action, data) + self.server.handler(state, action, params) self.send_response(200, "OK") self.end_headers() @@ -514,5 +514,4 @@ def do_GET(self) -> None: # noqa: N802 extra={"headers": self.headers}, ) self.close_connection = True - self.send_response(404) - self.end_headers() + self.send_error(404, "Not Found") diff --git a/src/pact/v3/types.py b/src/pact/v3/types.py index d4bd0e6a0..5850e434c 100644 --- a/src/pact/v3/types.py +++ b/src/pact/v3/types.py @@ -6,9 +6,13 @@ information to static type checkers like `mypy`. """ -from typing import Any +from __future__ import annotations + +from collections.abc import Callable +from typing import Any, TypedDict from typing_extensions import TypeAlias +from yarl import URL Matchable: TypeAlias = Any """ @@ -26,6 +30,110 @@ """ +class Message(TypedDict): + """ + Message definition. + + This is a dictionary that is used to represent the message. This class can + be used as an initializer to create a new message, or the return of a + dictionary can be used directly. + """ + + contents: bytes + """ + Message contents. + + These are the actual contents of the message, as a `bytes` object. + """ + metadata: dict[str, Any] | None + """ + Any additional metadata associated with the message. + """ + content_type: str | None + """ + Content type of the message. + + This should be specified as a MIME type, such as `application/json`. + """ + + +MessageProducerFull: TypeAlias = Callable[[str, dict[str, Any] | None], Message] +""" +Full message producer signature. + +This is the signature for a message producer that takes two arguments: + +1. The message name, as a string. +2. A dictionary of parameters, or `None` if no parameters are provided. + +The function must return a `bytes` object. +""" + +MessageProducerNoName: TypeAlias = Callable[[dict[str, Any] | None], Message] +""" +Message producer signature without the name. + +This is the signature for a message producer that takes one argument: + +1. A dictionary of parameters, or `None` if no parameters are provided. + +The function must return a `bytes` object. + +This function must be provided as part of a dictionary mapping message names to +functions. +""" + +StateHandlerFull: TypeAlias = Callable[[str, str, dict[str, Any] | None], None] +""" +Full state handler signature. + +This is the signature for a state handler that takes three arguments: + +1. The state name, as a string. +2. The action (either `setup` or `teardown`), as a string. +3. A dictionary of parameters, or `None` if no parameters are provided. +""" +StateHandlerNoAction: TypeAlias = Callable[[str, dict[str, Any] | None], None] +""" +State handler signature without the action. + +This is the signature for a state handler that takes two arguments: + +1. The state name, as a string. +2. A dictionary of parameters, or `None` if no parameters are provided. +""" +StateHandlerNoState: TypeAlias = Callable[[str, dict[str, Any] | None], None] +""" +State handler signature without the state. + +This is the signature for a state handler that takes two arguments: + +1. The action (either `setup` or `teardown`), as a string. +2. A dictionary of parameters, or `None` if no parameters are provided. + +This function must be provided as part of a dictionary mapping state names to +functions. +""" +StateHandlerNoActionNoState: TypeAlias = Callable[[dict[str, Any] | None], None] +""" +State handler signature without the state or action. + +This is the signature for a state handler that takes one argument: + +1. A dictionary of parameters, or `None` if no parameters are provided. + +This function must be provided as part of a dictionary mapping state names to +functions. +""" +StateHandlerUrl: TypeAlias = str | URL +""" +State handler URL signature. + +Instead of providing a function to handle state changes, it is possible to +provide a URL endpoint to which the request should be made. +""" + + class Unset: """ Special type to represent an unset value. diff --git a/src/pact/v3/types.pyi b/src/pact/v3/types.pyi index e29d6bd41..9c9f66264 100644 --- a/src/pact/v3/types.pyi +++ b/src/pact/v3/types.pyi @@ -4,15 +4,16 @@ # As a result, it is safe to perform expensive imports, even if they are not # used or available at runtime. -from collections.abc import Collection, Mapping, Sequence +from collections.abc import Callable, Collection, Mapping, Sequence from collections.abc import Set as AbstractSet from datetime import date, datetime, time from decimal import Decimal from fractions import Fraction -from typing import Literal +from typing import Any, Literal, TypedDict from pydantic import BaseModel from typing_extensions import TypeAlias +from yarl import URL _BaseMatchable: TypeAlias = ( int | float | complex | bool | str | bytes | bytearray | memoryview | None @@ -116,6 +117,87 @@ GeneratorType: TypeAlias = _GeneratorTypeV3 | _GeneratorTypeV4 All supported generator types. """ +class Message(TypedDict): + contents: bytes + metadata: dict[str, Any] | None + content_type: str | None + +MessageProducerFull: TypeAlias = Callable[[str, dict[str, Any] | None], Message] +""" +Full message producer signature. + +This is the signature for a message producer that takes two arguments: + +1. The message name, as a string. +2. A dictionary of parameters, or `None` if no parameters are provided. + +The function must return a `bytes` object. +""" + +MessageProducerNoName: TypeAlias = Callable[[dict[str, Any] | None], Message] +""" +Message producer signature without the name. + +This is the signature for a message producer that takes one argument: + +1. A dictionary of parameters, or `None` if no parameters are provided. + +The function must return a `bytes` object. + +This function must be provided as part of a dictionary mapping message names to +functions. +""" + +StateHandlerFull: TypeAlias = Callable[[str, str, dict[str, Any] | None], None] +""" +Full state handler signature. + +This is the signature for a state handler that takes three arguments: + +1. The state name, as a string. +2. The action (either `setup` or `teardown`), as a string. +3. A dictionary of parameters, or `None` if no parameters are provided. +""" +StateHandlerNoAction: TypeAlias = Callable[[str, dict[str, Any] | None], None] +""" +State handler signature without the action. + +This is the signature for a state handler that takes two arguments: + +1. The state name, as a string. +2. A dictionary of parameters, or `None` if no parameters are provided. +""" +StateHandlerNoState: TypeAlias = Callable[[str, dict[str, Any] | None], None] +""" +State handler signature without the state. + +This is the signature for a state handler that takes two arguments: + +1. The action (either `setup` or `teardown`), as a string. +2. A dictionary of parameters, or `None` if no parameters are provided. + +This function must be provided as part of a dictionary mapping state names to +functions. +""" +StateHandlerNoActionNoState: TypeAlias = Callable[[dict[str, Any] | None], None] +""" +State handler signature without the state or action. + +This is the signature for a state handler that takes one argument: + +1. A dictionary of parameters, or `None` if no parameters are provided. + +This function must be provided as part of a dictionary mapping state names to +functions. +""" +StateHandlerUrl: TypeAlias = str | URL +""" +State handler URL signature. + +Instead of providing a function to handle state changes, it is possible to +provide a URL endpoint to which the request should be made. +""" + class Unset: ... UNSET = Unset() diff --git a/src/pact/v3/verifier.py b/src/pact/v3/verifier.py index f20405fe6..f464bde5a 100644 --- a/src/pact/v3/verifier.py +++ b/src/pact/v3/verifier.py @@ -75,71 +75,33 @@ import inspect import json +import logging import typing from contextlib import nullcontext from datetime import date from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Literal, TypeAlias, TypedDict, overload +from typing import TYPE_CHECKING, Any, Literal, TypedDict, overload from typing_extensions import Self from yarl import URL import pact.v3.ffi -from pact.v3._server import MessageRelay, StateCallback +from pact.v3._server import MessageProducer, StateCallback +from pact.v3.types import ( + Message, + MessageProducerFull, + MessageProducerNoName, + StateHandlerFull, + StateHandlerNoAction, + StateHandlerNoActionNoState, + StateHandlerNoState, + StateHandlerUrl, +) if TYPE_CHECKING: from collections.abc import Iterable - -StateHandlerFull: TypeAlias = Callable[[str, str, dict[str, Any] | None], None] -""" -Full state handler signature. - -This is the signature for a state handler that takes three arguments: - -1. The state name, as a string. -2. The action (either `setup` or `teardown`), as a string. -3. A dictionary of parameters, or `None` if no parameters are provided. -""" -StateHandlerNoAction: TypeAlias = Callable[[str, dict[str, Any] | None], None] -""" -State handler signature without the action. - -This is the signature for a state handler that takes two arguments: - -1. The state name, as a string. -2. A dictionary of parameters, or `None` if no parameters are provided. -""" -StateHandlerNoState: TypeAlias = Callable[[str, dict[str, Any] | None], None] -""" -State handler signature without the state. - -This is the signature for a state handler that takes two arguments: - -1. The action (either `setup` or `teardown`), as a string. -2. A dictionary of parameters, or `None` if no parameters are provided. - -This function must be provided as part of a dictionary mapping state names to -functions. -""" -StateHandlerNoActionNoState: TypeAlias = Callable[[dict[str, Any] | None], None] -""" -State handler signature without the state or action. - -This is the signature for a state handler that takes one argument: - -1. A dictionary of parameters, or `None` if no parameters are provided. - -This function must be provided as part of a dictionary mapping state names to -functions. -""" -StateHandlerUrl: TypeAlias = str | URL -""" -State handler URL signature. - -Instead of providing a function to handle state changes, it is possible to -provide a URL endpoint to which the request should be made. -""" +logger = logging.getLogger(__name__) class _ProviderTransport(TypedDict): @@ -217,7 +179,7 @@ def __init__(self, name: str, host: str | None = None) -> None: # transport methods defined, and then before verification call the # `set_info` and `add_transport` FFI methods as needed. self._transports: list[_ProviderTransport] = [] - self._message_relay: MessageRelay | nullcontext[None] = nullcontext() + self._message_producer: MessageProducer | nullcontext[None] = nullcontext() self._state_handler: StateCallback | nullcontext[None] = nullcontext() self._disable_ssl_verification = False self._request_timeout = 5000 @@ -335,6 +297,15 @@ def add_transport( elif scheme.lower() == "https": port = 443 + logger.debug( + "Adding transport to verifier", + extra={ + "protocol": protocol, + "port": port, + "path": path, + "scheme": scheme, + }, + ) self._transports.append( _ProviderTransport( transport=protocol, @@ -346,40 +317,103 @@ def add_transport( return self - def message_handler(self, handler: Callable[[Any, Any], None]) -> Self: + @overload + def message_handler(self, handler: MessageProducerFull) -> Self: ... + @overload + def message_handler( + self, + handler: dict[str, MessageProducerNoName | Message], + ) -> Self: ... + + def message_handler( + self, + handler: MessageProducerFull | dict[str, MessageProducerNoName | Message], + ) -> Self: """ Set the message handler. - This method can be used to set a custom message handler for the - verifier. The message handler is called when the verifier needs to send - a message to the provider. + This method sets a custom message handler for the verifier. The handler + can be called to produce a specific message to send to the provider. + + This can be provided in one of two ways: + + 1. A fully fledged function that will be called for all messages. The + function must take two arguments: the name of the message (as a + string), and optional parameters (as a dictionary). This then + returns the message as bytes. - As message interactions abstract the transport layer, the message - handler is responsible for receiving (and possibly responding) to the - messages. + This is the most powerful option as it allows for full control over + the message generation. + + 2. A dictionary mapping message names to producer functions, or bytes. + In this case, the producer function must take optional parameters + (as a dictionary) and return the message as bytes. + + If the message to be produced is static, the bytes can be provided + directly. ## Implementation - Internally, Pact Python uses a lightweight HTTP server as we need to use - _some_ transport method to communicate between the Pact Core library and - the Python provider. The lightweight HTTP server receives the payloads - and then passes them to the message handler. + There are a large number of ways to send messages, and the specifics of + the transport methods are not specifically relevant to Pact. As such, + Pact abstracts the transport layer away and uses a lightweight HTTP + server to handle messages. - It is possible to use your own HTTP server to handle messages by using - the `add_transport` method. It is not possible to use both this method - and `add_transport` to handle messages. + Pact Python is capable of setting up this server and handling the + messages internally using user-provided handlers. It is possible to use + your own HTTP server to handle messages by using the `add_transport` + method. It is not possible to use both this method and `add_transport` + to handle messages. Args: handler: The message handler. This should be a callable that takes no - arguments. + arguments: the """ - self._message_relay = MessageRelay(handler) - self.add_transport( - protocol="message", - port=self._message_relay.port, - path="/_pact/message", + logger.debug( + "Setting message handler for verifier", + extra={ + "path": "/_pact/message", + }, ) + + if callable(handler): + if len(inspect.signature(handler).parameters) != 2: # noqa: PLR2004 + msg = "The function must take two arguments: name and parameters" + raise TypeError(msg) + + self._message_producer = MessageProducer(handler) + self.add_transport( + protocol="message", + port=self._message_producer.port, + path=self._message_producer.path, + ) + + if isinstance(handler, dict): + # Check that all values are either callable with one argument, or + # bytes. + for value in handler.values(): + if callable(value) and len(inspect.signature(value).parameters) != 1: + msg = "All functions must take one argument: parameters" + raise TypeError(msg) + if not callable(value) and not isinstance(value, dict): + msg = "All values must be callable or dictionaries" + raise TypeError(msg) + + def _handler(name: str, parameters: dict[str, Any] | None) -> Message: + logger.info("Internal handler called") + val = handler[name] + if callable(val): + return val(parameters) + return val + + self._message_producer = MessageProducer(_handler) + self.add_transport( + protocol="message", + port=self._message_producer.port, + path=self._message_producer.path, + ) + return self def filter( @@ -409,6 +443,14 @@ def filter( no_state: Whether to include interactions with no state. """ + logger.debug( + "Setting filter for verifier", + extra={ + "description": description, + "state": state, + "no_state": no_state, + }, + ) pact.v3.ffi.verifier_set_filter_info( self._handle, description, @@ -580,6 +622,14 @@ def _state_handler_url( Returns: The verifier instance. """ + logger.debug( + "Setting URL state handler for verifier", + extra={ + "handler": handler, + "teardown": teardown, + "body": body, + }, + ) pact.v3.ffi.verifier_set_provider_state( self._handle, str(handler), @@ -619,6 +669,14 @@ def _state_handler_dict( msg = "All values in the dictionary must be callable" raise TypeError(msg) + logger.debug( + "Setting dictionary state handler for verifier", + extra={ + "handler": handler, + "teardown": teardown, + }, + ) + if teardown: if any( len(inspect.signature(f).parameters) != 2 # noqa: PLR2004 @@ -690,6 +748,14 @@ def _set_function_state_handler( Returns: The verifier instance. """ + logger.debug( + "Setting function state handler for verifier", + extra={ + "handler": handler, + "teardown": teardown, + }, + ) + if teardown: if len(inspect.signature(handler).parameters) != 3: # noqa: PLR2004 msg = ( @@ -1165,8 +1231,9 @@ def verify(self) -> Self: transport["scheme"], ) - with self._message_relay, self._state_handler: + with self._message_producer, self._state_handler: pact.v3.ffi.verifier_execute(self._handle) + logger.debug("Verifier executed") return self From 5f4c0750d2d0a9951353edc639e2744707467bdf Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 5 Dec 2024 10:42:09 +1100 Subject: [PATCH 0662/1376] chore: update tests to use new message/state fns Signed-off-by: JP-Ellis --- .../compatibility_suite/test_v1_consumer.py | 2 +- .../compatibility_suite/test_v1_provider.py | 7 +- .../test_v3_http_matching.py | 32 +- .../test_v3_message_producer.py | 24 +- tests/v3/compatibility_suite/util/consumer.py | 2 + .../util/interaction_definition.py | 273 +++--- tests/v3/compatibility_suite/util/provider.py | 796 ++++++++---------- tests/v3/test_server.py | 14 +- 8 files changed, 529 insertions(+), 621 deletions(-) diff --git a/tests/v3/compatibility_suite/test_v1_consumer.py b/tests/v3/compatibility_suite/test_v1_consumer.py index 3f60c310b..578c0d4ed 100644 --- a/tests/v3/compatibility_suite/test_v1_consumer.py +++ b/tests/v3/compatibility_suite/test_v1_consumer.py @@ -312,7 +312,7 @@ def the_following_http_interactions_have_been_defined( The first row is ignored, as it is assumed to be the column headers. The order of the columns is similarly ignored. """ - logger.debug("Parsing interaction definitions") + logger.info("Parsing interaction definitions") # Check that the table is well-formed definitions = parse_horizontal_table(datatable) diff --git a/tests/v3/compatibility_suite/test_v1_provider.py b/tests/v3/compatibility_suite/test_v1_provider.py index c8ec326fa..e13ff25ae 100644 --- a/tests/v3/compatibility_suite/test_v1_provider.py +++ b/tests/v3/compatibility_suite/test_v1_provider.py @@ -27,7 +27,6 @@ a_verification_result_will_not_be_published_back, a_warning_will_be_displayed_that_there_was_no_callback_configured, publishing_of_verification_results_is_enabled, - reset_broker_var, the_provider_state_callback_will_be_called_after_the_verification_is_run, the_provider_state_callback_will_be_called_before_the_verification_is_run, the_provider_state_callback_will_not_receive_a_setup_call, @@ -93,7 +92,6 @@ def test_incorrect_request_is_made_to_provider() -> None: ) def test_verifying_a_simple_http_request_via_a_pact_broker() -> None: """Verifying a simple HTTP request via a Pact broker.""" - reset_broker_var.set(True) @pytest.mark.skipif( @@ -107,7 +105,6 @@ def test_verifying_a_simple_http_request_via_a_pact_broker() -> None: ) def test_verifying_a_simple_http_request_via_a_pact_broker_with_publishing() -> None: """Verifying a simple HTTP request via a Pact broker with publishing.""" - reset_broker_var.set(True) @pytest.mark.skipif( @@ -121,7 +118,6 @@ def test_verifying_a_simple_http_request_via_a_pact_broker_with_publishing() -> ) def test_verifying_multiple_pact_files_via_a_pact_broker() -> None: """Verifying multiple Pact files via a Pact broker.""" - reset_broker_var.set(True) @pytest.mark.skipif( @@ -135,7 +131,6 @@ def test_verifying_multiple_pact_files_via_a_pact_broker() -> None: ) def test_incorrect_request_is_made_to_provider_via_a_pact_broker() -> None: """Incorrect request is made to provider via a Pact broker.""" - reset_broker_var.set(True) @pytest.mark.skipif( @@ -397,7 +392,7 @@ def the_following_http_interactions_have_been_defined( The first row is ignored, as it is assumed to be the column headers. The order of the columns is similarly ignored. """ - logger.debug("Parsing interaction definitions") + logger.info("Parsing interaction definitions") # Check that the table is well-formed definitions = parse_horizontal_table(datatable) diff --git a/tests/v3/compatibility_suite/test_v3_http_matching.py b/tests/v3/compatibility_suite/test_v3_http_matching.py index 50a204f3f..750256e02 100644 --- a/tests/v3/compatibility_suite/test_v3_http_matching.py +++ b/tests/v3/compatibility_suite/test_v3_http_matching.py @@ -1,6 +1,5 @@ """Matching HTTP parts (request or response) feature tests.""" -import pickle import re import sys from collections.abc import Generator @@ -14,14 +13,13 @@ then, when, ) -from yarl import URL from pact.v3 import Pact from pact.v3.verifier import Verifier from tests.v3.compatibility_suite.util.interaction_definition import ( InteractionDefinition, ) -from tests.v3.compatibility_suite.util.provider import start_provider +from tests.v3.compatibility_suite.util.provider import Provider ################################################################################ ## Scenarios @@ -122,20 +120,20 @@ def test_comparing_content_type_headers_which_are_equal() -> None: @given( parsers.re( r'a request is received with an? "(?P[^"]+)" header of "(?P[^"]+)"' - ) + ), + target_fixture="interaction_definition", ) -def a_request_is_received_with_header(name: str, value: str, temp_dir: Path) -> None: +def a_request_is_received_with_header(name: str, value: str) -> InteractionDefinition: """A request is received with a "content-type" header of "application/json".""" interaction_definition = InteractionDefinition(method="GET", path="/", type="HTTP") interaction_definition.response_headers.update({name: value}) - with (temp_dir / "interactions.pkl").open("wb") as pkl_file: - pickle.dump([interaction_definition], pkl_file) + return interaction_definition @given( parsers.re( r'an expected request with an? "(?P[^"]+)" header of "(?P[^"]+)"' - ), + ) ) def an_expected_request_with_header(name: str, value: str, temp_dir: Path) -> None: """An expected request with a "content-type" header of "application/json".""" @@ -153,12 +151,16 @@ def an_expected_request_with_header(name: str, value: str, temp_dir: Path) -> No ################################################################################ -@when("the request is compared to the expected one", target_fixture="provider_url") +@when("the request is compared to the expected one", target_fixture="provider") def the_request_is_compared_to_the_expected_one( - temp_dir: Path, -) -> Generator[URL, None, None]: + interaction_definition: InteractionDefinition, +) -> Generator[Provider, None, None]: """The request is compared to the expected one.""" - yield from start_provider(temp_dir) + provider = Provider() + provider.add_interaction(interaction_definition) + + with provider: + yield provider ################################################################################ @@ -172,16 +174,16 @@ def the_request_is_compared_to_the_expected_one( target_fixture="verifier_result", ) def the_comparison_should_not_be_ok( - provider_url: URL, + provider: Provider, verifier: Verifier, temp_dir: Path, negated: bool, # noqa: FBT001 ) -> Verifier: """The comparison should NOT be OK.""" - verifier.add_transport(url=provider_url) + verifier.add_transport(url=provider.url) verifier.add_transport( protocol="http", - port=provider_url.port, + port=provider.url.port, path="/", ) verifier.add_source(temp_dir / "pacts") diff --git a/tests/v3/compatibility_suite/test_v3_message_producer.py b/tests/v3/compatibility_suite/test_v3_message_producer.py index 936a9dce7..2cb5cfb74 100644 --- a/tests/v3/compatibility_suite/test_v3_message_producer.py +++ b/tests/v3/compatibility_suite/test_v3_message_producer.py @@ -4,7 +4,6 @@ import json import logging -import pickle import re import sys from pathlib import Path @@ -30,7 +29,6 @@ a_pact_file_for_message_is_to_be_verified, a_provider_is_started_that_can_generate_the_message, a_provider_state_callback_is_configured, - start_provider, the_provider_state_callback_will_be_called_after_the_verification_is_run, the_provider_state_callback_will_be_called_before_the_verification_is_run, the_provider_state_callback_will_receive_a_setup_call, @@ -40,10 +38,6 @@ ) if TYPE_CHECKING: - from collections.abc import Generator - - from yarl import URL - from pact.v3.verifier import Verifier TEST_PACT_FILE_DIRECTORY = Path(Path(__file__).parent / "pacts") @@ -388,22 +382,18 @@ def a_pact_file_for_is_to_be_verified_with_the_following_metadata( r'message with "(?P[^"]+)" and the following metadata:', re.DOTALL, ), - target_fixture="provider_url", + target_fixture="provider", ) def a_provider_is_started_that_can_generate_the_message_with_the_following_metadata( - temp_dir: Path, + verifier: Verifier, name: str, fixture: str, datatable: list[list[str]], -) -> Generator[URL, None, None]: +) -> None: """A provider is started that can generate the message with the following metadata.""" # noqa: E501 - interaction_definitions: list[InteractionDefinition] = [] metadata = parse_horizontal_table(datatable) - if (temp_dir / "interactions.pkl").exists(): - with (temp_dir / "interactions.pkl").open("rb") as pkl_file: - interaction_definitions = pickle.load(pkl_file) # noqa: S301 - interaction_definition = InteractionDefinition( + interaction = InteractionDefinition( type="Async", description=name, body=fixture, @@ -414,12 +404,8 @@ def a_provider_is_started_that_can_generate_the_message_with_the_following_metad for row in metadata }, ) - interaction_definitions.append(interaction_definition) - - with (temp_dir / "interactions.pkl").open("wb") as pkl_file: - pickle.dump(interaction_definitions, pkl_file) - yield from start_provider(temp_dir) + verifier.message_handler(interaction.message_producer) ################################################################################ diff --git a/tests/v3/compatibility_suite/util/consumer.py b/tests/v3/compatibility_suite/util/consumer.py index dfe73c0d8..2646d740d 100644 --- a/tests/v3/compatibility_suite/util/consumer.py +++ b/tests/v3/compatibility_suite/util/consumer.py @@ -126,6 +126,7 @@ def _() -> PactInteractionTuple[AsyncMessageInteraction]: """ A message integration is being defined for a consumer test. """ + logger.info("Creating a message interaction") pact = Pact("consumer", "provider") pact.with_specification(version) return PactInteractionTuple( @@ -158,6 +159,7 @@ def _( interaction_definitions: dict[int, InteractionDefinition], ) -> Generator[PactServer, Any, None]: """The mock server is started with interactions.""" + logger.info("Starting Pact mock server") pact = Pact("consumer", "provider") pact.with_specification(version) for iid in ids: diff --git a/tests/v3/compatibility_suite/util/interaction_definition.py b/tests/v3/compatibility_suite/util/interaction_definition.py index b94dd1ff9..d490c9c6e 100644 --- a/tests/v3/compatibility_suite/util/interaction_definition.py +++ b/tests/v3/compatibility_suite/util/interaction_definition.py @@ -8,17 +8,14 @@ from __future__ import annotations -import base64 import contextlib import json import logging -import sys import typing +import warnings from typing import Any, Literal from xml.etree import ElementTree as ET -import flask -from flask import request from multidict import MultiDict from typing_extensions import Self from yarl import URL @@ -32,11 +29,12 @@ ) if typing.TYPE_CHECKING: + from http.server import SimpleHTTPRequestHandler from pathlib import Path from pact.v3.interaction import Interaction from pact.v3.pact import Pact - from tests.v3.compatibility_suite.util.provider import Provider + from pact.v3.types import Message logger = logging.getLogger(__name__) @@ -595,26 +593,55 @@ def add_to_pact( # noqa: C901, PLR0912, PLR0915 else: interaction.with_metadata({key: json.dumps(value)}) - def add_to_provider(self, provider: Provider) -> None: + def matches_request(self, request: SimpleHTTPRequestHandler) -> bool: """ - Add an interaction to a Flask app. + Check if a request matches the interaction. Args: - provider: - The test provider to add the interaction to. + request: + The request to check. + + Returns: + Whether the request matches the interaction. + """ + if self.type == "HTTP": + logger.debug( + "Checking whether request '%s %s' matches '%s %s'", + request.command, + request.path, + self.method, + self.path, + ) + return ( + request.command == self.method + and request.path.split("?")[0] == self.path + ) + return False + + def handle_request(self, request: SimpleHTTPRequestHandler) -> None: + """ + Handle a HTTP request. + + Internally, we use Python's built-in [`http.server`][http.server] module + to handle the request. For each request, Pythhon instantiates a new + [`SimpleHTTPRequestHandler`][http.server.SimpleHTTPRequestHandler] + object with the request details and provider the interface to respond. + + Args: + request: + The request to handle. """ - logger.debug("Adding %s interaction to Flask app", self.type) + logger.debug("Handling request: %s %s", request.command, request.path) if self.type == "HTTP": - self._add_http_to_provider(provider) - elif self.type == "Sync": - self._add_sync_to_provider(provider) - elif self.type == "Async": - self._add_async_to_provider(provider) + self._handle_request_http(request) + elif self.type in ("Sync", "Async"): + msg = "Sync and Async interactions are handled by message relay." + raise ValueError(msg) else: msg = f"Unknown interaction type: {self.type}" raise ValueError(msg) - def _add_http_to_provider(self, provider: Provider) -> None: + def _handle_request_http(self, request: SimpleHTTPRequestHandler) -> None: # noqa: C901, PLR0912 """ Add a HTTP interaction to a Flask app. @@ -623,137 +650,125 @@ def _add_http_to_provider(self, provider: Provider) -> None: route. Args: - provider: - The test provider to add the interaction to. + request: + The request to handle. """ assert isinstance(self.method, str), "Method must be a string" assert isinstance(self.path, str), "Path must be a string" logger.info( - "Adding HTTP '%s %s' interaction to Flask app", + "Handling HTTP '%s %s' interaction", self.method, self.path, ) - logger.debug("-> Query: %s", self.query) - logger.debug("-> Headers: %s", self.headers) - logger.debug("-> Body: %s", self.body) - logger.debug("-> Response Status: %s", self.response) - logger.debug("-> Response Headers: %s", self.response_headers) - logger.debug("-> Response Body: %s", self.response_body) - - def route_fn() -> flask.Response: - if self.query: - query = URL.build(query_string=self.query).query - # Perform a two-way check to ensure that the query parameters - # are present in the request, and that the request contains no - # unexpected query parameters. - for k, v in query.items(): - assert request.args[k] == v - for k, v in request.args.items(): - assert query[k] == v - - if self.headers: - # Perform a one-way check to ensure that the expected headers - # are present in the request, but don't check for any unexpected - # headers. - for k, v in self.headers.items(): - assert k in request.headers - assert request.headers[k] == v - - if self.body: - assert request.data == self.body.bytes - - return flask.Response( - response=self.response_body.bytes or self.response_body.string or None - if self.response_body - else None, - status=self.response, - headers=dict(**self.response_headers), - content_type=self.response_body.mime_type - if self.response_body - else None, - direct_passthrough=True, - ) - - # The route function needs to have a unique name - clean_name = self.path.replace("/", "_").replace("__", "_") - route_fn.__name__ = f"{self.method.lower()}_{clean_name}" - - provider.app.add_url_rule( - self.path, - view_func=route_fn, - methods=[self.method], - ) - - def _add_sync_to_provider(self, provider: Provider) -> None: - """ - Add a synchronous message interaction to a Flask app. - Args: - provider: - The test provider to add the interaction to. - """ - raise NotImplementedError + # Check the request method + if request.command != self.method: + logger.error("Method mismatch: %s != %s", request.command, self.method) + request.send_error(405, "Method Not Allowed") + return - def _add_async_to_provider(self, provider: Provider) -> None: - """ - Add a synchronous message interaction to a Flask app. + # Check the request path + if request.path.split("?")[0] != self.path: + logger.error("Path mismatch: %s != %s", request.path, self.path) + request.send_error(404, "Not Found") + return - Args: - provider: - The test provider to add the interaction to. - """ - assert self.description, "Description must be set for async messages" - provider.messages[self.description] = self + # Check the query parameters + # + # We expect an exact match of the query parameters (unlike the headers) + if self.query: + logger.info("Checking request query parameters") + expected_query = URL.build(query_string=self.query).query + request_query = URL.build( + query_string=request.path.split("?")[1] if "?" in request.path else "" + ).query + if (expected_keys := set(expected_query.keys())) != ( + request_keys := set(request_query.keys()) + ): + logger.error( + "Query parameter mismatch: %s != %s", + request_keys, + expected_keys, + ) + request.send_error(400, "Bad Request") + return - # All messages are handled by the same route. So we just need to check - # whether the route has been defined, and if not, define it. - for rule in provider.app.url_map.iter_rules(): - if rule.rule == "/_pact/message": - sys.stderr.write("Async message route already defined\n") + for k in expected_query: + if (request_vals := request_query.getall(k)) != ( + expected_vals := expected_query.getall(k) + ): + logger.error( + "Query parameter mismatch: %s != %s", + request_vals, + expected_vals, + ) + request.send_error(400, "Bad Request") + return + + # Check the headers + # + # We only check for the headers we expect from the interaction + # definition. It is very likely that the request will contain additional + # headers (e.g. `Host`, `User-Agent`, etc.) that we do not care about. + if self.headers: + logger.info("Checking request headers") + for k, v in self.headers.items(): + if (rv := request.headers.get(k)) != v: + logger.error("Header mismatch: %s != %s", rv, v) + request.send_error(400, "Bad Request") + return + + # Check the body + if self.body: + content_length = int(request.headers.get("Content-Length", 0)) + request_body = request.rfile.read(content_length) + if request_body != self.body.bytes: + request.send_error(400, "Bad Request") return - sys.stderr.write("Adding async message route\n") - - @provider.app.post("/_pact/message") - def post_message() -> flask.Response: - body: dict[str, Any] = json.loads(request.data) - description: str = body["description"] - - if description not in provider.messages: - return flask.Response( - response=json.dumps({ - "error": f"Message {description} not found", - }), - status=404, - headers={"Content-Type": "application/json"}, - content_type="application/json", - ) + # Send the response + if not self.response: + warnings.warn( + "No response defined, defaulting to 200", + RuntimeWarning, + stacklevel=2, + ) - interaction: InteractionDefinition = provider.messages[description] - return interaction.create_async_message_response() + request.send_response(self.response or 200) + for k, v in self.response_headers.items(): + request.send_header(k, v) + if self.response_body and self.response_body.mime_type: + request.send_header("Content-Type", self.response_body.mime_type) + request.end_headers() + if self.response_body and self.response_body.bytes: + request.wfile.write(self.response_body.bytes) - def create_async_message_response(self) -> flask.Response: + def message_producer( + self, + name: str, + metadata: dict[str, Any] | None, + ) -> Message: """ - Convert the interaction to a Flask response. + Handle a message interaction. - When an async message needs to be produced, Pact expects the response - from the special `/_pact/message` endppoint to generate the expected - message. + Args: + name: + The name of the message to produce. - Whilst this is a Response from Flask's perspective, the attributes - returned + metadata: + Metadata for the message. """ - assert self.type == "Async", "Only async messages are supported" + logger.info("Handling message interaction") + logger.info(" -> Body: %r", name) + logger.info(" -> Metadata: %r", metadata) + assert self.type in ("Sync", "Async"), "Message interactions only" - if self.metadata: - self.headers["Pact-Message-Metadata"] = base64.b64encode( - json.dumps(self.metadata).encode("utf-8") - ).decode("utf-8") - - return flask.Response( - response=self.body.bytes or self.body.string or None if self.body else None, - headers=((k, v) for k, v in self.headers.items()), - content_type=self.body.mime_type if self.body else None, - direct_passthrough=True, - ) + assert name == self.description, "Description mismatch" + + contents = self.body.bytes if self.body else None + return { + "contents": contents or b"", + "content_type": self.body.mime_type if self.body else None, + "metadata": self.metadata, + } diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index d5edc83c0..b733e4466 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -14,45 +14,35 @@ from __future__ import annotations -import sys -from pathlib import Path - -import pytest - -from pact.v3._util import find_free_port - -sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent)) - - import copy +import inspect import json import logging import os -import pickle import re import shutil -import signal import subprocess -import time import warnings -from contextvars import ContextVar -from datetime import datetime, timezone +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from io import BytesIO from threading import Thread -from typing import TYPE_CHECKING, Any, NoReturn +from typing import TYPE_CHECKING, Any, ClassVar, Self, TypedDict +from unittest.mock import MagicMock -import flask +import pytest import requests -from flask import request +from multidict import CIMultiDict from pytest_bdd import given, parsers, then, when from yarl import URL import pact.constants # type: ignore[import-untyped] +from pact import __version__ +from pact.v3._server import MessageProducer +from pact.v3._util import find_free_port from pact.v3.pact import Pact from tests.v3.compatibility_suite.util import ( parse_headers, parse_horizontal_table, - serialize, - truncate, ) from tests.v3.compatibility_suite.util.interaction_definition import ( InteractionDefinition, @@ -61,29 +51,15 @@ if TYPE_CHECKING: from collections.abc import Generator + from pathlib import Path + from types import TracebackType + from pact.v3.types import Message from pact.v3.verifier import Verifier logger = logging.getLogger(__name__) -version_var = ContextVar("version_var", default="0") -""" -Shared context variable to store the version of the consumer application. - -This is used to generate a new version for the consumer application to use when -publishing the interactions to the Pact Broker. -""" -reset_broker_var = ContextVar("reset_broker", default=True) -""" -This context variable is used to determine whether the Pact broker should be -cleaned up. It is used to ensure that the broker is only cleaned up once, even -if a step is run multiple times. - -All scenarios which make use of the Pact broker should set this to `True` at the -start of the scenario. -""" - VERIFIER_ERROR_MAP: dict[str, str] = { "Response status did not match": "StatusMismatch", "Headers had differences": "HeaderMismatch", @@ -92,7 +68,7 @@ } -def next_version() -> str: +def _next_version() -> Generator[str, None, None]: """ Get the next version for the consumer. @@ -102,221 +78,245 @@ def next_version() -> str: Returns: The next version. """ - version = version_var.get() - version_var.set(str(int(version) + 1)) - return version + version = 0 + while True: + yield str(version) + version += 1 -def _setup_logging(log_level: int) -> None: - """ - Set up logging for the provider. +version_iter = _next_version() - Pytest is responsible for setting up the logging for the main Python - process, but the provider runs in a subprocess and does not automatically - inherit the logging configuration. - This function sets up the logging within the provider subprocess, provided - that it wasn't already set up (in case any logging configuration is - inherited). +class Provider: """ - if logging.getLogger().handlers: - return + HTTP provider for the compatibility suite tests. - logging.basicConfig( - level=log_level, - format="%(asctime)s.%(msec)03d [%(levelname)s] %(name)s: %(message)s", - datefmt="%H:%M:%S", - ) - logger.debug("Debug logging enabled") + As we are testing specific scenarios, this provider server is designed to + be easily customized to return specific responses for specific requests. + """ + interactions: ClassVar[list[InteractionDefinition]] = [] -class Provider: + def __init__( + self, + host: str = "localhost", + port: int | None = None, + ) -> None: + """ + Initialize the provider. + + Args: + host: + The host for the provider. + + port: + The port for the provider. If not provided, then a free port + will be found. + """ + self._host = host + self._port = port or find_free_port() + + self._interactions: list[InteractionDefinition] = [] + self.requests: list[ProviderRequestDict] | None = None + self._server: ProviderServer | None = None + self._thread: Thread | None = None + + @property + def host(self) -> str: + """ + Server host. + """ + return self._host + + @property + def port(self) -> int: + """ + Server port. + """ + return self._port + + @property + def url(self) -> URL: + """ + Server URL. + """ + return URL(f"http://{self.host}:{self.port}") + + def add_interaction(self, interaction: InteractionDefinition) -> None: + """ + Add an interaction to the provider. + + Args: + interaction: + The interaction to add. + """ + self._interactions.append(interaction) + + def __enter__(self) -> Self: + """ + Start the provider. + """ + logger.info( + "Starting provider on %s with %s interaction(s)", + self.url, + len(self._interactions), + ) + self._server = ProviderServer( + (self.host, self.port), + ProviderRequestHandler, + interactions=self._interactions, + ) + self._thread = Thread( + target=self._server.serve_forever, + name="Compatibility Suite Provider Server", + ) + self._thread.start() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """ + Exit the Provider context. + """ + if not self._thread or not self._server: + warnings.warn( + "Exiting server context despite server not being started.", + stacklevel=2, + ) + return + + self.requests = self._server.requests + self._server.shutdown() + self._thread.join() + + +class ProviderServer(ThreadingHTTPServer): """ - HTTP Provider. + Simple HTTP server for the provider. """ - def __init__(self, provider_dir: Path | str, log_level: int) -> None: + def __init__( + self, + *args: Any, # noqa: ANN401 + interactions: list[InteractionDefinition], + **kwargs: Any, # noqa: ANN401 + ) -> None: """ - Instantiate a new provider. + Initialize the server. Args: - provider_dir: - The directory containing various files used to configure the - provider. At a minimum, this directory must contain a file - called `interactions.pkl`. This file must contain a list of - [`InteractionDefinition`] objects. + interactions: + The interactions to use for the server. + + *args: + Positional arguments to pass to the base `ThreadingHTTPServer` + class. - log_level: - The log level for the provider. + **kwargs: + Keyword arguments to pass to the base `ThreadingHTTPServer` + class. """ - _setup_logging(log_level) + self.interactions = interactions + self.requests: list[ProviderRequestDict] = [] + super().__init__(*args, **kwargs) - self.messages: dict[str, InteractionDefinition] = {} - self.provider_dir = Path(provider_dir) - if not self.provider_dir.is_dir(): - msg = f"Directory {self.provider_dir} does not exist" - raise ValueError(msg) - self.app: flask.Flask = flask.Flask("provider") - self._add_ping() - self._add_callback() - self._add_after_request() - self._add_interactions() - - def _add_ping(self) -> None: - """ - Add a ping endpoint to the provider. - - This is used to check that the provider is running. - """ - - @self.app.get("/_test/ping") - def ping() -> str: - """Simple ping endpoint for testing.""" - return "pong" - - def _add_callback(self) -> None: - """ - Add a callback endpoint to the provider. - - This is used to receive any callbacks from Pact to configure any - internal state (e.g., "given a user exists"). As far as the testing - is concerned, this is just a simple endpoint that records the request - and returns an empty response. - - If the provider directory contains a file called `fail_callback`, then - the callback will return a 404 response. - - If the provider directory contains a file called `provider_state`, then - the callback will check that the `state` query parameter matches the - contents of the file. - """ - - @self.app.route("/_pact/callback", methods=["GET", "POST"]) - def callback() -> tuple[str, int] | str: - if (self.provider_dir / "fail_callback").exists(): - return "Provider state not found", 404 - - provider_states_path = self.provider_dir / "provider_states" - if provider_states_path.exists(): - logger.debug("Provider states file found") - with provider_states_path.open() as f: - states = [InteractionState(**s) for s in json.load(f)] - logger.debug("Provider states: %s", states) - for state in states: - if request.args["state"] == state.name: - logger.debug("State found: %s", state) - for k, v in state.parameters.items(): - assert k in request.args - assert str(request.args[k]) == str(v) - logger.debug("State parameters match") - break - else: - msg = "State not found" - raise ValueError(msg) - - timestamp = datetime.now(tz=timezone.utc).strftime("%H:%M:%S.%f") - json_file = self.provider_dir / f"callback.{timestamp}.json" - with json_file.open("w") as f: - json.dump( - { - "method": request.method, - "path": request.path, - "query_string": request.query_string.decode("utf-8"), - "query_params": serialize(request.args), - "headers_list": serialize(request.headers), - "headers_dict": serialize(dict(request.headers)), - "body": request.data.decode("utf-8", errors="backslashreplace"), - "form": serialize(request.form), - }, - f, - ) - - return "" - - def _add_after_request(self) -> None: - """ - Add a handler to log requests and responses. - - This is used to log the requests and responses to the provider - application (both to the logger as well as to files). - """ - - @self.app.after_request - def log_request(response: flask.Response) -> flask.Response: - logger.debug("Received request: %s %s", request.method, request.path) - logger.debug("-> Query string: %s", request.query_string.decode("utf-8")) - logger.debug("-> Headers: %s", serialize(request.headers)) - logger.debug("-> Body: %s", truncate(request.get_data().decode("utf-8"))) - logger.debug("-> Form: %s", serialize(request.form)) - - timestamp = datetime.now(tz=timezone.utc).strftime("%H:%M:%S.%f") - with (self.provider_dir / f"request.{timestamp}.json").open("w") as f: - json.dump( - { - "method": request.method, - "path": request.path, - "query_string": request.query_string.decode("utf-8"), - "query_params": serialize(request.args), - "headers_list": serialize(request.headers), - "headers_dict": serialize(dict(request.headers)), - "body": request.data.decode("utf-8", errors="backslashreplace"), - "form": serialize(request.form), - }, - f, - ) - return response - - @self.app.after_request - def log_response(response: flask.Response) -> flask.Response: - logger.debug("Returning response: %d", response.status_code) - logger.debug("-> Headers: %s", serialize(response.headers)) - logger.debug( - "-> Body: %s", - truncate( - response.get_data().decode("utf-8", errors="backslashreplace") - ), - ) +class ProviderRequestDict(TypedDict): + """ + Request dictionary for the provider server. + """ + + method: str | None + path: str | None + query: str | None + headers: CIMultiDict[str] | None + body: bytes | None - timestamp = datetime.now(tz=timezone.utc).strftime("%H:%M:%S.%f") - with (self.provider_dir / f"response.{timestamp}.json").open("w") as f: - json.dump( - { - "status_code": response.status_code, - "headers_list": serialize(response.headers), - "headers_dict": serialize(dict(response.headers)), - "body": response.get_data().decode( - "utf-8", errors="backslashreplace" - ), - }, - f, - ) - return response - def _add_interactions(self) -> None: +class ProviderRequestHandler(SimpleHTTPRequestHandler): + """ + Request handler for the provider server. + + This class is responsible for handling the requests made to the provider + server. It uses the standard library's + [`SimpleHTTPRequestHandler`][http.server.SimpleHTTPRequestHandler]. + """ + + if TYPE_CHECKING: + server: ProviderServer + + def version_string(self) -> str: """ - Add the interactions to the provider. + Get the server version string. + + Returns: + The server version string. """ - with (self.provider_dir / "interactions.pkl").open("rb") as f: - interactions: list[InteractionDefinition] = pickle.load(f) # noqa: S301 + return f"Compatibility Suite Provider/{__version__}" - for interaction in interactions: - interaction.add_to_provider(self) + def _record_request(self) -> None: + """ + Record the request. + + Parses the request and records it in the server's request list. - def run(self) -> None: + The `rfile` attribute, being a file-like object, can only be read once. + This method reads the request body and then replaces the `rfile` + attribute with a new `BytesIO` object containing the request body. """ - Start the provider. + size = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(size) + request: ProviderRequestDict = { + "method": self.command, + "path": self.path, + "query": self.path.split("?", 1)[1] if "?" in self.path else None, + "headers": CIMultiDict(self.headers.items()), + "body": body, + } + self.server.requests.append(request) + self.rfile = BytesIO(body) + + def do_POST(self) -> None: # noqa: N802 """ - url = URL(f"http://localhost:{find_free_port()}") - sys.stderr.write(f"Starting provider on {url}\n") - for endpoint in self.app.url_map.iter_rules(): - sys.stderr.write(f" * {endpoint}\n") + Handle a POST request. + """ + logger.info("Handling %s %s", self.command, self.path) + self._record_request() + + for interaction in self.server.interactions: + if interaction.matches_request(self): + interaction.handle_request(self) + return - self.app.run( - host=url.host, - port=url.port, - debug=True, + logger.warning( + "No matching interaction found for %s %s", + self.command, + self.path, ) + self.send_error(404, "Not Found") + + def do_GET(self) -> None: # noqa: N802 + """ + Handle a GET request. + """ + logger.info("Handling %s %s", self.command, self.path) + self._record_request() + + for interaction in self.server.interactions: + if interaction.matches_request(self): + interaction.handle_request(self) + return + + logger.warning( + "No matching interaction found for %s %s", + self.command, + self.path, + ) + self.send_error(404, "Not Found") class PactBroker: @@ -408,7 +408,7 @@ def publish(self, directory: Path | str, version: str | None = None) -> None: if self.password: cmd.extend(["--broker-password", self.password]) - cmd.extend(["--consumer-app-version", version or next_version()]) + cmd.extend(["--consumer-app-version", version or next(version_iter)]) subprocess.run( # noqa: S603 cmd, @@ -503,16 +503,6 @@ def latest_verification_results(self) -> requests.Response | None: return response -if __name__ == "__main__": - import sys - - if len(sys.argv) != 3: - sys.stderr.write(f"Usage: {sys.argv[0]} \n") - sys.exit(1) - - Provider(sys.argv[1], int(sys.argv[2])).run() - - ################################################################################ ## Given ################################################################################ @@ -527,26 +517,25 @@ def a_provider_is_started_that_returns_the_responses_from_interactions( r'from interactions? "?(?P[0-9, ]+)"?', ), converters={"interactions": lambda x: [int(i) for i in x.split(",") if i]}, - target_fixture="provider_url", + target_fixture="provider", stacklevel=stacklevel + 1, ) def _( interaction_definitions: dict[int, InteractionDefinition], interactions: list[int], - temp_dir: Path, - ) -> Generator[URL, None, None]: + ) -> Generator[Provider, None, None]: """ Start a provider that returns the responses from the given interactions. """ - logger.debug("Starting provider for interactions %s", interactions) + logger.info("Starting provider for interactions %s", interactions) + provider = Provider() for i in interactions: - logger.debug("Interaction %d: %s", i, interaction_definitions[i]) - - with (temp_dir / "interactions.pkl").open("wb") as pkl_file: - pickle.dump([interaction_definitions[i] for i in interactions], pkl_file) + logger.info("Interaction %d: %s", i, interaction_definitions[i]) + provider.add_interaction(interaction_definitions[i]) - yield from start_provider(temp_dir) + with provider: + yield provider def a_provider_is_started_that_returns_the_responses_from_interactions_with_changes( @@ -555,44 +544,42 @@ def a_provider_is_started_that_returns_the_responses_from_interactions_with_chan @given( parsers.re( r"a provider is started that returns the responses?" - r' from interactions? "?(?P[0-9, ]+)"?' + r' from interactions? "?(?P[0-9, ]+)"?' r" with the following changes:", re.DOTALL, ), - converters={ - "interactions": lambda x: [int(i) for i in x.split(",") if i], - }, - target_fixture="provider_url", + converters={"ids": lambda x: [int(i) for i in x.split(",") if i]}, + target_fixture="provider", stacklevel=stacklevel + 1, ) def _( interaction_definitions: dict[int, InteractionDefinition], - interactions: list[int], + ids: list[int], datatable: list[list[str]], - temp_dir: Path, - ) -> Generator[URL, None, None]: + ) -> Generator[Provider, None, None]: """ Start a provider that returns the responses from the given interactions. """ - logger.debug("Starting provider for modified interactions %s", interactions) + logger.info("Starting provider for modified interactions %s", ids) changes = parse_horizontal_table(datatable) assert len(changes) == 1, "Only one set of changes is supported" - defns: list[InteractionDefinition] = [] - for interaction in interactions: - defn = copy.deepcopy(interaction_definitions[interaction]) - defn.update(**changes[0]) # type: ignore[arg-type] - defns.append(defn) - logger.debug( + interactions: list[InteractionDefinition] = [] + for id_ in ids: + interaction = copy.deepcopy(interaction_definitions[id_]) + interaction.update(**changes[0]) # type: ignore[arg-type] + interactions.append(interaction) + logger.info( "Updated interaction %d: %s", + id_, interaction, - defn, ) - with (temp_dir / "interactions.pkl").open("wb") as pkl_file: - pickle.dump(defns, pkl_file) - - yield from start_provider(temp_dir) + provider = Provider() + for interaction in interactions: + provider.add_interaction(interaction) + with provider: + yield provider def a_provider_is_started_that_can_generate_the_message( @@ -604,20 +591,15 @@ def a_provider_is_started_that_can_generate_the_message( r' that can generate the "(?P[^"]+)" message' r' with "(?P.+)"$' ), - target_fixture="provider_url", + target_fixture="provider", stacklevel=stacklevel + 1, ) def _( - temp_dir: Path, + verifier: Verifier, name: str, body: str, - ) -> Generator[URL, None, None]: - interactions: list[InteractionDefinition] = [] - interactions_pkl = temp_dir / "interactions.pkl" - if interactions_pkl.exists(): - with interactions_pkl.open("rb") as f: - interactions = pickle.load(f) # noqa: S301 - + ) -> None: + logger.info("Starting provider for message %s", name) interaction = InteractionDefinition( type="Async", description=name, @@ -626,78 +608,23 @@ def _( # If there's no content type, then it is a `text/plain` message if interaction.body and not interaction.body.mime_type: interaction.body.mime_type = "text/plain" - interactions.append(interaction) - - with (temp_dir / "interactions.pkl").open("wb") as pkl_file: - pickle.dump(interactions, pkl_file) - - yield from start_provider(temp_dir) - - -def start_provider(provider_dir: str | Path) -> Generator[URL, None, None]: # noqa: C901 - """Start the provider app with the given interactions.""" - process = subprocess.Popen( # noqa: S603 - [ - sys.executable, - Path(__file__), - str(provider_dir), - str(logger.getEffectiveLevel()), - ], - cwd=Path.cwd(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="utf-8", - ) - - pattern = re.compile(r" \* Running on (?P[^ ]+)") - while True: - if process.poll() is not None: - logger.error("Provider process exited with code %d", process.returncode) - logger.error( - "Provider stdout: %s", process.stdout.read() if process.stdout else "" - ) - logger.error( - "Provider stderr: %s", process.stderr.read() if process.stderr else "" - ) - msg = f"Provider process exited with code {process.returncode}" - raise RuntimeError(msg) - if ( - process.stderr - and (line := process.stderr.readline()) - and (match := pattern.match(line)) - ): - break - time.sleep(0.1) - - url = URL(match.group("url")) - logger.debug("Provider started on %s", url) - for _ in range(50): - try: - response = requests.get(str(url / "_test" / "ping"), timeout=1) - assert response.text == "pong" - break - except (requests.RequestException, AssertionError): - time.sleep(0.1) - continue - else: - msg = "Failed to ping provider" - raise RuntimeError(msg) - def redirect() -> NoReturn: - while True: - if process.stdout: - while line := process.stdout.readline(): - logger.debug("Provider stdout: %s", line.rstrip()) - if process.stderr: - while line := process.stderr.readline(): - logger.debug("Provider stderr: %s", line.rstrip()) + # The following is a hack to allow for multiple message interactions to + # be defined. Typically, the end user would know all messages to be + # produced; however, we don't have this luxury in this context. + if isinstance(verifier._message_producer, MessageProducer): # noqa: SLF001 + original_handler = verifier._message_producer._handler # noqa: SLF001 - thread = Thread(target=redirect, daemon=True) - thread.start() + def handler(*args: Any, **kwargs: Any) -> Message: # noqa: ANN401 + try: + return original_handler(*args, **kwargs) + except AssertionError: + return interaction.message_producer(*args, **kwargs) - yield url + verifier.message_handler(handler) - process.send_signal(signal.SIGINT) + else: + verifier.message_handler(interaction.message_producer) def a_pact_file_for_interaction_is_to_be_verified( @@ -722,7 +649,7 @@ def _( """ Verify the Pact file for the given interaction. """ - logger.debug( + logger.info( "Adding interaction %d to be verified: %s", interaction, interaction_definitions[interaction], @@ -742,7 +669,7 @@ def _( encoding="utf-8", ) as f: for line in f: - logger.debug("Pact file: %s", line.rstrip()) + logger.info("Pact file: %s", line.rstrip()) verifier.add_source(temp_dir / "pacts") @@ -772,7 +699,7 @@ def _( body=fixture, ) defn.pending = pending - logger.debug("Adding message interaction: %s", defn) + logger.info("Adding message interaction: %s", defn) pact = Pact("consumer", "provider") pact.with_specification(version) @@ -781,7 +708,7 @@ def _( pact.write_file(temp_dir / "pacts") with (temp_dir / "pacts" / "consumer-provider.json").open() as f: - logger.debug("Pact file contents: %s", f.read()) + logger.info("Pact file contents: %s", f.read()) verifier.add_source(temp_dir / "pacts") @@ -809,7 +736,7 @@ def _( """ Verify the Pact file for the given interaction. """ - logger.debug( + logger.info( "Adding interaction %d to be verified: %s", interaction, interaction_definitions[interaction], @@ -836,7 +763,7 @@ def _( encoding="utf-8", ) as f: for line in f: - logger.debug("Pact file: %s", line.rstrip()) + logger.info("Pact file: %s", line.rstrip()) verifier.add_source(temp_dir / "pacts") @@ -860,6 +787,7 @@ def _( fixture: str, datatable: list[list[str]], ) -> None: + logger.info("Adding message interaction %s with comments", name) defn = InteractionDefinition( type="Async", description=name, @@ -882,7 +810,7 @@ def _( pact.write_file(temp_dir / "pacts") with (temp_dir / "pacts" / "consumer-provider.json").open() as f: - logger.debug("Pact file contents: %s", f.read()) + logger.info("Pact file contents: %s", f.read()) verifier.add_source(temp_dir / "pacts") @@ -910,7 +838,7 @@ def _( """ Verify the Pact file for the given interaction from a Pact broker. """ - logger.debug( + logger.info( "Adding interaction %d to be verified from a Pact broker", interaction ) @@ -925,10 +853,6 @@ def _( pact.write_file(pacts_dir) pact_broker = PactBroker(broker_url) - if reset_broker_var.get(): - logger.debug("Resetting Pact broker") - pact_broker.reset() - reset_broker_var.set(False) pact_broker.publish(pacts_dir) verifier.broker_source(pact_broker.url) yield pact_broker @@ -940,7 +864,7 @@ def _(verifier: Verifier) -> None: """ Enable publishing of verification results. """ - logger.debug("Publishing verification results") + logger.info("Publishing verification results") verifier.set_publish_options( "0.0.0", @@ -955,29 +879,36 @@ def a_provider_state_callback_is_configured( r"a provider state callback is configured" r"(?P(, but will return a failure)?)", ), + target_fixture="provider_callback", converters={"failure": lambda x: x != ""}, stacklevel=stacklevel + 1, ) def _( verifier: Verifier, - provider_url: URL, - temp_dir: Path, failure: bool, # noqa: FBT001 - ) -> None: + ) -> MagicMock: """ Configure a provider state callback. """ - logger.debug("Configuring provider state callback") + logger.info("Configuring provider state callback") + + def _callback( + _name: str, + _action: str, + _params: dict[str, str] | None, + ) -> None: + pass + provider_callback = MagicMock(return_value=None, spec=_callback) + provider_callback.__signature__ = inspect.signature(_callback) if failure: - with (temp_dir / "fail_callback").open("w") as f: - f.write("true") + provider_callback.side_effect = RuntimeError("Provider state change failed") verifier.state_handler( - provider_url / "_pact" / "callback", + provider_callback, teardown=True, - body=False, ) + return provider_callback def a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state_defined( @@ -1002,7 +933,7 @@ def _( """ Verify the Pact file for the given interaction with a provider state defined. """ - logger.debug( + logger.info( "Adding interaction %d to be verified with provider state %s", interaction, state, @@ -1020,7 +951,7 @@ def _( verifier.add_source(temp_dir / "pacts") with (temp_dir / "provider_states").open("w") as f: - logger.debug("Writing provider state to %s", temp_dir / "provider_states") + logger.info("Writing provider state to %s", temp_dir / "provider_states") json.dump([s.as_dict() for s in defn.states], f) @@ -1048,7 +979,7 @@ def _( Verify the Pact file for the given interaction with provider states defined. """ states = parse_horizontal_table(datatable) - logger.debug( + logger.info( "Adding interaction %d to be verified with provider states %s", interaction, states, @@ -1068,7 +999,7 @@ def _( verifier.add_source(temp_dir / "pacts") with (temp_dir / "provider_states").open("w") as f: - logger.debug("Writing provider state to %s", temp_dir / "provider_states") + logger.info("Writing provider state to %s", temp_dir / "provider_states") json.dump([s.as_dict() for s in defn.states], f) @@ -1086,7 +1017,7 @@ def _( """ Configure a request filter to make the given changes. """ - logger.debug("Configuring request filter") + logger.info("Configuring request filter") changes = parse_horizontal_table(datatable) if "headers" in changes[0]: @@ -1111,19 +1042,16 @@ def the_verification_is_run( ) def _( verifier: Verifier, - provider_url: URL, + provider: Provider | None, ) -> tuple[Verifier, Exception | None]: """ Run the verification. """ - logger.debug("Running verification on %r", verifier) + logger.info("Running verification on %r", verifier) + + if provider: + verifier.add_transport(url=provider.url) - verifier.add_transport(url=provider_url) - verifier.add_transport( - protocol="message", - port=provider_url.port, - path="/_pact/message", - ) try: verifier.verify() except Exception as e: # noqa: BLE001 @@ -1151,8 +1079,8 @@ def _( """ Check that the verification was successful. """ - logger.debug("Checking verification result") - logger.debug("Verifier result: %s", verifier_result) + logger.info("Checking verification result") + logger.info("Verifier result: %s", verifier_result) if negated: assert verifier_result[1] is not None @@ -1171,10 +1099,10 @@ def _(verifier_result: tuple[Verifier, Exception | None], error: str) -> None: """ Check that the verification results contain the given error. """ - logger.debug("Checking that verification results contain error %s", error) + logger.info("Checking that verification results contain error %s", error) verifier = verifier_result[0] - logger.debug("Verification results: %s", json.dumps(verifier.results, indent=2)) + logger.info("Verification results: %s", json.dumps(verifier.results, indent=2)) mismatch_type = VERIFIER_ERROR_MAP.get(error) if not mismatch_type: @@ -1212,7 +1140,7 @@ def _(pact_broker: PactBroker) -> None: """ Check that the verification result was published back to the Pact broker. """ - logger.debug("Checking that verification result was not published back") + logger.info("Checking that verification result was not published back") response = pact_broker.latest_verification_results() if response: @@ -1239,7 +1167,7 @@ def _( """ Check that the verification result was published back to the Pact broker. """ - logger.debug( + logger.info( "Checking that verification result was published back for interaction %d", interaction, ) @@ -1279,7 +1207,7 @@ def _( """ Check that the verification result was published back to the Pact broker. """ - logger.debug( + logger.info( "Checking that failed verification result" " was published back for interaction %d", interaction, @@ -1312,7 +1240,7 @@ def _() -> None: """ Check that the provider state callback was called before the verification. """ - logger.debug("Checking provider state callback was called before verification") + logger.info("Checking provider state callback was called before verification") def the_provider_state_callback_will_receive_a_setup_call( @@ -1327,7 +1255,7 @@ def the_provider_state_callback_will_receive_a_setup_call( stacklevel=stacklevel + 1, ) def _( - temp_dir: Path, + provider_callback: MagicMock, action: str, state: str, ) -> None: @@ -1335,20 +1263,14 @@ def _( Check that the provider state callback received a setup call. """ logger.info("Checking provider state callback received a %s call", action) - logger.info("Callback files: %s", list(temp_dir.glob("callback.*.json"))) - for file in temp_dir.glob("callback.*.json"): - with file.open("r") as f: - data: dict[str, Any] = json.load(f) - logger.debug("Checking callback data: %s", data) - if ( - "action" in data["query_params"] - and data["query_params"]["action"] == action - and data["query_params"]["state"] == state - ): - break - else: - msg = f"No {action} call found" - raise AssertionError(msg) + logger.debug("Calls: %s", provider_callback.call_args_list) + provider_callback.assert_called() + for calls in provider_callback.call_args_list: + if calls.args[0] == state and calls.args[1] == action: + return + + msg = f"No {action} call found" + raise AssertionError(msg) def the_provider_state_callback_will_receive_a_setup_call_with_parameters( @@ -1365,7 +1287,7 @@ def the_provider_state_callback_will_receive_a_setup_call_with_parameters( stacklevel=stacklevel + 1, ) def _( - temp_dir: Path, + provider_callback: MagicMock, action: str, state: str, datatable: list[list[str]], @@ -1374,30 +1296,19 @@ def _( Check that the provider state callback received a setup call. """ logger.info("Checking provider state callback received a %s call", action) - logger.info("Callback files: %s", list(temp_dir.glob("callback.*.json"))) parameters = parse_horizontal_table(datatable) - params: dict[str, str] = parameters[0] - # If we have a string that looks quoted, unquote it + params: dict[str, Any] = parameters[0] + # Values are JSON values, so parse them for key, value in params.items(): - if value.startswith('"') and value.endswith('"'): - params[key] = value[1:-1] + params[key] = json.loads(value) - for file in temp_dir.glob("callback.*.json"): - with file.open("r") as f: - data: dict[str, Any] = json.load(f) - logger.debug("Checking callback data: %s", data) - if ( - "action" in data["query_params"] - and data["query_params"]["action"] == action - and data["query_params"]["state"] == state - ): - for key, value in params.items(): - assert key in data["query_params"], f"Parameter {key} not found" - assert data["query_params"][key] == value - break - else: - msg = f"No {action} call found" - raise AssertionError(msg) + provider_callback.assert_called() + for calls in provider_callback.call_args_list: + if calls.args[0] == state and calls.args[1] == action: + assert calls.args[2] == params + return + msg = f"No {action} call found" + raise AssertionError(msg) def the_provider_state_callback_will_not_receive_a_setup_call( @@ -1420,7 +1331,7 @@ def _( for file in temp_dir.glob("callback.*.json"): with file.open("r") as f: data: dict[str, Any] = json.load(f) - logger.debug("Checking callback data: %s", data) + logger.info("Checking callback data: %s", data) if ( "action" in data["query_params"] and data["query_params"]["action"] == action @@ -1459,7 +1370,7 @@ def _( """ Check that a warning was displayed that there was no callback configured. """ - logger.debug("Checking for warning about missing provider state callback") + logger.info("Checking for warning about missing provider state callback") assert state @@ -1474,27 +1385,24 @@ def the_request_to_the_provider_will_contain_the_header( stacklevel=stacklevel + 1, ) def _( - verifier_result: tuple[Verifier, Exception | None], + provider: Provider, header: dict[str, str], - temp_dir: Path, + # verifier_result: tuple[Verifier, Exception | None], + # temp_dir: Path, ) -> None: """ Check that the request to the provider contained the given header. """ - verifier = verifier_result[0] - logger.debug("verifier output: %s", verifier.output(strip_ansi=True)) - logger.debug("verifier results: %s", json.dumps(verifier.results, indent=2)) - for request_path in temp_dir.glob("request.*.json"): - with request_path.open("r") as f: - data: dict[str, Any] = json.load(f) - if data["path"].startswith("/_test"): - continue - logger.debug("Checking request data: %s", data) - assert all([k, v] in data["headers_list"] for k, v in header.items()) - break - else: - msg = "No request found" - raise AssertionError(msg) + logger.info("Checking for header %r in provider requests", header) + provider.__exit__(None, None, None) + assert provider.requests + assert len(provider.requests) == 1 + request = provider.requests[0] + assert request["headers"] + + for key, value in header.items(): + assert key in request["headers"] + assert request["headers"][key] == value def there_will_be_a_pending_error( @@ -1511,7 +1419,7 @@ def _( """ There will be a pending error. """ - logger.debug("Checking for pending error") + logger.info("Checking for pending error") verifier, err = verifier_result if error == "Body had differences": @@ -1551,8 +1459,8 @@ def _( Check that the given comment was printed to the console. """ verifier, err = verifier_result - logger.debug("Checking for comment %r in verifier output", comment) - logger.debug("Verifier output: %s", verifier.output(strip_ansi=True)) + logger.info("Checking for comment %r in verifier output", comment) + logger.info("Verifier output: %s", verifier.output(strip_ansi=True)) assert err is None assert comment in verifier.output(strip_ansi=True) @@ -1574,7 +1482,7 @@ def _( Check that the given test name was displayed as the original test name. """ verifier, err = verifier_result - logger.debug("Checking for test name %r in verifier output", test_name) - logger.debug("Verifier output: %s", verifier.output(strip_ansi=True)) + logger.info("Checking for test name %r in verifier output", test_name) + logger.info("Verifier output: %s", verifier.output(strip_ansi=True)) assert err is None assert test_name in verifier.output(strip_ansi=True) diff --git a/tests/v3/test_server.py b/tests/v3/test_server.py index de6f665db..7de91e006 100644 --- a/tests/v3/test_server.py +++ b/tests/v3/test_server.py @@ -9,12 +9,12 @@ import aiohttp import pytest -from pact.v3._server import MessageRelay, StateCallback +from pact.v3._server import MessageProducer, StateCallback def test_relay_default_init() -> None: handler = MagicMock() - server = MessageRelay(handler) + server = MessageProducer(handler) assert server.host == "localhost" assert server.port > 1024 # Random non-privileged port @@ -24,7 +24,7 @@ def test_relay_default_init() -> None: @pytest.mark.asyncio async def test_relay_invalid_path_http() -> None: handler = MagicMock(return_value="Not OK") - server = MessageRelay(handler) + server = MessageProducer(handler) with server: async with aiohttp.ClientSession() as session: @@ -36,7 +36,7 @@ async def test_relay_invalid_path_http() -> None: @pytest.mark.asyncio async def test_relay_get_http() -> None: handler = MagicMock(return_value=b"Pact Python is awesome!") - server = MessageRelay(handler) + server = MessageProducer(handler) with server: async with aiohttp.ClientSession() as session: @@ -51,7 +51,7 @@ async def test_relay_get_http() -> None: @pytest.mark.asyncio async def test_relay_post_http() -> None: handler = MagicMock(return_value=b"Pact Python is awesome!") - server = MessageRelay(handler) + server = MessageProducer(handler) with server: async with aiohttp.ClientSession() as session: @@ -69,7 +69,7 @@ async def test_relay_post_http() -> None: @pytest.mark.asyncio async def test_relay_get_with_metadata() -> None: handler = MagicMock(return_value=b"Pact Python is awesome!") - server = MessageRelay(handler) + server = MessageProducer(handler) metadata = base64.b64encode(json.dumps({"key": "value"}).encode()).decode() with server: @@ -88,7 +88,7 @@ async def test_relay_get_with_metadata() -> None: @pytest.mark.asyncio async def test_relay_post_with_metadata() -> None: handler = MagicMock(return_value=b"Pact Python is awesome!") - server = MessageRelay(handler) + server = MessageProducer(handler) metadata = base64.b64encode(json.dumps({"key": "value"}).encode()).decode() with server: From 00e6e0c611914d58f1ed4097431001d4e0ed3ec7 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 5 Dec 2024 13:35:31 +1100 Subject: [PATCH 0663/1376] chore: adapt examples to use function handlers With functions being able to be provided directly to the Verifier, this greatly simplifies the examples as we no longer need custom HTTP servers to wrap a function. Signed-off-by: JP-Ellis --- examples/src/consumer.py | 6 +- examples/src/fastapi.py | 6 +- examples/src/flask.py | 14 +- examples/src/message.py | 16 +- examples/src/message_producer.py | 4 + examples/tests/v3/provider_server.py | 242 ------------------ examples/tests/v3/test_01_fastapi_provider.py | 191 ++++++++------ examples/tests/v3/test_03_message_provider.py | 65 ++--- 8 files changed, 171 insertions(+), 373 deletions(-) delete mode 100644 examples/tests/v3/provider_server.py diff --git a/examples/src/consumer.py b/examples/src/consumer.py index 9986db9cd..13e097ce2 100644 --- a/examples/src/consumer.py +++ b/examples/src/consumer.py @@ -19,9 +19,9 @@ from the provider is the user's ID, name, and creation date. This is despite the provider having additional fields in the response. -Note that the code in this module is agnostic of Pact. The `pact-python` -dependency only appears in the tests. This is because the consumer is not -concerned with Pact, only the tests are. +Note that the code in this module is agnostic of Pact (i.e., this would be your +production code). The `pact-python` dependency only appears in the tests. This +is because the consumer is not concerned with Pact, only the tests are. """ from __future__ import annotations diff --git a/examples/src/fastapi.py b/examples/src/fastapi.py index c9e919c08..2a3b9e06b 100644 --- a/examples/src/fastapi.py +++ b/examples/src/fastapi.py @@ -20,9 +20,9 @@ testing will provide feedback on whether the consumer is compatible with the provider's changes. -Note that the code in this module is agnostic of Pact. The `pact-python` -dependency only appears in the tests. This is because the consumer is not -concerned with Pact, only the tests are. +Note that the code in this module is agnostic of Pact (i.e., this would be your +production code). The `pact-python` dependency only appears in the tests. This +is because the consumer is not concerned with Pact, only the tests are. """ from __future__ import annotations diff --git a/examples/src/flask.py b/examples/src/flask.py index 4d3b09a4c..6be632e5a 100644 --- a/examples/src/flask.py +++ b/examples/src/flask.py @@ -12,9 +12,17 @@ (the consumer) and returns a response. In this example, we have a simple endpoint which returns a user's information from a (fake) database. -Note that the code in this module is agnostic of Pact. The `pact-python` -dependency only appears in the tests. This is because the consumer is not -concerned with Pact, only the tests are. +This also showcases how Pact tests differ from merely testing adherence to an +OpenAPI specification. The Pact tests are more concerned with the practical use +of the API, rather than the formally defined specification. The User class +defined here has additional fields which are not used by the consumer. Should +the provider later decide to add or remove fields, Pact's consumer-driven +testing will provide feedback on whether the consumer is compatible with the +provider's changes. + +Note that the code in this module is agnostic of Pact (i.e., this would be your +production code). The `pact-python` dependency only appears in the tests. This +is because the consumer is not concerned with Pact, only the tests are. """ from __future__ import annotations diff --git a/examples/src/message.py b/examples/src/message.py index 13f14c49f..aa4f33333 100644 --- a/examples/src/message.py +++ b/examples/src/message.py @@ -2,9 +2,19 @@ Handler for non-HTTP interactions. This module implements a very basic handler to handle JSON payloads which might -be sent from Kafka, or some queueing system. Unlike a HTTP interaction, the -handler is solely responsible for processing the message, and does not -necessarily need to send a response. +be sent through a messaging system. Unlike a HTTP interaction, the handler is +solely responsible for processing the message, and does not necessarily need to +send a response. This specific example handles file system events. + +Due to the broad range of possible technologies underpinning message systems +(e.g., Kafka, RabbitMQ, SQS, SNS, etc.), Pact's implementation is agnostic to +the transport mechanism. Instead, Pact Python v3 allows to provide a simple +function (or mapping of functions) to produce messages. Under the hood, Pact +uses HTTP to communicate + +Note that the code in this module is agnostic of Pact (i.e., this would be your +production code). The `pact-python` dependency only appears in the tests. This +is because the consumer is not concerned with Pact, only the tests are. """ from __future__ import annotations diff --git a/examples/src/message_producer.py b/examples/src/message_producer.py index efbe7e762..fe8efcb26 100644 --- a/examples/src/message_producer.py +++ b/examples/src/message_producer.py @@ -3,6 +3,10 @@ This modules implements a very basic message producer which could send to an eventing system, such as Kafka, or a message queue. + +Note that the code in this module is agnostic of Pact (i.e., this would be your +production code). The `pact-python` dependency only appears in the tests. This +is because the consumer is not concerned with Pact, only the tests are. """ from __future__ import annotations diff --git a/examples/tests/v3/provider_server.py b/examples/tests/v3/provider_server.py deleted file mode 100644 index 20f1637ca..000000000 --- a/examples/tests/v3/provider_server.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -HTTP Server to route message requests to message producer function. -""" - -from __future__ import annotations - -import logging -import re -import signal -import subprocess -import sys -import time -from contextlib import contextmanager -from importlib import import_module -from pathlib import Path -from threading import Thread -from typing import TYPE_CHECKING, NoReturn - -import requests - -from pact.v3._util import find_free_port - -sys.path.append(str(Path(__file__).parent.parent.parent.parent)) - -from yarl import URL - -import flask - -if TYPE_CHECKING: - from collections.abc import Generator - -logger = logging.getLogger(__name__) - - -class Provider: - """ - Provider class to route message requests to message producer function. - - Sets up three endpoints: - - /_test/ping: A simple ping endpoint for testing. - - /produce_message: Route message requests to the handler function. - - /set_provider_state: Set the provider state. - - The specific `produce_message` and `set_provider_state` URLs can be configured - with the `produce_message_url` and `set_provider_state_url` arguments. - """ - - def __init__( # noqa: PLR0913 - self, - handler_module: str, - handler_function: str, - produce_message_url: str, - state_provider_module: str, - state_provider_function: str, - set_provider_state_url: str, - ) -> None: - """ - Initialize the provider. - - Args: - handler_module: - The name of the module containing the handler function. - handler_function: - The name of the handler function. - produce_message_url: - The URL to route message requests to the handler function. - state_provider_module: - The name of the module containing the state provider setup function. - state_provider_function: - The name of the state provider setup function. - set_provider_state_url: - The URL to set the provider state. - """ - self.app = flask.Flask("Provider") - self.handler_function = getattr(import_module(handler_module), handler_function) - self.produce_message_url = produce_message_url - self.set_provider_state_url = set_provider_state_url - if state_provider_module: - self.state_provider_function = getattr( - import_module(state_provider_module), state_provider_function - ) - - @self.app.get("/_test/ping") - def ping() -> str: - """Simple ping endpoint for testing.""" - return "pong" - - @self.app.route(self.produce_message_url, methods=["POST"]) - def produce_message() -> flask.Response | tuple[str, int]: - """ - Route a message request to the handler function. - - Returns: - The response from the handler function. - """ - try: - body, content_type = self.handler_function() - return flask.Response( - response=body, - status=200, - content_type=content_type, - direct_passthrough=True, - ) - except Exception as e: # noqa: BLE001 - return str(e), 500 - - @self.app.route(self.set_provider_state_url, methods=["POST"]) - def set_provider_state() -> tuple[str, int]: - """ - Calls the state provider function with the state provided in the request. - - Returns: - A response indicating that the state has been set. - """ - if self.state_provider_function: - self.state_provider_function(flask.request.args["state"]) - return "Provider state set", 200 - - def run(self) -> None: - """ - Start the provider. - """ - url = URL(f"http://localhost:{find_free_port()}") - sys.stderr.write(f"Starting provider on {url}\n") - - self.app.run( - host=url.host, - port=url.port, - debug=True, - ) - - -@contextmanager -def start_provider(**kwargs: str) -> Generator[URL, None, None]: # noqa: C901 - """ - Start the provider app. - - Expects kwargs to to contain the following: - handler_module: Required. The name of the module containing - the handler function. - handler_function: Required. The name of the handler function. - produce_message_url: Optional. The URL to route message requests to - the handler function. - state_provider_module: Optional. The name of the module containing - the state provider setup function. - state_provider_function: Optional. The name of the state provider - setup function. - set_provider_state_url: Optional. The URL to set the provider state. - """ - process = subprocess.Popen( # noqa: S603 - [ - sys.executable, - Path(__file__), - kwargs.pop("handler_module"), - kwargs.pop("handler_function"), - kwargs.pop("produce_message_url", "/produce_message"), - kwargs.pop("state_provider_module", ""), - kwargs.pop("state_provider_function", ""), - kwargs.pop("set_provider_state_url", "/set_provider_state"), - ], - cwd=Path.cwd(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="utf-8", - ) - - pattern = re.compile(r" \* Running on (?P[^ ]+)") - while True: - if process.poll() is not None: - logger.error("Provider process exited with code %d", process.returncode) - logger.error( - "Provider stdout: %s", process.stdout.read() if process.stdout else "" - ) - logger.error( - "Provider stderr: %s", process.stderr.read() if process.stderr else "" - ) - msg = f"Provider process exited with code {process.returncode}" - raise RuntimeError(msg) - if ( - process.stderr - and (line := process.stderr.readline()) - and (match := pattern.match(line)) - ): - break - time.sleep(0.1) - - url = URL(match.group("url")) - logger.debug("Provider started on %s", url) - for _ in range(50): - try: - response = requests.get(str(url / "_test" / "ping"), timeout=1) - assert response.text == "pong" - break - except (requests.RequestException, AssertionError): - time.sleep(0.1) - continue - else: - msg = "Failed to ping provider" - raise RuntimeError(msg) - - def redirect() -> NoReturn: - while True: - if process.stdout: - while line := process.stdout.readline(): - logger.debug("Provider stdout: %s", line.strip()) - if process.stderr: - while line := process.stderr.readline(): - logger.debug("Provider stderr: %s", line.strip()) - - thread = Thread(target=redirect, daemon=True) - thread.start() - - try: - yield url - finally: - process.send_signal(signal.SIGINT) - - -if __name__ == "__main__": - import sys - - if len(sys.argv) < 5: - sys.stderr.write( - f"Usage: {sys.argv[0]} " - f" " - ) - sys.exit(1) - - handler_module = sys.argv[1] - handler_function = sys.argv[2] - produce_message_url = sys.argv[3] - state_provider_module = sys.argv[4] - state_provider_function = sys.argv[5] - set_provider_state_url = sys.argv[6] - Provider( - handler_module, - handler_function, - produce_message_url, - state_provider_module, - state_provider_function, - set_provider_state_url, - ).run() diff --git a/examples/tests/v3/test_01_fastapi_provider.py b/examples/tests/v3/test_01_fastapi_provider.py index 2f91fbabe..2ef49a213 100644 --- a/examples/tests/v3/test_01_fastapi_provider.py +++ b/examples/tests/v3/test_01_fastapi_provider.py @@ -18,99 +18,79 @@ side effects, the provider's database calls are mocked out using functionalities from `unittest.mock`. -In order to set the provider into the correct state, this test module defines an -additional endpoint on the provider, in this case `/_pact/callback`. Calls to -this endpoint mock the relevant database calls to set the provider into the -correct state. +Note that Pact requires tat the provider be running on an accessible URL. This +means that FastAPI's [`TestClient`][fastapi.testclient.TestClient] cannot be used +to test the provider. Instead, the provider is run in a separate thread using +Python's [`Thread`][threading.Thread] class. """ from __future__ import annotations +import contextlib import time from datetime import datetime, timezone -from multiprocessing import Process -from typing import TYPE_CHECKING, Callable, Literal +from threading import Thread +from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock +import pytest import uvicorn from yarl import URL -from examples.src.fastapi import User, app +from examples.src.fastapi import User from pact.v3 import Verifier -PROVIDER_URL = URL("http://localhost:8000") - - -@app.post("/_pact/callback") -async def mock_pact_provider_states( - action: Literal["setup", "teardown"], - state: str, -) -> dict[Literal["result"], str]: - """ - Handler for the provider state callback. - - For Pact to be able to correctly tests compliance with the contract, the - internal state of the provider needs to be set up correctly. For example, if - the consumer expects a user to exist in the database, the provider needs to - have a user with the given ID in the database. - - Naïvely, this can be achieved by setting up the database with the correct - data for the test, but this can be slow and error-prone, and requires - standing up additional infrastructure. The alternative showcased here is to - mock the relevant calls to the database so as to avoid any side effects. The - `unittest.mock` library is used to achieve this as part of the `setup` - action. - - The added benefit of using this approach is that the mock can subsequently - be inspected to ensure that the correct calls were made to the database. For - example, asserting that the correct user ID was retrieved from the database. - These checks are performed as part of the `teardown` action. This action can - also be used to reset the mock, or in the case were a real database is used, - to clean up any side effects. - - Args: - action: - One of `setup` or `teardown`. Determines whether the provider state - should be set up or torn down. - - state: - The name of the state to set up or tear down. - - Returns: - A dictionary containing the result of the action. - """ - mapping: dict[str, dict[str, Callable[[], None]]] = {} - mapping["setup"] = { - "user doesn't exists": mock_user_doesnt_exist, - "user exists": mock_user_exists, - "the specified user doesn't exist": mock_post_request_to_create_user, - "user is present in DB": mock_delete_request_to_delete_user, - } - mapping["teardown"] = { - "user doesn't exists": verify_user_doesnt_exist_mock, - "user exists": verify_user_exists_mock, - "the specified user doesn't exist": verify_mock_post_request_to_create_user, - "user is present in DB": verify_mock_delete_request_to_delete_user, - } +if TYPE_CHECKING: + from collections.abc import Generator - mapping[action][state]() - return {"result": f"{action} {state} completed"} +PROVIDER_URL = URL("http://localhost:8000") -def run_server() -> None: +class Server(uvicorn.Server): """ - Run the FastAPI server. + Custom server class to run the FastAPI server in a separate thread. - This function is required to run the FastAPI server in a separate process. A - lambda cannot be used as the target of a `multiprocessing.Process` as it - cannot be pickled. + Thanks to [this StackOverflow + answer](https://stackoverflow.com/a/64521239/1573761) for this solution. """ - host = PROVIDER_URL.host if PROVIDER_URL.host else "localhost" - port = PROVIDER_URL.port if PROVIDER_URL.port else 8000 - uvicorn.run(app, host=host, port=port) - -def test_provider() -> None: + def install_signal_handlers(self) -> None: + """ + Prevent the server from installing signal handlers. + + This is required to run the FastAPI server in a separate process. The + default behaviour of `uvicorn.Server` is to install signal handlers which + would interfere with the signal handlers of the main process. + """ + + @contextlib.contextmanager + def run_in_thread(self) -> Generator[str, None, None]: + """ + Run the FastAPI server in a separate thread. + + This method runs the FastAPI server in a separate thread and yields the + URL of the server. The server is started in a separate thread to allow the + tests to run in the main thread. + """ + thread = Thread(target=self.run) + thread.start() + try: + while not self.started: + time.sleep(0.01) + yield f"http://{self.config.host}:{self.config.port}" + finally: + self.should_exit = True + thread.join() + + +@pytest.fixture(scope="session") +def server() -> Generator[str, None, None]: + server = Server(uvicorn.Config("examples.src.fastapi:app", host="localhost")) + with server.run_in_thread() as url: + yield url + + +def test_provider(server: str) -> None: """ Test the FastAPI provider with Pact. @@ -154,21 +134,68 @@ def test_provider() -> None: the tests will fail and the output will show which interactions failed and why. """ - proc = Process(target=run_server, daemon=True) - proc.start() - time.sleep(2) verifier = ( Verifier("v3_http_provider") - .add_transport(url=PROVIDER_URL) + .add_transport(url=server) .add_source("examples/pacts/v3_http_consumer-v3_http_provider.json") - .set_state( - PROVIDER_URL / "_pact" / "callback", - teardown=True, - ) + .state_handler(provider_state_handler, teardown=True) ) verifier.verify() - proc.terminate() + +def provider_state_handler( + state: str, + action: str, + _parameters: dict[str, Any] | None, +) -> None: + """ + Handler for the provider state callback. + + For Pact to be able to correctly tests compliance with the contract, the + internal state of the provider needs to be set up correctly. For example, if + the consumer expects a user to exist in the database, the provider needs to + have a user with the given ID in the database. + + Naïvely, this can be achieved by setting up the database with the correct + data for the test, but this can be slow and error-prone, and requires + standing up additional infrastructure. The alternative showcased here is to + mock the relevant calls to the database so as to avoid any side effects. The + `unittest.mock` library is used to achieve this as part of the `setup` + action. + + The added benefit of using this approach is that the mock can subsequently + be inspected to ensure that the correct calls were made to the database. For + example, asserting that the correct user ID was retrieved from the database. + These checks are performed as part of the `teardown` action. This action can + also be used to reset the mock, or in the case were a real database is used, + to clean up any side effects. + + Args: + action: + One of `setup` or `teardown`. Determines whether the provider state + should be set up or torn down. + + state: + The name of the state to set up or tear down. + + Returns: + A dictionary containing the result of the action. + """ + if action == "setup": + { + "user doesn't exists": mock_user_doesnt_exist, + "user exists": mock_user_exists, + "the specified user doesn't exist": mock_post_request_to_create_user, + "user is present in DB": mock_delete_request_to_delete_user, + }[state]() + + if action == "teardown": + { + "user doesn't exists": verify_user_doesnt_exist_mock, + "user exists": verify_user_exists_mock, + "the specified user doesn't exist": verify_mock_post_request_to_create_user, + "user is present in DB": verify_mock_delete_request_to_delete_user, + }[state]() def mock_user_doesnt_exist() -> None: diff --git a/examples/tests/v3/test_03_message_provider.py b/examples/tests/v3/test_03_message_provider.py index e9473c496..a6462947c 100644 --- a/examples/tests/v3/test_03_message_provider.py +++ b/examples/tests/v3/test_03_message_provider.py @@ -7,16 +7,18 @@ from __future__ import annotations +import json from pathlib import Path +from typing import Any from unittest.mock import MagicMock from examples.src.message_producer import FileSystemMessageProducer -from examples.tests.v3.provider_server import start_provider from pact.v3 import Verifier +from pact.v3.types import Message PACT_DIR = (Path(__file__).parent.parent.parent / "pacts").resolve() -responses: dict[str, dict[str, str]] = { +RESPONSES: dict[str, dict[str, str]] = { "a request to write test.txt": { "function_name": "send_write_event", }, @@ -25,49 +27,38 @@ }, } -CURRENT_STATE: str | None = None +def message_producer(message: str, metadata: dict[str, Any] | None) -> Message: # noqa: ARG001 + """ + Function to produce a message for the provider. -def message_producer_function() -> tuple[str, str]: - producer = FileSystemMessageProducer() - producer.queue = MagicMock() - - assert CURRENT_STATE is not None, "State is not set" - function_name = responses.get(CURRENT_STATE, {}).get("function_name") - assert function_name is not None, "Function name could not be found" - producer_function = getattr(producer, function_name) - - if producer_function.__name__ == "send_write_event": - producer_function("provider_file_name.txt", "Hello, world!") - elif producer_function.__name__ == "send_read_event": - producer_function("provider_file_name.txt") + This specific implementation is rather simple as it returns static content. + In fact, a straight mapping of the message names to the expected responses + could be given to the message handler directly. However, this function is + provided to demonstrate the capability of the message handler to be very + generic. - return producer.queue.send.call_args[0][0], "application/json" + Args: + message: + The message name. + metadata: + Any metadata associated with the message which can be used to + determine the response. + """ + producer = FileSystemMessageProducer() + producer.queue = MagicMock() -def state_provider_function(state_name: str) -> None: - global CURRENT_STATE # noqa: PLW0603 - CURRENT_STATE = state_name + return Message( + contents=json.dumps(RESPONSES[message]).encode("utf-8"), + content_type="application/json", + metadata=None, + ) def test_producer() -> None: """ Test the message producer. """ - with start_provider( - handler_module=__name__, - handler_function="message_producer_function", - state_provider_module=__name__, - state_provider_function="state_provider_function", - ) as provider_url: - verifier = ( - Verifier("provider") - .add_transport(url=f"{provider_url}/produce_message") - .set_state( - provider_url / "set_provider_state", - teardown=True, - ) - .filter_consumers("v3_message_consumer") - .add_source(PACT_DIR / "v3_message_consumer-v3_message_provider.json") - ) - verifier.verify() + verifier = Verifier("provider").message_handler(message_producer) + verifier.verify() From 72e63b9887d8f1b48d93ed76c1175bda433d1869 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 5 Dec 2024 13:55:46 +1100 Subject: [PATCH 0664/1376] chore: move matchers test out of examples Signed-off-by: JP-Ellis --- examples/tests/v3/basic_flask_server.py | 148 --------------------- {examples/tests => tests}/v3/test_match.py | 142 +++++++++++++++++++- 2 files changed, 140 insertions(+), 150 deletions(-) delete mode 100644 examples/tests/v3/basic_flask_server.py rename {examples/tests => tests}/v3/test_match.py (54%) diff --git a/examples/tests/v3/basic_flask_server.py b/examples/tests/v3/basic_flask_server.py deleted file mode 100644 index 1dfbd406f..000000000 --- a/examples/tests/v3/basic_flask_server.py +++ /dev/null @@ -1,148 +0,0 @@ -""" -Simple flask server for matcher example. -""" - -import logging -import re -import signal -import subprocess -import sys -import time -from collections.abc import Generator -from contextlib import contextmanager -from datetime import datetime -from pathlib import Path -from random import randint, uniform -from threading import Thread -from typing import NoReturn - -import requests -from yarl import URL - -from flask import Flask, Response, make_response - -logger = logging.getLogger(__name__) - - -@contextmanager -def start_provider() -> Generator[URL, None, None]: # noqa: C901 - """ - Start the provider app. - """ - process = subprocess.Popen( # noqa: S603 - [ - sys.executable, - Path(__file__), - ], - cwd=Path.cwd(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="utf-8", - ) - - pattern = re.compile(r" \* Running on (?P[^ ]+)") - while True: - if process.poll() is not None: - logger.error("Provider process exited with code %d", process.returncode) - logger.error( - "Provider stdout: %s", process.stdout.read() if process.stdout else "" - ) - logger.error( - "Provider stderr: %s", process.stderr.read() if process.stderr else "" - ) - msg = f"Provider process exited with code {process.returncode}" - raise RuntimeError(msg) - if ( - process.stderr - and (line := process.stderr.readline()) - and (match := pattern.match(line)) - ): - break - time.sleep(0.1) - - url = URL(match.group("url")) - logger.debug("Provider started on %s", url) - for _ in range(50): - try: - response = requests.get(str(url / "_test" / "ping"), timeout=1) - assert response.text == "pong" - break - except (requests.RequestException, AssertionError): - time.sleep(0.1) - continue - else: - msg = "Failed to ping provider" - raise RuntimeError(msg) - - def redirect() -> NoReturn: - while True: - if process.stdout: - while line := process.stdout.readline(): - logger.debug("Provider stdout: %s", line.strip()) - if process.stderr: - while line := process.stderr.readline(): - logger.debug("Provider stderr: %s", line.strip()) - - thread = Thread(target=redirect, daemon=True) - thread.start() - - try: - yield url - finally: - process.send_signal(signal.SIGINT) - - -if __name__ == "__main__": - app = Flask(__name__) - - @app.route("/path/to/") - def hello_world(test_id: int) -> Response: - random_regex_matches = "1-8 digits: 12345678, 1-8 random letters abcdefgh" - response = make_response({ - "response": { - "id": test_id, - "regexMatches": "must end with 'hello world'", - "randomRegexMatches": random_regex_matches, - "integerMatches": test_id, - "decimalMatches": round(uniform(0, 9), 3), # noqa: S311 - "booleanMatches": True, - "randomIntegerMatches": randint(1, 100), # noqa: S311 - "randomDecimalMatches": round(uniform(0, 9), 1), # noqa: S311 - "randomStringMatches": "hi there", - "includeMatches": "hello world", - "includeWithGeneratorMatches": "say 'hello world' for me", - "minMaxArrayMatches": [ - round(uniform(0, 9), 1) # noqa: S311 - for _ in range(randint(3, 5)) # noqa: S311 - ], - "arrayContainingMatches": [randint(1, 100), randint(1, 100)], # noqa: S311 - "numbers": { - "intMatches": 42, - "floatMatches": 3.1415, - "intGeneratorMatches": randint(1, 100), # noqa: S311, - "decimalGeneratorMatches": round(uniform(10, 99), 2), # noqa: S311 - }, - "dateMatches": "1999-12-31", - "randomDateMatches": "1999-12-31", - "timeMatches": "12:34:56", - "timestampMatches": datetime.now().isoformat(), # noqa: DTZ005 - "nullMatches": None, - "eachKeyMatches": { - "id_1": { - "name": "John Doe", - }, - "id_2": { - "name": "Jane Doe", - }, - }, - } - }) - response.headers["SpecialHeader"] = "Special: Hi" - return response - - @app.get("/_test/ping") - def ping() -> str: - """Simple ping endpoint for testing.""" - return "pong" - - app.run() diff --git a/examples/tests/v3/test_match.py b/tests/v3/test_match.py similarity index 54% rename from examples/tests/v3/test_match.py rename to tests/v3/test_match.py index 02080b1f6..2c003f50c 100644 --- a/examples/tests/v3/test_match.py +++ b/tests/v3/test_match.py @@ -2,14 +2,152 @@ Example test to show usage of matchers (and generators by extension). """ +import logging import re +import signal +import subprocess +import sys +import time +from collections.abc import Generator +from contextlib import contextmanager +from datetime import datetime from pathlib import Path +from random import randint, uniform +from threading import Thread +from typing import NoReturn import requests +from flask import Flask, Response, make_response +from yarl import URL -from examples.tests.v3.basic_flask_server import start_provider from pact.v3 import Pact, Verifier, generate, match +logger = logging.getLogger(__name__) + + +@contextmanager +def start_provider() -> Generator[URL, None, None]: # noqa: C901 + """ + Start the provider app. + """ + process = subprocess.Popen( # noqa: S603 + [ + sys.executable, + Path(__file__), + ], + cwd=Path.cwd(), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + + pattern = re.compile(r" \* Running on (?P[^ ]+)") + while True: + if process.poll() is not None: + logger.error("Provider process exited with code %d", process.returncode) + logger.error( + "Provider stdout: %s", process.stdout.read() if process.stdout else "" + ) + logger.error( + "Provider stderr: %s", process.stderr.read() if process.stderr else "" + ) + msg = f"Provider process exited with code {process.returncode}" + raise RuntimeError(msg) + if ( + process.stderr + and (line := process.stderr.readline()) + and (match := pattern.match(line)) + ): + break + time.sleep(0.1) + + url = URL(match.group("url")) + logger.debug("Provider started on %s", url) + for _ in range(50): + try: + response = requests.get(str(url / "_test" / "ping"), timeout=1) + assert response.text == "pong" + break + except (requests.RequestException, AssertionError): + time.sleep(0.1) + continue + else: + msg = "Failed to ping provider" + raise RuntimeError(msg) + + def redirect() -> NoReturn: + while True: + if process.stdout: + while line := process.stdout.readline(): + logger.debug("Provider stdout: %s", line.strip()) + if process.stderr: + while line := process.stderr.readline(): + logger.debug("Provider stderr: %s", line.strip()) + + thread = Thread(target=redirect, daemon=True) + thread.start() + + try: + yield url + finally: + process.send_signal(signal.SIGINT) + + +if __name__ == "__main__": + app = Flask(__name__) + + @app.route("/path/to/") + def hello_world(test_id: int) -> Response: + random_regex_matches = "1-8 digits: 12345678, 1-8 random letters abcdefgh" + response = make_response({ + "response": { + "id": test_id, + "regexMatches": "must end with 'hello world'", + "randomRegexMatches": random_regex_matches, + "integerMatches": test_id, + "decimalMatches": round(uniform(0, 9), 3), # noqa: S311 + "booleanMatches": True, + "randomIntegerMatches": randint(1, 100), # noqa: S311 + "randomDecimalMatches": round(uniform(0, 9), 1), # noqa: S311 + "randomStringMatches": "hi there", + "includeMatches": "hello world", + "includeWithGeneratorMatches": "say 'hello world' for me", + "minMaxArrayMatches": [ + round(uniform(0, 9), 1) # noqa: S311 + for _ in range(randint(3, 5)) # noqa: S311 + ], + "arrayContainingMatches": [randint(1, 100), randint(1, 100)], # noqa: S311 + "numbers": { + "intMatches": 42, + "floatMatches": 3.1415, + "intGeneratorMatches": randint(1, 100), # noqa: S311, + "decimalGeneratorMatches": round(uniform(10, 99), 2), # noqa: S311 + }, + "dateMatches": "1999-12-31", + "randomDateMatches": "1999-12-31", + "timeMatches": "12:34:56", + "timestampMatches": datetime.now().isoformat(), # noqa: DTZ005 + "nullMatches": None, + "eachKeyMatches": { + "id_1": { + "name": "John Doe", + }, + "id_2": { + "name": "Jane Doe", + }, + }, + } + }) + response.headers["SpecialHeader"] = "Special: Hi" + return response + + @app.get("/_test/ping") + def ping() -> str: + """Simple ping endpoint for testing.""" + return "pong" + + app.run() + def test_matchers() -> None: pact_dir = Path(Path(__file__).parent.parent.parent / "pacts") @@ -127,7 +265,7 @@ def test_matchers() -> None: pact.write_file(pact_dir, overwrite=True) with start_provider() as url: verifier = ( - Verifier("My Provider") + Verifier("My Provider", host="127.0.0.1") .add_transport(url=url) .add_source(pact_dir / "consumer-provider.json") ) From 173ff62612de239768da3ed6d71af6b7ce87884c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 5 Dec 2024 13:56:18 +1100 Subject: [PATCH 0665/1376] chore: adjust tests based on new implementation Signed-off-by: JP-Ellis --- tests/v3/test_server.py | 117 ++++++++++------------------------------ 1 file changed, 29 insertions(+), 88 deletions(-) diff --git a/tests/v3/test_server.py b/tests/v3/test_server.py index 7de91e006..eb3fded09 100644 --- a/tests/v3/test_server.py +++ b/tests/v3/test_server.py @@ -2,7 +2,6 @@ Tests for `pact.v3._server` module. """ -import base64 import json from unittest.mock import MagicMock @@ -12,17 +11,17 @@ from pact.v3._server import MessageProducer, StateCallback -def test_relay_default_init() -> None: +def test_message_default_init() -> None: handler = MagicMock() server = MessageProducer(handler) assert server.host == "localhost" assert server.port > 1024 # Random non-privileged port - assert server.url == f"http://{server.host}:{server.port}" + assert server.url == f"http://{server.host}:{server.port}/_pact/message" @pytest.mark.asyncio -async def test_relay_invalid_path_http() -> None: +async def test_message_invalid_path_http() -> None: handler = MagicMock(return_value="Not OK") server = MessageProducer(handler) @@ -34,75 +33,42 @@ async def test_relay_invalid_path_http() -> None: @pytest.mark.asyncio -async def test_relay_get_http() -> None: +async def test_message_get_http() -> None: handler = MagicMock(return_value=b"Pact Python is awesome!") server = MessageProducer(handler) with server: async with aiohttp.ClientSession() as session: - async with session.get(server.url + "/_pact/message") as response: - assert response.status == 200 - assert await response.text() == "Pact Python is awesome!" - - handler.assert_called_once() - assert handler.call_args.args == (None, None) - - -@pytest.mark.asyncio -async def test_relay_post_http() -> None: - handler = MagicMock(return_value=b"Pact Python is awesome!") - server = MessageProducer(handler) - - with server: - async with aiohttp.ClientSession() as session: - async with session.post( - server.url + "/_pact/message", - data='{"hello": "world"}', - ) as response: - assert response.status == 200 - assert await response.text() == "Pact Python is awesome!" - - handler.assert_called_once() - assert handler.call_args.args == (b'{"hello": "world"}', None) - - -@pytest.mark.asyncio -async def test_relay_get_with_metadata() -> None: - handler = MagicMock(return_value=b"Pact Python is awesome!") - server = MessageProducer(handler) - metadata = base64.b64encode(json.dumps({"key": "value"}).encode()).decode() - - with server: - async with aiohttp.ClientSession() as session: - async with session.get( - server.url + "/_pact/message", - headers={"Pact-Message-Metadata": metadata}, - ) as response: - assert response.status == 200 - assert await response.text() == "Pact Python is awesome!" + async with session.get(server.url) as response: + assert response.status == 404 - handler.assert_called_once() - assert handler.call_args.args == (None, {"key": "value"}) + handler.assert_not_called() @pytest.mark.asyncio -async def test_relay_post_with_metadata() -> None: - handler = MagicMock(return_value=b"Pact Python is awesome!") +async def test_message_post_http() -> None: + handler = MagicMock( + return_value={ + "contents": json.dumps({"hello": "world"}).encode(), + "metadata": None, + "content_type": "application/json", + } + ) server = MessageProducer(handler) - metadata = base64.b64encode(json.dumps({"key": "value"}).encode()).decode() with server: async with aiohttp.ClientSession() as session: async with session.post( - server.url + "/_pact/message", - data='{"hello": "world"}', - headers={"Pact-Message-Metadata": metadata}, + server.url, + data=json.dumps({ + "description": "A simple message", + }), ) as response: assert response.status == 200 - assert await response.text() == "Pact Python is awesome!" + assert await response.text() == '{"hello": "world"}' handler.assert_called_once() - assert handler.call_args.args == (b'{"hello": "world"}', {"key": "value"}) + assert handler.call_args.args == ("A simple message", {}) def test_callback_default_init() -> None: @@ -111,7 +77,7 @@ def test_callback_default_init() -> None: assert server.host == "localhost" assert server.port > 1024 # Random non-privileged port - assert server.url == f"http://{server.host}:{server.port}" + assert server.url == f"http://{server.host}:{server.port}/_pact/state" @pytest.mark.asyncio @@ -133,52 +99,27 @@ async def test_callback_get_http() -> None: with server: async with aiohttp.ClientSession() as session: - async with session.get(server.url + "/_pact/state") as response: + async with session.get(server.url) as response: assert response.status == 404 handler.assert_not_called() @pytest.mark.asyncio -async def test_callback_post_query() -> None: - handler = MagicMock(return_value=None) - server = StateCallback(handler) - - with server: - async with aiohttp.ClientSession() as session: - async with session.post( - server.url + "/_pact/state", - params={ - "state": "user exists", - "action": "setup", - "foo": "bar", - "1": 2, - }, - ) as response: - assert response.status == 200 - - handler.assert_called_once() - assert handler.call_args.args == ( - "user exists", - "setup", - {"foo": "bar", "1": "2"}, - ) - - -@pytest.mark.asyncio -async def test_callback_post_body() -> None: +async def test_callback_post() -> None: handler = MagicMock(return_value=None) server = StateCallback(handler) with server: async with aiohttp.ClientSession() as session: async with session.post( - server.url + "/_pact/state", + server.url, json={ "state": "user exists", "action": "setup", - "foo": "bar", - "1": 2, + "params": { + "id": 123, + }, }, ) as response: assert response.status == 200 @@ -187,5 +128,5 @@ async def test_callback_post_body() -> None: assert handler.call_args.args == ( "user exists", "setup", - {"foo": "bar", "1": 2}, + {"id": 123}, ) From 475966d6ccbec2735b8e5dd2f3fb64b0ce9d46a4 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 5 Dec 2024 16:10:22 +1100 Subject: [PATCH 0666/1376] chore: remove dead code Signed-off-by: JP-Ellis --- src/pact/v3/_server.py | 54 +++++------------------------------------- 1 file changed, 6 insertions(+), 48 deletions(-) diff --git a/src/pact/v3/_server.py b/src/pact/v3/_server.py index bb6301af7..6c81d4e0d 100644 --- a/src/pact/v3/_server.py +++ b/src/pact/v3/_server.py @@ -22,7 +22,6 @@ from __future__ import annotations import base64 -import binascii import json import logging import warnings @@ -30,7 +29,7 @@ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from threading import Thread from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar -from urllib.parse import parse_qs, urlparse +from urllib.parse import urlparse from pact import __version__ from pact.v3._util import find_free_port @@ -238,39 +237,6 @@ def version_string(self) -> str: """ return f"Pact Python Message Relay/{__version__}" - def _process(self) -> tuple[bytes | None, dict[str, str] | None]: - """ - Process the request. - - Read the body and headers from the request and perform some common logic - shared between GET and POST requests. - - Returns: - body: - The body of the request as a byte string, if present. - - metadata: - The metadata of the request, if present. - """ - if content_length := self.headers.get("Content-Length"): - body = self.rfile.read(int(content_length)) - else: - body = None - - if data := self.headers.get("Pact-Message-Metadata"): - try: - metadata = json.loads(base64.b64decode(data)) - except binascii.Error as err: - msg = "Unable to base64 decode Pact metadata header." - raise RuntimeError(msg) from err - except json.JSONDecodeError as err: - msg = "Unable to JSON decode Pact metadata header." - raise RuntimeError(msg) from err - else: - return body, metadata - - return body, None - def do_POST(self) -> None: # noqa: N802 """ Handle a POST request. @@ -476,19 +442,11 @@ def do_POST(self) -> None: # noqa: N802 self.send_error(404, "Not Found") return - if query := url.query: - data: dict[str, Any] = parse_qs(query) - # Convert single-element lists to single values - for k, v in data.items(): - if isinstance(v, list) and len(v) == 1: - data[k] = v[0] - - else: - content_length = self.headers.get("Content-Length") - if not content_length: - self.send_error(400, "Bad Request") - return - data = json.loads(self.rfile.read(int(content_length))) + content_length = self.headers.get("Content-Length") + if not content_length: + self.send_error(400, "Bad Request") + return + data = json.loads(self.rfile.read(int(content_length))) state = data.pop("state") action = data.pop("action") From b6a537154416a272dfb052b23a138bea3b557973 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 23 Dec 2024 15:00:00 +1100 Subject: [PATCH 0667/1376] docs: fix minor typos Signed-off-by: JP-Ellis --- examples/tests/test_01_provider_fastapi.py | 2 +- examples/tests/test_01_provider_flask.py | 2 +- examples/tests/v3/test_01_fastapi_provider.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/tests/test_01_provider_fastapi.py index f0328b4f2..3fab5d9c3 100644 --- a/examples/tests/test_01_provider_fastapi.py +++ b/examples/tests/test_01_provider_fastapi.py @@ -58,7 +58,7 @@ async def mock_pact_provider_states( """ Define the provider state. - For Pact to be able to correctly tests compliance with the contract, the + For Pact to be able to correctly test compliance with the contract, the internal state of the provider needs to be set up correctly. Naively, this would be achieved by setting up the database with the correct data for the test, but this can be slow and error-prone. Instead this is best achieved by diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py index bc6d2e037..347d57e7f 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/tests/test_01_provider_flask.py @@ -48,7 +48,7 @@ async def mock_pact_provider_states() -> dict[str, str | None]: """ Define the provider state. - For Pact to be able to correctly tests compliance with the contract, the + For Pact to be able to correctly test compliance with the contract, the internal state of the provider needs to be set up correctly. Naively, this would be achieved by setting up the database with the correct data for the test, but this can be slow and error-prone. Instead this is best achieved by diff --git a/examples/tests/v3/test_01_fastapi_provider.py b/examples/tests/v3/test_01_fastapi_provider.py index 2ef49a213..6bdecf580 100644 --- a/examples/tests/v3/test_01_fastapi_provider.py +++ b/examples/tests/v3/test_01_fastapi_provider.py @@ -18,7 +18,7 @@ side effects, the provider's database calls are mocked out using functionalities from `unittest.mock`. -Note that Pact requires tat the provider be running on an accessible URL. This +Note that Pact requires that the provider be running on an accessible URL. This means that FastAPI's [`TestClient`][fastapi.testclient.TestClient] cannot be used to test the provider. Instead, the provider is run in a separate thread using Python's [`Thread`][threading.Thread] class. @@ -151,7 +151,7 @@ def provider_state_handler( """ Handler for the provider state callback. - For Pact to be able to correctly tests compliance with the contract, the + For Pact to be able to correctly test compliance with the contract, the internal state of the provider needs to be set up correctly. For example, if the consumer expects a user to exist in the database, the provider needs to have a user with the given ID in the database. From f61a09348330f100d6aeb11c3260b51181d08821 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 30 Dec 2024 14:30:55 +1100 Subject: [PATCH 0668/1376] chore: fix compatibility with 3.9, 3.10 The `typing.Self` annotation was only introduced in Python 3.11, and therefore we have to rely on the typing extensions for versions 3.9 and 3.10. There are also issues with TypeAliases and the use of the `|` operator. Signed-off-by: JP-Ellis --- src/pact/v3/_server.py | 4 +++- src/pact/v3/types.py | 16 ++++++++-------- src/pact/v3/verifier.py | 4 +++- tests/v3/compatibility_suite/util/provider.py | 3 ++- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/pact/v3/_server.py b/src/pact/v3/_server.py index 6c81d4e0d..299531109 100644 --- a/src/pact/v3/_server.py +++ b/src/pact/v3/_server.py @@ -28,9 +28,11 @@ from collections.abc import Callable from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from threading import Thread -from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar from urllib.parse import urlparse +from typing_extensions import Self + from pact import __version__ from pact.v3._util import find_free_port diff --git a/src/pact/v3/types.py b/src/pact/v3/types.py index 5850e434c..7a40062d0 100644 --- a/src/pact/v3/types.py +++ b/src/pact/v3/types.py @@ -9,7 +9,7 @@ from __future__ import annotations from collections.abc import Callable -from typing import Any, TypedDict +from typing import Any, Optional, TypedDict, Union from typing_extensions import TypeAlias from yarl import URL @@ -57,7 +57,7 @@ class Message(TypedDict): """ -MessageProducerFull: TypeAlias = Callable[[str, dict[str, Any] | None], Message] +MessageProducerFull: TypeAlias = Callable[[str, Optional[dict[str, Any]]], Message] """ Full message producer signature. @@ -69,7 +69,7 @@ class Message(TypedDict): The function must return a `bytes` object. """ -MessageProducerNoName: TypeAlias = Callable[[dict[str, Any] | None], Message] +MessageProducerNoName: TypeAlias = Callable[[Optional[dict[str, Any]]], Message] """ Message producer signature without the name. @@ -83,7 +83,7 @@ class Message(TypedDict): functions. """ -StateHandlerFull: TypeAlias = Callable[[str, str, dict[str, Any] | None], None] +StateHandlerFull: TypeAlias = Callable[[str, str, Optional[dict[str, Any]]], None] """ Full state handler signature. @@ -93,7 +93,7 @@ class Message(TypedDict): 2. The action (either `setup` or `teardown`), as a string. 3. A dictionary of parameters, or `None` if no parameters are provided. """ -StateHandlerNoAction: TypeAlias = Callable[[str, dict[str, Any] | None], None] +StateHandlerNoAction: TypeAlias = Callable[[str, Optional[dict[str, Any]]], None] """ State handler signature without the action. @@ -102,7 +102,7 @@ class Message(TypedDict): 1. The state name, as a string. 2. A dictionary of parameters, or `None` if no parameters are provided. """ -StateHandlerNoState: TypeAlias = Callable[[str, dict[str, Any] | None], None] +StateHandlerNoState: TypeAlias = Callable[[str, Optional[dict[str, Any]]], None] """ State handler signature without the state. @@ -114,7 +114,7 @@ class Message(TypedDict): This function must be provided as part of a dictionary mapping state names to functions. """ -StateHandlerNoActionNoState: TypeAlias = Callable[[dict[str, Any] | None], None] +StateHandlerNoActionNoState: TypeAlias = Callable[[Optional[dict[str, Any]]], None] """ State handler signature without the state or action. @@ -125,7 +125,7 @@ class Message(TypedDict): This function must be provided as part of a dictionary mapping state names to functions. """ -StateHandlerUrl: TypeAlias = str | URL +StateHandlerUrl: TypeAlias = Union[str, URL] """ State handler URL signature. diff --git a/src/pact/v3/verifier.py b/src/pact/v3/verifier.py index f464bde5a..0821418ea 100644 --- a/src/pact/v3/verifier.py +++ b/src/pact/v3/verifier.py @@ -573,7 +573,9 @@ def state_handler( providing one or more handler functions; and it must be set to a boolean if providing a URL. """ - if isinstance(handler, StateHandlerUrl): + # A tuple is required instead of `StateHandlerUrl` for support for + # Python 3.9. This should be changed to `StateHandlerUrl` in the future. + if isinstance(handler, (str, URL)): if body is None: msg = "The `body` parameter must be a boolean when providing a URL" raise ValueError(msg) diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index b733e4466..f678c92c5 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -26,13 +26,14 @@ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from io import BytesIO from threading import Thread -from typing import TYPE_CHECKING, Any, ClassVar, Self, TypedDict +from typing import TYPE_CHECKING, Any, ClassVar, TypedDict from unittest.mock import MagicMock import pytest import requests from multidict import CIMultiDict from pytest_bdd import given, parsers, then, when +from typing_extensions import Self from yarl import URL import pact.constants # type: ignore[import-untyped] From 86110ced56def73d5f34f4c3d3d271bdb5b18155 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 30 Dec 2024 15:02:31 +1100 Subject: [PATCH 0669/1376] chore: add pytest-rerunfailures Unfortunately, CI is flaky and this is difficult to replicate locally. Signed-off-by: JP-Ellis --- pyproject.toml | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 16dd46529..079ff7955 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,17 +76,18 @@ devel-docs = [ "mkdocstrings[python] ~= 0.23", ] devel-test = [ - "aiohttp[speedups] ~=3.0", - "coverage[toml] ~=7.0", - "flask[async] ~=3.0", - "httpx ~=0.0", - "mock ~=5.0", - "pytest-asyncio ~=0.0", - "pytest-bdd ~=8.0", - "pytest-cov ~=6.0", - "pytest-xdist ~=3.0", - "pytest ~=8.0", - "testcontainers ~=4.0", + "aiohttp[speedups] ~=3.0", + "coverage[toml] ~=7.0", + "flask[async] ~=3.0", + "httpx ~=0.0", + "mock ~=5.0", + "pytest-asyncio ~=0.0", + "pytest-bdd ~=8.0", + "pytest-cov ~=6.0", + "pytest-rerunfailures ~=15.0", + "pytest-xdist ~=3.0", + "pytest ~=8.0", + "testcontainers ~=4.0", ] devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.4"] @@ -191,6 +192,9 @@ addopts = [ # Xdist options "--numprocesses=logical", "--dist=worksteal", + # Rerun options + "--reruns=3", + "--rerun-except=assert", ] filterwarnings = [ "ignore::DeprecationWarning:examples", From 6c5607381e5f92470e401f53dc8106a0fba50cf6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 30 Dec 2024 15:10:40 +1100 Subject: [PATCH 0670/1376] chore: fix windows compatibility The `SIGINT` signal is _not_ supported on Windows, so it is replaced with the cross-platform `process.terminate` method. Signed-off-by: JP-Ellis --- tests/v3/test_match.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/v3/test_match.py b/tests/v3/test_match.py index 2c003f50c..9ed6581f7 100644 --- a/tests/v3/test_match.py +++ b/tests/v3/test_match.py @@ -4,7 +4,6 @@ import logging import re -import signal import subprocess import sys import time @@ -90,7 +89,7 @@ def redirect() -> NoReturn: try: yield url finally: - process.send_signal(signal.SIGINT) + process.terminate() if __name__ == "__main__": From b6ce32c84ca0ef6fea83ae6fec394e43b95da541 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 19:24:07 +0000 Subject: [PATCH 0671/1376] fix(deps): update dependency mypy to v1.14.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 079ff7955..2881896b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ pact-verifier = "pact.cli.verify:main" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. devel-types = [ - "mypy ==1.14.0", + "mypy ==1.14.1", "types-cffi ~=1.0", "types-requests ~=2.0", ] From bd790c5f4710ef69a5902863dbbb6cf9ed951736 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 30 Dec 2024 17:36:33 +1100 Subject: [PATCH 0672/1376] docs(blog): add functional arguments post Signed-off-by: JP-Ellis --- .../posts/2024/12-30 functional arguments.md | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 docs/blog/posts/2024/12-30 functional arguments.md diff --git a/docs/blog/posts/2024/12-30 functional arguments.md b/docs/blog/posts/2024/12-30 functional arguments.md new file mode 100644 index 000000000..999fdaa77 --- /dev/null +++ b/docs/blog/posts/2024/12-30 functional arguments.md @@ -0,0 +1,317 @@ +--- +authors: + - JP-Ellis +date: + created: 2024-12-30 +--- + +# Functional Arguments + +Today marks the [release of Pact Python version 2.3.0](https://github.com/pact-foundation/pact-python/releases/tag/v2.3.0). Among the many incremental improvements, the most significant is the [support of functional arguments](https://github.com/pact-foundation/pact-python/pull/890). This feature provides an improved user experience for providers, and also introduces several breaking changes to the `pact.v3` preview. + +If you just want to update your existing code to the latest version without any other changes, you can skip to the [Breaking Changes TL;DR](#breaking-changes-tldr) section. Otherwise, key new features now allow you to [define provider states using functions](#functional-state-handler) and [use functions to produce messages](#functional-message-producer). + + +## Breaking Changes TL;DR + +While I highly recommend everyone experiment with the new possibilities that functional arguments bring, if you merely want to update your existing code to the latest version, here is a quick summary of the breaking changes: + +- The `Verifier` initialization now requires a `name` argument which is used to identify the provider in the Pact file. This information was previously given through the `set_info` method which has been removed. The change required is: + + + === "Before" + + ```python + verifier = Verifier() + verifier.set_info("provider_name", ...) + ``` + + === "After" + + ```python + verifier = Verifier(name="provider_name") + ``` + + +- The `Verifier.set_info` method has been entirely removed. Instead, the `Verifier` class now has a `name` attribute which is set during initialization for the provider's name, and the transport information that was previously set is now passed through the `add_transport` method: + + + === "Before" + + ```python + verifier = Verifier() + verifier.set_info( + "provider_name", + url="http://localhost:8123", + ) + ``` + + === "After" + + ```python + verifier = Verifier("provider_name") + verifier.add_transport(url="http://localhost:8123") + ``` + + +- The `Verifier.set_state` function has been renamed to `Verifier.state_handler`. Furthermore, if you have already set up a custom endpoint to handle provider state changes, you will now need to explicitly state whether your endpoint expects data to be passed through the query string or through a `POST` body: + + + === "Before" + + ```python + verifier = Verifier() + verifier.set_state("http://localhost:8123/provider-states") + ``` + + === "After" + + ```python + verifier = Verifier() + verifier.state_handler( + "http://localhost:8123/provider-states", + body=False, # the previous default must be explicitly set + ) + ``` + + +## Functional State Handler + +When a Pact interaction is to be verified, the consumer will often expect the provider to be in a particular state. For example, a consumer might want to fetch a specific user's details, and therefore the provider must be in a state where that user exists. The user experience prior to version 2.3.0 was less than ideal: the developers behind the provider had to set up a custom endpoint to handle the state changes, and then pass the URL of that endpoint to the `Verifier` object. + +The new `state_handler` method replaces the `set_state` method and simplifies this process significantly by allowing functions to be called to set up and tear down the provider state. For example, the following code snippet demonstrates how to set up a state handler that uses a custom endpoint to handle the provider state: + + +???+ example + + ```python + from pact.v3 import Verifier + + def provider_state_callback( + name: str, # (1) + action: Literal["setup", "teardown"], # (2) + params: dict[str, Any] | None, # (3) + ) -> None: + """ + Callback to set up and tear down the provider state. + + Args: + name: + The name of the provider state. For example, `"a user with ID 123 + exists"` or `"no users exist"`. + + action: + The action to perform. Either `"setup"` or `"teardown"`. The setup + action should create the provider state, and the teardown action + should remove it. + + params: + If the provider state has additional parameters, they will be + passed here. For example, instead of `"a user with ID 123 exists"`, + the provider state might be `"a user with the given ID exists"` and + the specific ID would be passed in the params. + """ + ... + + def test_provider(): + verifier = Verifier("provider_name") + verifier.state_handler(provider_state_callback, teardown=True) + ``` + + 1. The `name` parameter is the name of the provider state. For example, `"a user with ID 123 exists"` or `"no users exist"`. If you instead use a mapping of provider state names to functions, this parameter is not passed to the function. + 2. The `action` parameter is either `"setup"` or `"teardown"`. The setup action should create the provider state, and the teardown action should remove it. If you specify `teardown=False`, then the `action` parameter is _not_ passed to the callback function. + 3. The `params` parameter is a dictionary of additional parameters that the provider state requires. For example, instead of `"a user with ID 123 exists"`, the provider state might be `"a user with the given ID exists"` and the specific ID would be passed in the `params` dictionary. Note that `params` is always present, but may be `None` if no parameters are specified by the consumer. + + +This snippet showcases a way to set up the provider state with a function that is fully parameterized. The `state_handler` method also handles the following scenarios: + +- If teardowns are never required, then one should specify `teardown=False` in which case the `action` parameter is _not_ passed to the callback function. + + + ??? example + + ```python + from pact.v3 import Verifier + + def provider_state_callback( + name: str, + params: dict[str, Any] | None, + ) -> None: + ... + + def test_provider(): + verifier = Verifier("provider_name") + verifier.state_handler(provider_state_callback, teardown=False) + ``` + + +- A mapping can be provided to the `state_handler` method with keys as the provider state names and values as the function to call. This can help to keep the code organized and to avoid a large number of `if` statements in the callback function. + + + ??? example + + ```python + from pact.v3 import Verifier + + def user_state_callback( + action: Literal["setup", "teardown"], + params: dict[str, Any] | None, + ) -> None: + ... + + def no_users_state_callback( + action: Literal["setup", "teardown"], + params: dict[str, Any] | None, + ) -> None: + ... + + def test_provider(): + verifier = Verifier("provider_name") + verifier.state_handler( + { + "a user with ID 123 exists": user_state_callback, + "no users exist": no_users_state_callback, + }, + ) + ``` + + +- Both scenarios can be combined, in which a mapping of provide state names to functions is provided, and the `teardown=False` option is specified. In this case, the function should expect only one argument: the `params` dictionary (which itself may be `None`). + + + ??? example + + ```python + from pact.v3 import Verifier + + def user_state_callback( + params: dict[str, Any] | None, + ) -> None: + ... + + def no_users_state_callback( + params: dict[str, Any] | None, + ) -> None: + ... + + def test_provider(): + verifier = Verifier("provider_name") + verifier.state_handler( + { + "a user with ID 123 exists": user_state_callback, + "no users exist": no_users_state_callback, + }, + teardown=False, + ) + ``` + +## Functional Message Producer + +In the messaging paradigm, the Pact consumer consumes the message produced by the provider (which is often referred to as the "producer"). As there are many and varied transport mechanisms for messages, Pact approaches the verification of messages in a transport-agnostic way. Previously, the provider would need to define a special HTTP endpoint to generate the message, and then pass the URL of that endpoint to the `Verifier` object. This process was cumbersome, especially considering that most producers do not expose any HTTP endpoints to begin with. + +With the update to 2.3.0, the `Verifier` class has a new `message_handler` method which allows the provider to pass a function that generates the message. This function is called by the `Verifier` object when it needs a message to verify. The following code snippet demonstrates how to set up a message producer that uses a custom endpoint to generate the message: + + +???+ example + + ```python + from pact.v3 import Verifier + from pact.v3.types import Message + + def message_producer_callback( + name: str, # (1) + params: dict[str, Any] | None, # (2) + ) -> Message: + """ + Callback to produce the message that the consumer expects. + + Args: + name: + The name of the message. For example `"request to delete a user"`. + + params: + If the message has additional parameters, they will be passed here. + For example, one could specify the user ID to delete in the + parameters instead of the message. + + Returns: + The message that the consumer expects. + """ + ... + + def test_provider(): + verifier = Verifier("provider_name") + verifier.message_handler(message_producer_callback) + ``` + + 1. The `name` parameter is the name of the message. For example, `"request to delete a user"`. If you instead use a mapping of message names to functions, this parameter is not passed to the function. + 2. The `params` parameter is a dictionary of additional parameters that the message requires. For example, one could specify the user ID to delete in the parameters instead of the message. Note that `params` is always present, but may be `None` if no parameters are specified by the consumer. + + +The output of the callback function should be an instance of the `Message` type. This is a simple [TypedDict][typing.TypedDict] that represents the message that the consumer expects and can be specified as a simple dictionary, or with typing hints through the `Message` constructor: + + +=== "With typing hints" + + ```python + from pact.v3.types import Message + + def message_producer_callback( + name: str, + params: dict[str, Any] | None, + ) -> Message: + assert name == "request to delete a user" + return Message( + contents=json.dumps({ + "action": "delete_user", + "user_id": "123", + }).encode("utf-8"), + metadata=None, + content_type="application/json", + ) + ``` + +=== "Without typing hints" + + ```python + def message_producer_callback( + name: str, + params: dict[str, Any] | None, + ) -> dict[str, Any]: + assert name == "request to delete a user" + return { + "contents": json.dumps({ + "action": "delete_user", + "user_id": "123", + }).encode("utf-8"), + "metadata": None, + "content_type": "application/json", + } + ``` + +In much the same way as the `state_handler` method, the `message_handler` method can also accept a mapping of message names to functions or raw messages. The function should expect only one argument: the `params` dictionary (which itself may be `None`); or if the message is static, the message can be provided directly: + + +???+ example + + ```python + from pact.v3 import Verifier + from pact.v3.types import Message + + def delete_user_message(params: dict[str, Any] | None) -> Message: + ... + + def test_provider(): + verifier = Verifier("provider_name") + verifier.message_handler( + { + "request to delete a user": delete_user_message, + "create user": { + "contents": b"some message", + "metadata": None, + "content_type": "text/plain", + }, + }, + ) + ``` + From 46216712ef3ee82817ffe349e26f5f511af56ef5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 31 Dec 2024 10:24:23 +1100 Subject: [PATCH 0673/1376] chore(ci): automerge renovate PRs Signed-off-by: JP-Ellis --- .github/renovate.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json b/.github/renovate.json index c821376d9..bf19f907b 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -9,6 +9,7 @@ }, "prHourlyLimit": 0, "prConcurrentLimit": 0, + "automerge": true, "packageRules": [ { "groupName": "Ruff", From 5a71da0b8eb50196a7be064ea2ffcbb27bf5081f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 31 Dec 2024 10:26:40 +1100 Subject: [PATCH 0674/1376] Revert "chore(deps): update softprops/action-gh-release action to v2.2.0" This reverts commit 15f61caeb9dfa829b4548895b2deb1c9dda54f82. There's a bug in softprops 'action-gh-release' action which appears when uploading larger assets. Reverting to an earlier version until fix is merged. Ref: softprops/action-gh-release#562 Ref: softprops/action-gh-release#556 Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 713f6ecf2..15460c9f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -270,7 +270,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0 + uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 with: files: wheels/* body_path: ${{ runner.temp }}/changelog From f31786dd6c58c0f3af5c16be69451e208fca805b Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Tue, 31 Dec 2024 00:06:29 +0000 Subject: [PATCH 0675/1376] chore: update changelog v2.3.0 Signed-off-by: JP-Ellis --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce2db544c..43b1a28e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## v2.3.0 (2024-12-31) + +### BREAKING CHANGE + +- `message_handler` signature has been changed and expanded. +- `set_state` has been renamed to `state_handler`. If using a URL still, the `body` keyword argument is now a _required_ parameter. +- The provider name must be given as an argument of the `Verifier` constructor, instead of the first argument of the `set_info` method. +- The `set_info` verifier method is removed, with `add_transport` needing to be used. +- `pact.v3.util` has been renamed to `pact.v3._util` and is now private. +- The PactServer `__exit__` arguments no longer have leading underscores. This is typically handled by Python itself and therefore is unlikely to be a change for any user, unless the end user was calling the `__exit__` method explicitly _and_ using keyword arguments. + +### Feat + +- **v3**: further simplify message interface +- **v3**: add state handler server +- **v3**: integrate message relay server +- **v3**: add message relay and callback servers + ## v2.2.2 (2024-10-10) ### BREAKING CHANGE From a53699c4ff7a93e38b0956a9179a772bbbec4e6b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 22:56:00 +0000 Subject: [PATCH 0676/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.29.0 (#920) Signed-off-by: JP-Ellis Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- src/pact/v3/_server.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 43bb6c0b6..69a6f0bb6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -327,7 +327,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@9d890159570d5018df91fedfa40b4730cd4a81b1 # v1.28.4 + uses: crate-ci/typos@c8fd3764afbf5eaf6e53d2e6571c835db2c8fa5f # v1.29.0 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d2369ff7f..54df2f921 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,7 +72,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.28.4 + rev: v1.29.0 hooks: - id: typos diff --git a/src/pact/v3/_server.py b/src/pact/v3/_server.py index 299531109..5b1fb32c0 100644 --- a/src/pact/v3/_server.py +++ b/src/pact/v3/_server.py @@ -308,7 +308,7 @@ def do_GET(self) -> None: # noqa: N802 class StateCallback: """ - Internal server for handlng state callbacks. + Internal server for handling state callbacks. The state handler is a lightweight HTTP server which listens for state change requests from the underlying Pact Core library. It then calls a From 476fbacd3e4e172081379f066f6506708006319e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Jan 2025 10:17:46 +1100 Subject: [PATCH 0677/1376] fix(deps): update ruff to v0.8.5 (#921) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 54df2f921..7730e0cae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: - '@biomejs/biome@1.9.4' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.4 + rev: v0.8.5 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 2881896b8..b8f5b3901 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest ~=8.0", "testcontainers ~=4.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.4"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.5"] ################################################################################ ## Hatch Build Configuration From ec09a77c5a52749f6cb54ca080913bca1c6b7e61 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Jan 2025 10:18:30 +1100 Subject: [PATCH 0678/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.29.3 (#922) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 69a6f0bb6..77f00e12a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -327,7 +327,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@c8fd3764afbf5eaf6e53d2e6571c835db2c8fa5f # v1.29.0 + uses: crate-ci/typos@752bd034d6d0b6fcb6421c73a3789161e0cdf70a # v1.29.3 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7730e0cae..8c0c96524 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,7 +72,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.29.0 + rev: v1.29.3 hooks: - id: typos From bc79562c61d9bcd192d8f424a4b2f8e2b31abf33 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 5 Jan 2025 11:28:19 +1100 Subject: [PATCH 0679/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.29.4 (#923) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 77f00e12a..8a87118a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -327,7 +327,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@752bd034d6d0b6fcb6421c73a3789161e0cdf70a # v1.29.3 + uses: crate-ci/typos@685eb3d55be2f85191e8c84acb9f44d7756f84ab # v1.29.4 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c0c96524..33b3edf30 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,7 +72,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.29.3 + rev: v1.29.4 hooks: - id: typos From 58e7933af777935748d51325ae91940af78a1140 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 5 Jan 2025 11:28:42 +1100 Subject: [PATCH 0680/1376] fix(deps): update ruff to v0.8.6 (#924) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 33b3edf30..a42521349 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: - '@biomejs/biome@1.9.4' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.5 + rev: v0.8.6 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index b8f5b3901..3c14eedb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest ~=8.0", "testcontainers ~=4.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.5"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.6"] ################################################################################ ## Hatch Build Configuration From 5614778e0f4f05a3a24b4d451520b160a01683fc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:29:07 +1100 Subject: [PATCH 0681/1376] chore(deps): update softprops/action-gh-release action to v2.2.1 (#926) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15460c9f5..1b08a3b46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -270,7 +270,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 + uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 with: files: wheels/* body_path: ${{ runner.temp }}/changelog From 6ebbbbf4fb89a73340c8107e7d7c0d7de1db567c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:30:43 +1100 Subject: [PATCH 0682/1376] chore(deps): update docker/setup-qemu-action action to v3.3.0 (#925) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b08a3b46..9ac9b9022 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -184,7 +184,7 @@ jobs: - name: Set up QEMU if: startsWith(matrix.os, 'ubuntu-') - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 with: platforms: arm64 From a1adc2358c95aeb0c5b167d97cd5543708464613 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 04:13:04 +0000 Subject: [PATCH 0683/1376] fix(deps): update ruff to v0.9.0 (#927) Signed-off-by: JP-Ellis Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- src/pact/v3/types.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a42521349..e6eb801c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: - '@biomejs/biome@1.9.4' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.6 + rev: v0.9.0 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 3c14eedb1..e11d57b4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest ~=8.0", "testcontainers ~=4.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.8.6"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.9.0"] ################################################################################ ## Hatch Build Configuration diff --git a/src/pact/v3/types.py b/src/pact/v3/types.py index 7a40062d0..2bb2d224b 100644 --- a/src/pact/v3/types.py +++ b/src/pact/v3/types.py @@ -1,3 +1,4 @@ +# noqa: A005 """ Typing definitions for the matchers. From 00e810bd4a4e2d49f38c1bc111842d69db5923a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:15:14 +1100 Subject: [PATCH 0684/1376] chore(deps): update actions/upload-artifact action to v4.6.0 (#929) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ac9b9022..dff69416e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,7 +70,7 @@ jobs: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: wheels-sdist path: ./dist/*.tar.* @@ -132,7 +132,7 @@ jobs: CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} - name: Upload wheels - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }} path: ./wheelhouse/*.whl @@ -195,7 +195,7 @@ jobs: CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} - name: Upload wheels - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: wheels-${{ matrix.os }}-${{ matrix.archs }}-${{ matrix.build }} path: ./wheelhouse/*.whl From bfd02d7a70c5d8c798983f7c2805fbafb30942e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 13:19:24 +1100 Subject: [PATCH 0685/1376] fix(deps): update ruff to v0.9.1 (#930) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e6eb801c9..bea7f4cd6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: - '@biomejs/biome@1.9.4' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.0 + rev: v0.9.1 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index e11d57b4c..2c982e168 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest ~=8.0", "testcontainers ~=4.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.9.0"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.9.1"] ################################################################################ ## Hatch Build Configuration From 73b63adcdd5cf8793e9b32cb03a0729b91abfc1d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:22:11 +1100 Subject: [PATCH 0686/1376] fix(deps): update ruff to v0.9.2 (#934) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bea7f4cd6..da42e79cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: - '@biomejs/biome@1.9.4' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.1 + rev: v0.9.2 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 2c982e168..75619b780 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ devel-test = [ "pytest ~=8.0", "testcontainers ~=4.0", ] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.9.1"] +devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.9.2"] ################################################################################ ## Hatch Build Configuration From 63c2db402c080886b6347112e87b4c78ea189613 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:22:24 +1100 Subject: [PATCH 0687/1376] chore(deps): update astral-sh/setup-uv action to v5.2.1 (#932) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dff69416e..bdfa26d1d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5.2.1 with: enable-cache: true cache-dependency-glob: | @@ -238,7 +238,7 @@ jobs: merge-multiple: true - name: Set up uv - uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5.2.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8a5ccc423..e780c5323 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5.2.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a87118a0..d50a317d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up uv - uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5.2.1 with: enable-cache: true cache-dependency-glob: | @@ -170,7 +170,7 @@ jobs: patch -p1 -d definition < definition-update.diff - name: Set up uv - uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5.2.1 with: enable-cache: true cache-dependency-glob: | @@ -208,7 +208,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5.2.1 with: enable-cache: true cache-dependency-glob: | @@ -252,7 +252,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5.2.1 with: enable-cache: true cache-dependency-glob: | @@ -277,7 +277,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5.2.1 with: enable-cache: true cache-dependency-glob: | @@ -302,7 +302,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5.2.1 with: enable-cache: true cache-dependency-glob: | @@ -350,7 +350,7 @@ jobs: ${{ runner.os }}-pre-commit- - name: Set up uv - uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0 + uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5.2.1 with: enable-cache: true cache-suffix: pre-commit From 954c108007ff413b95f5895866f73aac17784540 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 06:51:00 +1100 Subject: [PATCH 0688/1376] chore(deps): update codecov/codecov-action action to v5.2.0 (#936) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d50a317d1..f912a48d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -126,7 +126,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2 + uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests @@ -238,7 +238,7 @@ jobs: hatch run example --broker-url=http://pactbroker:pactbroker@localhost:9292 - name: Upload coverage - uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2 + uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0 with: token: ${{ secrets.CODECOV_TOKEN }} flags: examples From 473388e0b35533f603d22b83e61c9ba39f047d64 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 20 Jan 2025 11:55:38 +1100 Subject: [PATCH 0689/1376] fix(v3): defer setting pact broker source When setting that Pact broker source, the verifier name _must_ be set; however, the FFI call to set this is deferred until the `verify()` call. The setting of the Pact broker source (which itself depends on knowing the verifier name) must therefore be also deferred. Signed-off-by: JP-Ellis --- src/pact/v3/verifier.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/pact/v3/verifier.py b/src/pact/v3/verifier.py index 0821418ea..fcf99e083 100644 --- a/src/pact/v3/verifier.py +++ b/src/pact/v3/verifier.py @@ -80,7 +80,7 @@ from contextlib import nullcontext from datetime import date from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, TypedDict, overload +from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, overload from typing_extensions import Self from yarl import URL @@ -183,6 +183,11 @@ def __init__(self, name: str, host: str | None = None) -> None: self._state_handler: StateCallback | nullcontext[None] = nullcontext() self._disable_ssl_verification = False self._request_timeout = 5000 + # Using a broker source requires knowing the provider name, which is + # only provided to the FFI just before verification. As such, we store + # the broker source as a hook to be called just before verification, and + # after the provider name is set. + self._broker_source_hook: Callable[[], None] | None = None def __str__(self) -> str: """ @@ -1193,13 +1198,15 @@ def broker_source( password, token, ) - pact.v3.ffi.verifier_broker_source( + + self._broker_source_hook = lambda: pact.v3.ffi.verifier_broker_source( self._handle, str(url.with_user(None).with_password(None)), username, password, token, ) + return self def verify(self) -> Self: @@ -1233,6 +1240,9 @@ def verify(self) -> Self: transport["scheme"], ) + if self._broker_source_hook: + self._broker_source_hook() + with self._message_producer, self._state_handler: pact.v3.ffi.verifier_execute(self._handle) logger.debug("Verifier executed") @@ -1381,18 +1391,20 @@ def build(self) -> Verifier: Returns: The Verifier instance with the broker source added. """ - pact.v3.ffi.verifier_broker_source_with_selectors( - self._verifier._handle, # noqa: SLF001 - self._url, - self._username, - self._password, - self._token, - self._include_pending, - self._include_wip_since, - self._provider_tags or [], - self._provider_branch, - self._consumer_versions or [], - self._consumer_tags or [], + self._verifier._broker_source_hook = ( # noqa: SLF001 + lambda: pact.v3.ffi.verifier_broker_source_with_selectors( + self._verifier._handle, # noqa: SLF001 + self._url, + self._username, + self._password, + self._token, + self._include_pending, + self._include_wip_since, + self._provider_tags or [], + self._provider_branch, + self._consumer_versions or [], + self._consumer_tags or [], + ) ) self._built = True return self._verifier From f3942f0de0c1d6be0c0297d2f67c4748e77ea029 Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Thu, 23 Jan 2025 00:11:28 +0000 Subject: [PATCH 0690/1376] chore: update changelog v2.3.1 Signed-off-by: JP-Ellis --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43b1a28e0..c56762dca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v2.3.1 (2025-01-23) + +### Fix + +- **v3**: defer setting pact broker source + ## v2.3.0 (2024-12-31) ### BREAKING CHANGE From ded5bc8d4b2f5a0f2ef2a9479a3042f6d2bdf147 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 23 Jan 2025 11:55:45 +1100 Subject: [PATCH 0691/1376] chore: update pre-commit hooks Add a few basic pre-commit hooks from pre-commit's base set of hooks. Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da42e79cb..82d8c3da0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,25 +5,23 @@ default_install_hook_types: - pre-push repos: - # Generic hooks that apply to a lot of files - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-executables-have-shebangs + - id: check-illegal-windows-names + - id: check-merge-conflict - id: check-shebang-scripts-are-executable - id: check-symlinks + - id: check-vcs-permalinks - id: destroyed-symlinks - id: end-of-file-fixer + - id: fix-byte-order-marker - id: mixed-line-ending - id: trailing-whitespace - # The following only check that the files are parseable and does _not_ - # modify the formatting. - - id: check-toml - - id: check-xml - - repo: https://github.com/lyz-code/yamlfix/ rev: 1.17.0 hooks: From acfc73a4abd80b05f9118ee09d3023bbf35814e1 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 23 Jan 2025 12:09:06 +1100 Subject: [PATCH 0692/1376] chore: update committed configuration Signed-off-by: JP-Ellis --- committed.toml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/committed.toml b/committed.toml index 0899f2f51..86b00c78f 100644 --- a/committed.toml +++ b/committed.toml @@ -2,16 +2,24 @@ ## ## See style = "conventional" + +line_length = 80 +merge_commit = false +no_fixup = false +subject_capitalized = false + allowed_types = [ - "fix", - "feat", "chore", "docs", - "style", - "refactor", + "feat", + "fix", "perf", + "refactor", + "revert", + "style", "test", - "release", ] -merge_commit = false -subject_capitalized = false + +# The author string is of the form `Name `. We want to ignore all bots +# which typically have are ofthe form `some-name[bot] `. +ignore_author_re = "(?i)^.*\\[bot\\] <.*>$" From 097888ddf38a5c7ea00c053a41e2abbd1bb0e250 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 23 Jan 2025 11:48:00 +1100 Subject: [PATCH 0693/1376] docs: replace commitizen with git cliff This also completely updates the changelog. Signed-off-by: JP-Ellis --- .github/CHANGELOG.md.j2 | 19 - .github/workflows/build.yml | 53 +- CHANGELOG.md | 2176 +++++++++++++++++++++-------------- cliff.toml | 105 ++ 4 files changed, 1454 insertions(+), 899 deletions(-) delete mode 100644 .github/CHANGELOG.md.j2 create mode 100644 cliff.toml diff --git a/.github/CHANGELOG.md.j2 b/.github/CHANGELOG.md.j2 deleted file mode 100644 index 806a23c24..000000000 --- a/.github/CHANGELOG.md.j2 +++ /dev/null @@ -1,19 +0,0 @@ -{% for entry in tree %} - -## {{ entry.version }}{% if entry.date %} ({{ entry.date }}){% endif %} - -{% for change_key, changes in entry.changes.items() %} - -{% if change_key %} -### {{ change_key }} -{% endif %} - -{% for change in changes %} -{% if change.scope %} -- **{{ change.scope }}**: {{ change.message }} -{% elif change.message %} -- {{ change.message }} -{% endif %} -{% endfor %} -{% endfor %} -{% endfor %} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bdfa26d1d..6137b0970 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -231,49 +231,40 @@ jobs: # Fetch all tags fetch-depth: 0 + - name: Install git cliff and typos + uses: taiki-e/install-action@7e1dca9e0c58340bd342a8aa00ad82587936018d # v2.47.23 + with: + tool: git-cliff,typos + - name: Download wheels and sdist uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: wheels merge-multiple: true - - name: Set up uv - uses: astral-sh/setup-uv@b5f58b2abc5763ade55e4e9d0fe52cd1ff7979ca # v5.2.1 - with: - enable-cache: true - cache-dependency-glob: | - **/pyproject.toml - **/uv.lock - - - name: Install Python - run: uv python install ${{ env.STABLE_PYTHON_VERSION }} - - - name: Install commitizen - run: uv tool install commitizen - - name: Update changelog - id: changelog - run: | - pip install --upgrade commitizen + run: git cliff --verbose + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - cz changelog \ - --incremental \ - --template .github/CHANGELOG.md.j2 \ - --dry-run \ - | tail -n+2 \ - > ${{ runner.temp }}/changelog - echo -e "\n\n## Pull Requests\n\n" >> ${{ runner.temp }}/changelog + - name: Generate release changelog + id: release-changelog + run: | + git cliff \ + --current \ + --strip header \ + --output ${{ runner.temp }}/release-changelog.md - cz changelog \ - --incremental \ - --template .github/CHANGELOG.md.j2 + echo -e "\n\n## Pull Requests\n\n" >> ${{ runner.temp }}/release-changelog.md + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - name: Generate release id: release uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 with: files: wheels/* - body_path: ${{ runner.temp }}/changelog + body_path: ${{ runner.temp }}/release-changelog.md draft: false prerelease: false generate_release_notes: true @@ -288,9 +279,9 @@ jobs: uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6 with: token: ${{ secrets.GH_TOKEN }} - commit-message: 'chore: update changelog ${{ github.ref_name }}' - title: 'chore: update changelog' + commit-message: 'docs: update changelog for ${{ github.ref_name }}' + title: 'docs: update changelog' body: | This PR updates the changelog for ${{ github.ref_name }}. - branch: chore/update-changelog + branch: docs/update-changelog base: main diff --git a/CHANGELOG.md b/CHANGELOG.md index c56762dca..868f13efb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,899 +1,1377 @@ -## v2.3.1 (2025-01-23) +# Changelog + +All notable changes to this project will be documented in this file. + + + + + +## [2.3.1] _2025-01-22_ + +### 🐛 Bug Fixes + +- _(v3)_ Defer setting pact broker source + +### Contributors + +- @JP-Ellis + +## [2.3.0] _2024-12-30_ + +### 🚀 Features + +- _(v3)_ Add message relay and callback servers +- _(v3)_ [**breaking**] Integrate message relay server + > The provider name must be given as an argument of the `Verifier` constructor, instead of the first argument of the `set_info` method. +- _(v3)_ [**breaking**] Add state handler server + > `set_state` has been renamed to `state_handler`. If using a URL still, the `body` keyword argument is now a _required_ parameter. +- _(v3)_ [**breaking**] Further simplify message interface + > `message_handler` signature has been changed and expanded. + +### 🎨 Styling + +- Lint +- Lint + +### 📚 Documentation + +- Fix minor typos +- _(blog)_ Add functional arguments post + +### ⚙️ Miscellaneous Tasks + +- Fix __url__ +- _(ci)_ Pin full version +- Add yamlfix +- Remove docker files and scripts +- Update biome version +- Rename master to main +- _(ci)_ Pin typos to version +- _(ci)_ Pin minor version of checkout action +- Silence unset default fixture loop scope +- _(ci)_ Replace pre-commit/action +- _(v3)_ [**breaking**] Remove unnecessary underscores + > The PactServer `__exit__` arguments no longer have leading underscores. This is typically handled by Python itself and therefore is unlikely to be a change for any user, unless the end user was calling the `__exit__` method explicitly _and_ using keyword arguments. +- _(v3)_ [**breaking**] Make util module private + > `pact.v3.util` has been renamed to `pact.v3._util` and is now private. +- _(ci)_ Upgrade macos-12 to macos-13 +- _(c)_ Specify full action version +- Add pytest-xdist +- _(ci)_ Remove condition on examples +- Update tests to use new message/state fns +- Adapt examples to use function handlers +- Move matchers test out of examples +- Adjust tests based on new implementation +- Remove dead code +- Fix compatibility with 3.9, 3.10 +- Add pytest-rerunfailures +- Fix windows compatibility +- _(ci)_ Automerge renovate PRs + +### Contributors + +- @JP-Ellis + +## [2.2.2] _2024-10-10_ + +### 🚀 Features + +- _(examples)_ Add post and delete +- Add matchable typevar +- Add strftime to java date format converter +- Add match aliases +- Add uuid matcher +- Add each key/value matchers +- Add ArrayContainsMatcher +- [**breaking**] Improve mismatch error + > The `srv.mismatches` is changed from a `list[dict[str, Any]]` to a `list[Mismatch]`. +- [**breaking**] Add Python 3.13, drop 3.8 + > Python 3.8 support dropped + +### 🐛 Bug Fixes + +- Missing typing arguments +- Incompatible override +- Kwargs typing +- Ensure matchers optionally use generators +- _(examples)_ Do not overwrite pact file on every test +- _(examples)_ Use wget for broker healthcheck +- _(examples)_ Correct URL for healthcheck +- _(examples)_ Do not publish postgres port +- Typing annotations +- ISO 8601 incompatibility -### Fix +### 🚜 Refactor + +- Prefer `|` over Optional and Union +- Rename matchers to match +- Split types into stub +- Matcher +- Rename generators to generate +- Generate module in style of match module +- Create pact.v3.types module +- Generators module +- Match module + +### 📚 Documentation + +- _(blog)_ Don't use footnote numbers +- _(blog)_ Add async message blog post +- Update example docs +- Add matcher module preamble +- Add module docstring + +### ⚙️ Miscellaneous Tasks + +- _(ci)_ Use pypi trusted publishing +- Fix typo in previous blog post +- _(ci)_ Update docs on push to master +- Regroup ruff in renovate +- Add extra checks +- Added v3 http interaction examples +- _(ci)_ Add codecov +- Refactor tests +- Prefer ABC over ABCMeta +- Re-organise match module +- Split stdlib and 3rd party types +- Silence a few mypy complaints +- Add pyi to editor config +- Add test for full ISO 8601 date +- Minor improvements to match.matcher +- Align generator with matcher +- Remove MatchableT +- Get test to run again +- Add boolean alias +- Fix compatibility with Python <= 3.9 +- Fix match tests +- Remove unused generalisation +- Use matchers in v3 examples +- Use native Python datetime object +- Adjust tests to use new Mismatch class +- Disable wait +- _(ci)_ Switch to uv fully +- _(ci)_ Disable docs workflow on tags +- _(ci)_ Tweak build conditions +- Disable pypy builds + +### Contributors + +- @JP-Ellis +- @individual-it +- @valkolovos +- @amit828as + +## [2.2.1] _2024-07-22_ + +### 🚀 Features + +- _(ffi)_ Upgrade ffi 0.4.21 +- _(v3)_ Add enum type aliases +- _(v3)_ Improve exception types +- _(v3)_ Remove deprecated messages iterator +- _(v3)_ Implement message verification +- _(v3)_ Add async message provider +- _(ffi)_ Upgrade ffi to 0.4.22 + +### 🐛 Bug Fixes + +- _(ffi)_ Use `with_binary_body` + +### 🚜 Refactor + +- _(v3)_ New interaction iterators +- _(tests)_ Make `_add_body` a method of Body +- _(tests)_ Move InteractionDefinition in own module + +### 📚 Documentation + +- _(CONTRIBUTING.md)_ Update installation steps +- Add additional code capabilities +- Add blog post about rust ffi +- _(ffi)_ Properly document exceptions +- Minor refinements +- _(example)_ Clarify purpose of fs interface + +### ⚙️ Miscellaneous Tasks + +- Group renovate updates +- Use uv to install packages +- _(v3)_ Re-export Pact and Verifier at root +- _(ffi)_ Disable private usage lint +- _(ffi)_ Implement AsynchronousMessage +- _(ffi)_ Implement Generator +- _(ffi)_ Implement MatchingRule +- _(ffi)_ Remove old message and message handle +- _(ffi)_ Implement MessageContents +- _(ffi)_ Implement MessageMetadataPair and Iterator +- _(ffi)_ Implement ProviderState and related +- _(ffi)_ Implement SynchronousHttp +- _(ffi)_ Implement SynchronousMessage +- _(ffi)_ Bump links to 0.4.21 +- _(tests)_ Implement v3/v4 consumer message compatibility suite +- _(examples)_ Add v3 message consumer examples +- Update GitHub templates +- _(examples)_ Add asynchronous message +- _(tests)_ Replace stderr with logger +- _(tests)_ Increase message shown by `truncate` +- Minor typing fix +- _(tests)_ Significant refactor of InteractionDefinition +- _(tests)_ Add v4 message provider compatibility suite +- _(tests)_ Skip windows tests +- _(ci)_ Disable windows arm wheels + +### � Other + +- Fix macos-latest +- Narrow when docs are built and published + +### Contributors + +- @JP-Ellis +- @valkolovos +- @qmg-drettie + +## [2.2.0] _2024-04-11_ + +### 🚀 Features + +- _(v3)_ Add verifier class +- _(v3)_ Add verbose mismatches +- Upgrade FFI to 0.4.19 + +### 🐛 Bug Fixes + +- Delay pytest 8.1 +- _(v3)_ Allow optional publish options +- _(v3)_ Strip embedded user/password from urls + +### 🚜 Refactor + +- _(tests)_ Move parse_headers/matching_rules out of class +- Remove relative imports + +### 📚 Documentation + +- Setup mkdocs +- Update README +- Rework mkdocs-gen-files scripts +- Ignore private python modules +- Overhaul readme +- Update v3 docs +- Fix links to docs/ +- Add social image support +- Add blog post about v2.2 + +### ⚙️ Miscellaneous Tasks + +- _(ci)_ Remove cirrus +- _(ffi)_ Implement verifier handle +- _(v3)_ Add basic verifier tests +- Unskip tests +- Fix missed s/test/devel-test/ +- _(v3)_ Improve body representation +- _(test)_ Improve test logging +- _(tests)_ Update log formatting +- _(test)_ Add state to interaction definition +- _(test)_ Adapt InteractionDefinition for provider +- _(test)_ Add serialize function +- _(test)_ Add provider utilities +- _(tests)_ Add v1 provider compatibility suite +- _(tests)_ Fixes for lower python versions +- _(tests)_ Re-enable warning check +- _(tests)_ Improve logging from provider +- _(test)_ Strip authentication from url +- _(tests)_ Use long-lived pact broker +- _(test)_ Apply a temporary diff to compatibility suite +- _(test)_ Refactor v1 bdd steps +- _(test)_ Fix misspelling in step name +- _(tests)_ Improve logging +- _(tests)_ Allow multiple states with parameters +- _(tests)_ Implement http provider compatibility suite +- _(tests)_ Fix compatibility with py38 +- _(docs)_ Update emoji indices/generators +- _(docs)_ Fix typos +- _(docs)_ Enforce fenced code blocks +- _(docs)_ Minor fixes in examples/ +- Remove redundant __all__ +- _(docs)_ Update examples/readme.md +- _(ci)_ Update environment variables +- _(docs)_ Only publish from master +- _(test)_ Disable failing tests on windows + +### Contributors + +- @JP-Ellis +- @JosephBJoyce + +## [2.1.3] _2024-03-07_ + +### 🐛 Bug Fixes + +- Avoid wheel bloat + +### 📚 Documentation + +- Fix repository link typo +- Fix links to `CONTRIBUTING.md` + +### ⚙️ Miscellaneous Tasks + +- _(ci)_ Fix pypy before-build +- _(ci)_ Pin os to older versions +- _(ci)_ Set osx deployment target +- _(ci)_ Replace hatch clean with rm +- _(ci)_ Update concurrency group +- _(ci)_ Adapt before-build for windows + +### Contributors + +- @JP-Ellis + +## [2.1.2] _2024-03-05_ + +### 🚀 Features + +- _(v3)_ Add v3.ffi module +- _(v3)_ Implement pact class +- _(v3)_ Implement interaction methods +- _(ffi)_ Add OwnedString class +- _(v3)_ Implement Pact Handle methods +- _(v3)_ Add mock server mismatches +- _(v3)_ Implement server log methods +- Add python 3.12 support +- _(v3)_ Add with_matching_rules +- Determine version from vcs +- _(v3)_ Upgrade ffi to 0.4.18 +- _(v3)_ Add specification attribute to pacts +- Add support for musllinux_aarch64 + +### 🐛 Bug Fixes + +- _(ci)_ Add missing environment +- _(test)_ Ignore internal deprecation warnings +- _(v3)_ Unconventional __repr__ implementation +- _(v3)_ Add __next__ implementation +- _(example)_ Unknown action +- _(example)_ Publish_verification_results typo +- _(example)_ Publish message pact +- _(v3)_ Rename `with_binary_file` +- _(v3)_ Incorrect arg order +- Clean pact interactions on exception + +### 🚜 Refactor + +- _(v3)_ Split interactions into modules + +### 🎨 Styling + +- Fix pre-commit lints +- [**breaking**] Refactor constants + > The public functions within the constants module have been removed. If you previously used them, please make use of the constants. For example, instead of `pact.constants.broker_client_exe()` use `pact.constants.BROKER_CLIENT_PATH` instead. + +### 📚 Documentation + +- _(v3)_ Update ffi documentation +- _(readme)_ Fix links to examples +- Add git submodule init +- Fix typos + +### ⚙️ Miscellaneous Tasks + +- Add future deprecation warnings +- _(ci)_ Disable on draft pull requests +- _(ci)_ Separate concurrency groups for builds +- Fix hatch test scripts +- _(test)_ Add pytest options in root +- _(build)_ Update packaging to build ffi +- _(tests)_ Add ruff.toml for tests directory +- _(ci)_ Update build targets +- _(v3)_ Create ffi.py +- _(tests)_ Remove empty file +- _(v3)_ Add str and repr to enums +- _(test)_ Move pytest cli args definition +- Add label sync +- _(test)_ Automatically generated xml coverage +- Enable lints fully +- _(pre-commit)_ Add mypy +- _(ffi)_ Add typing +- _(labels)_ Fix incorrect label alias +- Exclude python 3.12 +- Fix wheel builds +- _(ci)_ Revise pypi publishing +- _(tests)_ Reduce log verbosity +- Fix ruff lints +- _(tests)_ Add compatibility suite as submodule +- _(ruff)_ Disable TD002 +- Allow None content type +- _(tests)_ Implement consumer v1 feature +- _(ci)_ Checkout submodules +- _(ci)_ Fix examples testing +- _(ci)_ Clone submodules in Cirrus +- _(tests)_ Automatic submodule init +- Fix lints +- Update submodule +- _(ci)_ Set hatch to be verbose +- _(ci)_ Add test conclusion step +- _(ci)_ Breaking changes with for artifacts +- _(ci)_ Re-enable pypy builds on Windows +- _(dev)_ Replace black with ruff +- _(dev)_ Add markdownlint pre-commit +- _(ci)_ Fix pypy linux builds +- _(test/v3)_ Move bdd steps into shared module +- _(test/v3)_ Add v2 consumer compatibility suite +- _(tests)_ Add v3 consumer compatibility suite +- Update metadata +- _(tests)_ Move the_pact_file_for_the_test_is_generated to share util +- _(tests)_ Add v4 http consumer compatibility suite +- _(ci)_ Speed up wheels building on prs +- _(ci)_ Add caching +- Migrate from flat to src layout +- _(docs)_ Update changelog +- _(ci)_ Automate release process +- _(v3)_ Add warning on pact.v3 import +- _(ci)_ Remove check of wheels +- _(ci)_ Speed up build pipeline +- _(ci)_ Another build pipeline fix +- _(ci)_ Typo + +### � Other + +- Add g++ to cirrus linux image + +### Contributors + +- @JP-Ellis +- @YOU54F +- @dryobates +- @filipsnastins +- @neringaalt + +## [2.1.0] _2023-10-03_ + +### 🚀 Features + +- _(example)_ Simplify docker-compose +- Bump pact standalone to 2.0.7 + +### 🐛 Bug Fixes + +- _(github)_ Fix typo in template +- _(ci)_ Pypi publish + +### 🎨 Styling + +- Add pre-commit hooks and editorconfig + +### 📚 Documentation + +- Rewrite contributing.md +- Add issue and pr templates +- Incorporate suggestions from @YOU54F + +### ⚙️ Miscellaneous Tasks + +- Add pact-foundation triage automation +- Update pre-commit config +- [**breaking**] Migrate to pyproject.toml and hatch + > Drop support for Python 3.6 and 3.7 +- _(ci)_ Migrate cicd to hatch +- _(example)_ Migrate consumer example +- _(example)_ Migrate fastapi provider example +- _(example)_ Migrate flask provider example +- _(example)_ Update readme +- _(example)_ Migrate message pact example +- _(ci)_ Split tests examples and lints +- _(example)_ Avoid changing python path +- Address pr comments +- _(gitignore)_ Update from upstream templates +- V2.1.0 + +### Contributors + +- @JP-Ellis +- @mefellows -- **v3**: defer setting pact broker source +## [2.0.1] _2023-07-26_ -## v2.3.0 (2024-12-31) +### 🚀 Features -### BREAKING CHANGE +- Update standalone to 2.0.3 -- `message_handler` signature has been changed and expanded. -- `set_state` has been renamed to `state_handler`. If using a URL still, the `body` keyword argument is now a _required_ parameter. -- The provider name must be given as an argument of the `Verifier` constructor, instead of the first argument of the `set_info` method. -- The `set_info` verifier method is removed, with `add_transport` needing to be used. -- `pact.v3.util` has been renamed to `pact.v3._util` and is now private. -- The PactServer `__exit__` arguments no longer have leading underscores. This is typically handled by Python itself and therefore is unlikely to be a change for any user, unless the end user was calling the `__exit__` method explicitly _and_ using keyword arguments. +### ⚙️ Miscellaneous Tasks -### Feat +- Update MANIFEST file to note 2.0.2 standalone +- _(examples)_ Update docker setup for non linux os +- Releasing version 2.0.1 -- **v3**: further simplify message interface -- **v3**: add state handler server -- **v3**: integrate message relay server -- **v3**: add message relay and callback servers +### Contributors -## v2.2.2 (2024-10-10) +- @YOU54F -### BREAKING CHANGE +## [2.0.0] _2023-07-10_ -- Python 3.8 support dropped -- The `srv.mismatches` is changed from a `list[dict[str, -Any]]` to a `list[Mismatch]`. +### 🚀 Features -### Feat +- Describe classifiers and python version for pypi package +- _(test)_ Add docker images for Python 3.9-3.11 for testing purposes +- Add matchers for ISO 8601 date format +- Support arm64 osx/linux +- Support x86 and x86_64 windows +- Use pact-ruby-standalone 2.0.0 release -- add Python 3.13, drop 3.8 -- improve mismatch error -- add ArrayContainsMatcher -- add each key/value matchers -- add uuid matcher -- add match aliases -- improve match module -- add strftime to java date format converter -- add matchable typevar -- **examples**: add post and delete +### 🐛 Bug Fixes -### Fix +- Actualize doc on how to make contributions +- Remove dead code +- Fix cors parameter not doing anything -- ISO 8601 incompatibility -- typing annotations -- **examples**: do not publish postgres port -- **examples**: use wget for broker healthcheck -- **examples**: do not overwrite pact file on every test -- ensure matchers optionally use generators -- kwargs typing -- incompatible override -- missing typing arguments +### 🎨 Styling + +- Add missing newline/linefeed + +### 📚 Documentation + +- Add Python 3.11 to CONTRIBUTING.md +- Fix link for GitHub badge +- Fix instruction to build python 3.11 image +- Paraphrase the instructions for running the tests +- Rephrase the instructions for running the tests +- Reformat releasing documentation + +### 🧪 Testing + +- V2.0.1 (pact-2.0.1) - pact-ruby-standalone + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.7.0 +- Do not add merge commits to the change log +- _(docs)_ Update provider verifier options table +- _(docs)_ Correct table +- _(docs)_ Improve table alignment and abs links +- Update to 2.0.2 pact-ruby-standalone +- Releasing version 2.0.0 + +### � Other + +- Correct links in contributing manual +- Improve commit messages guide +- Add python 3.11 to test matrix +- Use compatible dependency versions for Python 3.6 +- Use a single Dockerfile, providing args for the Python version instead of multiple files +- Test arm64 on cirrus-ci / test win/osx on gh +- Skip 3.6 python arm64 failing in cirrus, passing locally with cirrus run +- _(deps)_ Bump flask from 2.2.2 to 2.2.5 in /examples/message +- _(deps)_ Bump flask from 2.2.2 to 2.2.5 in /examples/flask_provider +- _(deps-dev)_ Bump flask from 2.2.2 to 2.2.5 + +### Contributors + +- @YOU54F +- @sergeyklay +- @Lukas-dev-threads +- @elliottmurray +- @mikegeeves + +## [1.7.0] _2023-02-19_ + +### 🚀 Features + +- Enhance provider states for pact-message (#322) + +### 🐛 Bug Fixes + +- Requirements_dev.txt to reduce vulnerabilities (#317) +- Setup security issue (#318) + +### ⚙️ Miscellaneous Tasks + +- Add workflow to create a jira issue for pactflow team when smartbear-supported label added to github issue +- /s/Pactflow/PactFlow +- Releasing version 1.7.0 + +### Contributors + +- @elliottmurray +- @YOU54F +- @nsfrias +- @bethesque +- @mefellows + +## [1.6.0] _2022-09-11_ + +### 🚀 Features + +- Support publish pact with branch (#300) +- Support verify with branch (#302) + +### 📚 Documentation + +- Update docs to reflect usage for native Python + +### ⚙️ Miscellaneous Tasks + +- _(test)_ Fix consumer message test (#301) +- Releasing version 1.6.0 + +### � Other + +- Correct download logic when installing. Add a helper target to setup a pyenv via make (#297) + +### Contributors + +- @elliottmurray +- @YOU54F +- @B3nnyL +- @mikegeeves +- @jnfang + +## [1.5.2] _2022-03-21_ + +### ⚙️ Miscellaneous Tasks + +- Update PACT_STANDALONE_VERSION to 1.88.83 +- Releasing version 1.5.2 + +### Contributors + +- @elliottmurray +- @YOU54F + +## [1.5.1] _2022-03-10_ + +### 🚀 Features + +- Message_pact -> with_metadata() updated to accept term (#289) + +### 📚 Documentation + +- _(examples-consumer)_ Add pip install requirements to the consumer… (#291) + +### 🧪 Testing + +- _(examples)_ Move shared fixtures to a common folder so they can b… (#280) + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.5.1 + +### Contributors + +- @elliottmurray +- @sunsathish88 +- @mikegeeves + +## [1.5.0] _2022-02-05_ + +### 🚀 Features + +- No include pending (#284) + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.5.0 + +### � Other + +- Python36-support-removed (#283) + +### Contributors + +- @elliottmurray +- @abgora +- @mikegeeves + +## [1.4.6] _2022-01-03_ + +### 🚀 Features + +- _(matcher)_ Allow bytes type in from_term function (#281) + +### 🐛 Bug Fixes + +- _(consumer)_ Ensure a description is provided for all interactions + +### 📚 Documentation + +- Docs/examples (#273) + +### 🧪 Testing + +- _(examples-fastapi)_ Tidy FastAPI example, making consistent with Flask (#274) + +### ⚙️ Miscellaneous Tasks + +- Flake8 config to ignore direnv +- Releasing version 1.4.6 + +### Contributors + +- @elliottmurray +- @joshua-badger +- @mikegeeves + +## [1.4.5] _2021-10-11_ + +### 🐛 Bug Fixes + +- Update standalone to 1.88.77 to fix Let's Encrypt CA issue + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.4.5 + +### Contributors + +- @mefellows + +## [1.4.4] _2021-10-02_ + +### 🐛 Bug Fixes + +- _(ruby)_ Update ruby standalone to support disabling SSL verification via an environment variable + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.4.4 + +### Contributors + +- @mefellows +- @m-aciek + +## [1.4.3] _2021-09-05_ + +### 🚀 Features + +- Added support for message provider using pact broker (#257) + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.4.3 + +### Contributors + +- @elliottmurray +- @pulphix + +## [1.4.2] _2021-08-22_ + +### ⚙️ Miscellaneous Tasks + +- Bundle Ruby standalones into dist artifact. (#256) +- Releasing version 1.4.2 + +### Contributors + +- @elliottmurray +- @taj-p + +## [1.4.1] _2021-08-17_ + +### 🐛 Bug Fixes + +- Make uvicorn versions over 0.14 + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.4.1 + +### Contributors + +- @elliottmurray + +## [1.4.0] _2021-08-07_ + +### 🚀 Features + +- Added support for message provider (#251) + +### 🐛 Bug Fixes + +- Issue originating from snyk with requests and urllib + +### ⚙️ Miscellaneous Tasks + +- _(snyk)_ Update fastapi +- Releasing version 1.4.0 + +### Contributors + +- @elliottmurray +- @pulphix + +## [1.3.9] _2021-05-13_ + +### 🐛 Bug Fixes + +- Change default from empty string to empty list (#235) + +### ⚙️ Miscellaneous Tasks + +- _(ruby)_ Update ruby standalen +- Releasing version 1.3.9 + +### Contributors + +- @elliottmurray +- @tephe + +## [1.3.8] _2021-05-01_ + +### 🐛 Bug Fixes + +- Fix datetime serialization issues in Format + +### 📚 Documentation + +- Example uses date matcher + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.3.8 + +### Contributors + +- @elliottmurray +- @DawoudSheraz + +## [1.3.7] _2021-04-24_ + +### 🐛 Bug Fixes + +- _(broker)_ Token added to verify steps + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.3.7 + +### Contributors + +- @elliottmurray + +## [1.3.6] _2021-04-20_ + +### 🐛 Bug Fixes + +- Docker/py36.Dockerfile to reduce vulnerabilities +- Docker/py38.Dockerfile to reduce vulnerabilities +- Docker/py37.Dockerfile to reduce vulnerabilities +- Publish verification results was wrong (#222) + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.3.6 + +### � Other + +- Revert docker36 back + +### Contributors + +- @elliottmurray +- @snyk-bot + +## [1.3.5] _2021-03-28_ + +### 🐛 Bug Fixes + +- _(publish)_ Fixing the fix. Pact Python api uses only publish_version and ensures it follows that + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.3.5 + +### Contributors + +- @elliottmurray + +## [1.3.4] _2021-03-27_ + +### 🐛 Bug Fixes + +- Verifier should now publish + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.3.4 + +### Contributors + +- @elliottmurray + +## [1.3.3] _2021-03-25_ + +### 🐛 Bug Fixes + +- Pass pact_dir to publish() + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.3.3 + +### Contributors + +- @elliottmurray +- @ + +## [1.3.2] _2021-03-21_ + +### 🐛 Bug Fixes + +- Ensure path is passed to broker and allow running from root rather than test file +- Remove pacts from examples + +### ⚙️ Miscellaneous Tasks + +- Move from nose to pytests as we are now 3.6+ +- Update ci stuff +- More clean up +- Wip on using test containers on examples +- Spiking testcontainers +- Added some docs about how to use the e2e example +- Releasing version 1.3.2 + +### Contributors + +- @elliottmurray + +## [1.3.1] _2021-02-27_ + +### 🐛 Bug Fixes + +- Introduced and renamed specification version + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.3.1 + +### Contributors + +- @elliottmurray + +## [1.3.0] _2021-01-26_ + +### 🚀 Features + +- Initial interface +- Add MessageConsumer +- Single message flow +- Create basic tests for single pact message +- Update MessageConsumer and tests +- Add constants test +- Add pact-message integration +- Add pact-message integration test +- Add more test +- Update message pact tests +- Change dummy handler to a message handler +- Update handler to handle error exceptions +- Move publish function to broker class +- Update message handler to be independent of pact +- Address PR comments + +### 🐛 Bug Fixes + +- Send to cli pact_files with the pact_dir in their path +- Add e2e example test into ci back in +- Remove publish fn for now +- Linting +- Add missing conftest +- Try different way to mock +- Flake8 warning +- Revert changes to quotes +- Improve test coverage +- Few more tests to improve coverage + +### 📚 Documentation + +- Add readme for message consumer +- Update readme + +### 🧪 Testing + +- Create external dummy handler in test +- Update message handler condition based on content +- Remove mock and check generated json file +- Consider publish to broker with no pact_dir argument + +### ⚙️ Miscellaneous Tasks + +- Remove python35 and 34 and add 39 +- Fix bad merge +- Add missing files in src +- Add generate_pact_test +- Remove log_dir, refactor test +- Flake8 revert +- Remove test param for provider +- Flake8, clean up deadcode +- Pydocstyle +- Add missing import +- Releasing version 1.3.0 + +### � Other + +- Pr not triggering workflow + +### Contributors + +- @elliottmurray +- @williaminfante +- @tuan-pham +- @cdambo + +## [1.2.11] _2020-12-29_ + +### 🐛 Bug Fixes + +- Not creating wheel + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.2.11 + +### Contributors + +- @elliottmurray + +## [1.2.10] _2020-12-19_ + +### 📚 Documentation + +- Fix small typo in `with_request` doc string +- _(example)_ Created example and have relative imports kinda working. Provider not working as it cant find one of our urls +- Typo in pact-verifier help string: PUT -> POST for --provider-states-setup-url +- Added badge to README + +### ⚙️ Miscellaneous Tasks + +- _(upgrade)_ Upgrade python version to 3.8 +- Wqshell script to run flask in examples +- Added run test to travis +- Releasing version 1.2.10 + +### � Other + +- _(github actions)_ Added Github Actions configuration for build and test +- Removed Travis CI configuration +- Add publishing actions + +### Contributors + +- @elliottmurray +- @matthewbalvanz-wf +- @noelslice +- @hstoebel + +## [1.2.9] _2020-10-19_ + +### 🚀 Features + +- _(verifier)_ Allow setting consumer_version_selectors on Verifier + +### 🐛 Bug Fixes + +- Fix flaky tests using OrderedDict + +### 🎨 Styling + +- Fix linting issues +- Fix one more linting issue + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.2.9 + +### Contributors + +- @elliottmurray +- @thatguysimon + +## [1.2.8] _2020-10-18_ + +### 🚀 Features + +- _(verifier)_ Support include-wip-pacts-since in CLI + +### 🐛 Bug Fixes + +- Fix command building bug + +### 🚜 Refactor + +- Extract input validation in call_verify out into a dedicated method + +### 🎨 Styling + +- Fix linting + +### 📚 Documentation + +- _(examples)_ Tweak to readme +- _(examples)_ Changed provider example to use atexit + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.2.8 + +### Contributors + +- @elliottmurray +- @thatguysimon + +## [1.2.7] _2020-10-09_ + +### 🐛 Bug Fixes + +- _(verifier)_ Headers not propagated properly + +### 📚 Documentation + +- _(examples)_ Removed manual publish to broker + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.2.7 + +### Contributors + +- @elliottmurray + +## [1.2.6] _2020-09-11_ + +### 🚀 Features + +- _(verifier)_ Allow to use unauthenticated brokers + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.2.6 + +### Contributors + +- @elliottmurray +- @copalco + +## [1.2.5] _2020-09-10_ + +### 🚀 Features + +- _(verifier)_ Add enable_pending argument handling in verify wrapper +- _(verifier)_ Pass enable_pending flag in Verifier's methods +- _(verifier)_ Support --enable-pending flag in CLI + +### 🐛 Bug Fixes + +- _(verifier)_ Remove superfluous option from verify CLI command +- _(verifier)_ Remove superfluous verbose mentions + +### 🚜 Refactor + +- _(verifier)_ Add enable_pending to signature of verify methods + +### 🧪 Testing + +- Bump mock to 3.0.5 + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.2.5 + +### � Other + +- _(pre-commit)_ Add commitizen to pre-commit configuration + +### Contributors + +- @elliottmurray +- @ +- @m-aciek + +## [1.2.4] _2020-08-27_ + +### 🚀 Features + +- _(cli)_ Add consumer-version-selector option + +### 📚 Documentation + +- Update README.md with relevant option documentation +- _(cli)_ Improve cli help grammar + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.2.4 + +### Contributors + +- @elliottmurray +- @alecgerona + +## [1.2.3] _2020-08-26_ + +### 🚀 Features + +- Update standalone to 1.88.3 + +### ⚙️ Miscellaneous Tasks + +- Script now uses gh over hub +- Release script updates version automatically now +- Fix release script +- Releasing version 1.2.3 + +### Contributors + +- @elliottmurray + +## [1.2.2] _2020-08-24_ + +### 🚀 Features + +- Added env vars for broker verify + +### 📚 Documentation + +- Https svg + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.2.2 + +### Contributors + +- @elliottmurray + +## [1.2.1] _2020-08-08_ + +### 🐛 Bug Fixes + +- Custom headers had a typo + +### 📚 Documentation + +- Example code verifier +- Merged 2 examples + +### ⚙️ Miscellaneous Tasks + +- Releasing version 1.2.1 + +### Contributors + +- @elliottmurray + +## [1.2.0] _2020-07-24_ + +### 🚀 Features + +- Create beta verifier class and api +- Fixing up tests and examples and code for provider class + +### 🐛 Bug Fixes + +- Change to head from master + +### 📚 Documentation + +- Update links for rendering page correctly in docs.pact.io +- Update stackoverflow link +- Contributing md updated for commit messages + +### ⚙️ Miscellaneous Tasks + +- Add workflow to trigger pact docs update when markdown files change +- Added semantic yml for git messages +- Releasing version 1.2.0 +- Releasing with fix version v1.2.0 -### Refactor +### � Other -- match module -- generators module -- create pact.v3.types module -- generate module in style of match module -- rename generators to generate -- matcher -- split types into stub -- rename matchers to match -- prefer `|` over Optional and Union +- Add check for commit messages +- Tweak to regex +- Temporary fix for testing purposes of messages: +- Remove commit message as it is breaking releases -## v2.2.1 (2024-07-22) +### Contributors -### Feat +- @elliottmurray +- @bethesque -- **ffi**: upgrade ffi to 0.4.22 -- **v3**: add async message provider -- **v3**: implement message verification -- **v3**: remove deprecated messages iterator -- **v3**: improve exception types -- **v3**: add enum type aliases -- **ffi**: upgrade ffi 0.4.21 +## [1.1.0] _2020-06-25_ -### Fix +### 🚀 Features -- **ffi**: use `with_binary_body` +- Update standalone to 1.86.0 -### Refactor +### ⚙️ Miscellaneous Tasks -- **tests**: move InteractionDefinition in own module -- **tests**: make `_add_body` a method of Body -- **v3**: new interaction iterators +- Removed some files and moved a few things around -## v2.2.0 (2024-04-11) +### Contributors -### Feat +- @elliottmurray +- @bethesque +- @hstoebel -- upgrade FFI to 0.4.19 -- **v3**: add verbose mismatches -- **v3**: add verifier class +## [0.22.0] _2020-05-11_ -### Fix +### 🚀 Features -- **v3**: strip embedded user/password from urls -- **v3**: allow optional publish options -- delay pytest 8.1 +- Update standalone to 1.84.0 -### Refactor +### 📚 Documentation -- remove relative imports -- **tests**: move parse_headers/matching_rules out of class +- Update RELEASING.md -## v2.1.3 (2024-03-07) +### ⚙️ Miscellaneous Tasks -### Fix +- Add script to create a PR to update the pact-ruby-standalone version -- avoid wheel bloat - -## v2.1.2 (2024-03-05) - -### BREAKING CHANGE - -- The public functions within the constants module have been removed. If you previously used them, please make use of the constants. For example, instead of `pact.constants.broker_client_exe()` use `pact.constants.BROKER_CLIENT_PATH` instead. -- It is possible to use the system installed Pact executables by setting `PACT_USE_SYSTEM_BINS` to `True` or `Yes` (case insensitive). - -### Feat - -- add support for musllinux_aarch64 -- **v3**: add specification attribute to pacts -- **v3**: upgrade ffi to 0.4.18 -- determine version from vcs -- **v3**: add with_matching_rules -- add python 3.12 support -- **v3**: implement server log methods -- **v3**: add mock server mismatches -- **v3**: implement Pact Handle methods -- **ffi**: add OwnedString class -- **v3**: implement interaction methods -- **v3**: implement pact class -- **v3**: add v3.ffi module - -### Fix - -- clean pact interactions on exception -- **v3**: incorrect arg order -- **v3**: rename `with_binary_file` -- **example**: publish message pact -- **example**: publish_verification_results typo -- **example**: unknown action -- **v3**: add `__next__` implementation -- **deps**: add yarl dependency -- **v3**: unconventional `__repr__` implementation -- **build**: include omitted `lib` dir -- **test**: ignore internal deprecation warnings -- **ci**: add missing environment +### Contributors -### Refactor +- @pyasi +- @elliottmurray +- @bethesque +- @ -- **v3**: split interactions into modules -- refactor constants +## [0.20.0] _2020-01-16_ -## v2.1.1 (2023-10-04) +### 🚀 Features -Identical to 2.1.0, but with a fix to the publication process to PyPI. +- Support using environment variables to set pact broker configuration +- Update to pact-ruby-standalone-1.79.0 -## v2.1.0 (2023-10-04) +### Contributors -### BREAKING CHANGE +- @bethesque +- @matthewbalvanz-wf +- @elliottmurray +- @mikahjc +- @mefellows +- @dlmiddlecote +- @ejrb -- Drop support for Python 3.6 and 3.7 +## [0.18.0] _2018-08-21_ -### Feat +### ⚙️ Miscellaneous Tasks -- bump pact standalone to 2.0.7 -- **example**: simplify docker-compose +- _(docs)_ Update contact information -### Fix +### Contributors -- **ci**: pypi publish -- **github**: fix typo in template -- migrate to pyproject.toml and hatch +- @matthewbalvanz-wf +- @mefellows -## 2.0.1 +## [0.13.0] _2018-01-20_ -- d3397b7 - chore(examples): update docker setup for non linux os (Yousaf Nabi, Tue Jul 25 14:55:42 2023 +0100) -- ef12e56 - feat: update standalone to 2.0.3 (Yousaf Nabi, Tue Jul 25 14:00:38 2023 +0100) -- 1429d2f - chore: update MANIFEST file to note 2.0.2 standalone (Yousaf Nabi, Tue Jul 25 13:56:08 2023 +0100) +### 📚 Documentation -## 2.0.0 +- Remove reference to v3 pact in provider-states-setup-url -- 2a244ea - chore: update to 2.0.2 pact-ruby-standalone (Yousaf Nabi, Sat Jul 8 14:12:18 2023 +0100) -- 819f0a7 - test: v2.0.1 (pact-2.0.1) - pact-ruby-standalone (Yousaf Nabi, Thu May 18 23:30:30 2023 +0100) -- 9bc3e21 - chore(docs): improve table alignment and abs links (Yousaf Nabi, Thu May 4 12:10:39 2023 +0100) -- 80f06cf - chore(docs): correct table (Yousaf Nabi, Wed May 3 19:20:37 2023 +0100) -- c70573c - chore(docs): update provider verifier options table (Yousaf Nabi, Wed May 3 19:17:28 2023 +0100) -- fc6ced8 - style: add missing newline/linefeed (Serghei Iakovlev, Thu May 4 09:51:19 2023 +0200) -- 7b14aa3 - build(deps-dev): bump flask from 2.2.2 to 2.2.5 (dependabot[bot], Wed May 3 20:30:31 2023 +0000) -- a0efd69 - build(deps): bump flask from 2.2.2 to 2.2.5 in /examples/flask_provider (dependabot[bot], Wed May 3 20:30:24 2023 +0000) -- 1267d7d - build(deps): bump flask from 2.2.2 to 2.2.5 in /examples/message (dependabot[bot], Wed May 3 19:51:09 2023 +0000) -- 4e3ca38 - feat: use pact-ruby-standalone 2.0.0 release (Yousaf Nabi, Sat Apr 29 00:43:31 2023 +0100) -- 2c673ea - ci: skip 3.6 python arm64 failing in cirrus, passing locally with cirrus run (Yousaf Nabi, Fri Apr 21 15:46:22 2023 +0100) -- 7aff538 - feat: support x86 and x86_64 windows (Yousaf Nabi, Fri Apr 21 15:44:48 2023 +0100) -- 28440da - ci: test arm64 on cirrus-ci / test win/osx on gh (Yousaf Nabi, Fri Apr 21 15:37:30 2023 +0100) -- 93db8ae - feat: support arm64 osx/linux (Yousaf Nabi, Fri Apr 21 12:35:23 2023 +0100) -- 19be499 - fix: fix cors parameter not doing anything (Lukas Riedersberger, Fri Apr 14 12:22:21 2023 +0200) -- e721d81 - docs: reformat releasing documentation (Serghei Iakovlev, Wed Apr 5 12:39:35 2023 +0200) -- 71f1529 - chore: do not add merge commits to the change log (Serghei Iakovlev, Wed Apr 5 12:27:49 2023 +0200) -- 9ce2d69 - chore: Releasing version 1.7.0 (Elliott Murray, Sun Feb 19 11:28:01 2023 +0000) -- 429e171 - build: use a single Dockerfile, providing args for the Python version instead of multiple files (Mike Geeves, Mon Apr 3 09:01:37 2023 +0100) -- e99e7fb - docs: rephrase the instructions for running the tests (Serghei Iakovlev, Sun Apr 2 22:20:07 2023 +0200) -- a5d3a2e - docs: paraphrase the instructions for running the tests (Serghei Iakovlev, Sun Apr 2 22:19:37 2023 +0200) -- 24c2dbf - docs: fix instruction to build python 3.11 image (Serghei Iakovlev, Sun Apr 2 22:18:10 2023 +0200) -- 55dcaf2 - feat(test): add docker images for Python 3.9-3.11 for testing purposes (Serghei Iakovlev, Fri Mar 17 11:24:42 2023 +0100) -- 28fc7d3 - docs: fix link for GitHub badge (Serghei Iakovlev, Fri Mar 31 22:50:23 2023 +0200) -- 26eaaac - fix: remove dead code (Serghei Iakovlev, Sun Mar 5 02:05:14 2023 +0100) -- f7c5006 - docs: add Python 3.11 to CONTRIBUTING.md (Serghei Iakovlev, Thu Mar 30 23:22:22 2023 +0200) -- 348bf5e - build: use compatible dependency versions for Python 3.6 (Serghei Iakovlev, Thu Mar 30 23:18:57 2023 +0200) -- 4d9f4cd - feat: describe classifiers and python version for pypi package (Serghei Iakovlev, Sun Mar 5 09:16:29 2023 +0100) -- 7603815 - ci: add python 3.11 to test matrix (Serghei Iakovlev, Sun Mar 5 09:15:23 2023 +0100) -- bea1563 - doc: improve commit messages guide (Serghei Iakovlev, Sat Mar 4 00:30:56 2023 +0100) -- 60f2aac - doc: correct links in contributing manual (Serghei Iakovlev, Fri Mar 3 21:38:58 2023 +0100) -- a219f49 - fix: actualize doc on how to make contributions (Serghei Iakovlev, Thu Mar 2 08:56:48 2023 +0100) -- 4919772 - feat: add matchers for ISO 8601 date format (Serghei Iakovlev, Sun Mar 12 16:03:44 2023 +0100) +### Contributors -## 1.7.0 +- @matthewbalvanz-wf +- @bethesque +- @ -- 44cda33 - chore: /s/Pactflow/PactFlow (Yousaf Nabi, Thu Jan 26 16:11:54 2023 +0000) -- 1bbdd37 - feat: Enhance provider states for pact-message (#322) (nsfrias, Tue Jan 24 17:04:29 2023 +0000) -- 53ca129 - chore: add workflow to create a jira issue for pactflow team when smartbear-supported label added to github issue (Beth Skurrie, Wed Jan 18 10:51:05 2023 +1100) -- d87d54b - fix: setup security issue (#318) (Elliott Murray, Mon Nov 21 09:39:41 2022 +0000) -- 55f2a64 - fix: requirements_dev.txt to reduce vulnerabilities (#317) (Matt Fellows, Sun Nov 6 02:12:30 2022 +1100) - -## 1.6.0 - -- ceff89b - Publish verify branches (#306) (Yousaf Nabi, Sun Sep 11 11:33:44 2022 +0100) -- 89733d6 - feat: Support verify with branch (#302) (B3nnyL, Sun Sep 11 20:14:13 2022 +1000) -- 42e0db8 - feat: Support publish pact with branch (#300) (B3nnyL, Sun Sep 11 20:06:27 2022 +1000) -- 80d7b13 - chore(test): fix consumer message test (#301) (B3nnyL, Tue Aug 23 23:50:27 2022 +1000) -- 2015f72 - build: Correct download logic when installing. Add a helper target to setup a pyenv via make (#297) (mikegeeves, Sun Jun 19 09:27:07 2022 +0100) -- c17ac70 - docs: Update docs to reflect usage for native Python (#227) (Jiayun Fang, Wed Apr 27 10:00:50 2022 -0700) - -## 1.5.2 - -- 25823ae - chore: update PACT_STANDALONE_VERSION to 1.88.83 (#292) (Yousaf Nabi, Mon Mar 21 22:14:40 2022 +0000) - -## 1.5.1 - -- e645b24 - feat: message_pact -> with_metadata() updated to accept term (#289) (sunsathish88, Tue Mar 8 12:08:34 2022 -0500) -- b981865 - docs(examples-consumer): add pip install requirements to the consumer… (#291) (mikegeeves, Sun Mar 6 10:12:32 2022 +0000) -- 4c76ae8 - test(examples): move shared fixtures to a common folder so they can b… (#280) (mikegeeves, Sun Mar 6 10:10:11 2022 +0000) - -## 1.5.0 - -- 8085be0 - feat: No include pending (#284) (Abraham Gonzalez, Wed Feb 2 13:20:39 2022 +0100) -- f169f3b - ci: python36-support-removed (#283) (mikegeeves, Sat Jan 22 10:26:44 2022 +0000) - -## 1.4.6 - -- 6c25844 - chore: flake8 config to ignore direnv (Elliott Murray, Mon Jan 3 18:33:47 2022 +0000) -- 891134a - feat(matcher): Allow bytes type in from_term function (#281) (joshua-badger, Mon Jan 3 11:23:40 2022 -0700) -- 588b55d - fix(consumer): ensure a description is provided for all interactions (#278) (mikegeeves, Thu Dec 30 16:57:03 2021 +0000) -- 02643d4 - test(examples-fastapi): tidy FastAPI example, making consistent with Flask (#274) (mikegeeves, Sun Oct 31 21:52:54 2021 +0000) -- bf110e2 - docs: Docs/examples (#273) (Elliott Murray, Tue Oct 26 21:54:00 2021 +0100) - -## 1.4.5 - -- 695d51f - fix: update standalone to 1.88.77 to fix Let's Encrypt CA issue (Matt Fellows, Mon Oct 11 13:29:34 2021 +1100) - -## 1.4.4 - -- b90cf3d - fix(ruby): update ruby standalone to support disabling SSL verification via an environment variable (m-aciek, Sat Oct 2 03:04:14 2021 +0200) - -## 1.4.3 - -- 08f0dc0 - feat: added support for message provider using pact broker (#257) (Fabio Pulvirenti, Sun Sep 5 22:49:51 2021 +0200) - -## 1.4.2 - -- f2230b6 - chore: Bundle Ruby standalones into dist artifact. (#256) (Taj Pereira, Sun Aug 22 19:53:53 2021 +0930) -- e370786 - chore: Releasing version 1.4.1 (Elliott Murray, Tue Aug 17 18:55:53 2021 +0100) -- 7dc8864 - fix: make uvicorn versions over 0.14 (#255) (Elliott Murray, Tue Aug 17 18:51:52 2021 +0100) -- da49cd7 - chore: Releasing version 1.4.0 (Elliott Murray, Sat Aug 7 10:17:26 2021 +0100) -- 0089937 - fix: issue originating from snyk with requests and urllib (#252) (Elliott Murray, Sat Jul 31 12:46:15 2021 +0100) -- 903371b - feat: added support for message provider (#251) (Fabio Pulvirenti, Sat Jul 31 13:24:19 2021 +0200) -- 2c81029 - chore(snyk): update fastapi (#239) (Elliott Murray, Fri Jun 11 09:12:38 2021 +0100) - -## 1.4.1 - -- 7dc8864 - fix: make uvicorn versions over 0.14 (#255) (Elliott Murray, Tue Aug 17 18:51:52 2021 +0100) - -## 1.4.0 - -- 0089937 - fix: issue originating from snyk with requests and urllib (#252) (Elliott Murray, Sat Jul 31 12:46:15 2021 +0100) -- 903371b - feat: added support for message provider (#251) (Fabio Pulvirenti, Sat Jul 31 13:24:19 2021 +0200) -- 2c81029 - chore(snyk): update fastapi (#239) (Elliott Murray, Fri Jun 11 09:12:38 2021 +0100) - -## 1.3.9 - -- 98d9a4b - chore(ruby): update ruby standalen (#233) (Elliott Murray, Thu May 13 20:21:10 2021 +0100) -- 657e770 - fix: change default from empty string to empty list (#235) (Vasile Tofan, Thu May 13 22:20:47 2021 +0300) -- 99fd965 - chore: Releasing version 1.3.8 (Elliott Murray, Sat May 1 12:26:47 2021 +0100) -- 3c909f1 - docs: example uses date matcher (#231) (Elliott Murray, Sat May 1 11:51:28 2021 +0100) -- 6390144 - fix: fix datetime serialization issues in Format (#230) (Syed Muhammad Dawoud Sheraz Ali, Thu Apr 29 01:49:53 2021 +0500) - -## 1.3.8 - -- 3c909f1 - docs: example uses date matcher (#231) (Elliott Murray, Sat May 1 11:51:28 2021 +0100) -- 6390144 - fix: fix datetime serialization issues in Format (#230) (Syed Muhammad Dawoud Sheraz Ali, Thu Apr 29 01:49:53 2021 +0500) - -## 1.3.7 - -- 20f828f - fix(broker): token added to verify steps (#226) (Elliott Murray, Sat Apr 24 13:47:22 2021 +0100) -- c4fe422 - chore: Releasing version 1.3.6 (Elliott Murray, Tue Apr 20 20:58:50 2021 +0100) -- 34160a8 - fix: publish verification results was wrong (#222) (Elliott Murray, Tue Apr 20 20:58:20 2021 +0100) -- 2c0252c - Merge pull request #219 from pact-foundation/ci/revert_snyk_36 (Elliott Murray, Sat Apr 3 11:13:49 2021 +0100) -- 1a162cf - ci: revert docker36 back (Elliott Murray, Sat Apr 3 11:00:37 2021 +0100) -- 4282de4 - Merge pull request #217 from pact-foundation/snyk-fix-8f994ea63cfa41070b04b182dbd11c74 (Elliott Murray, Sat Apr 3 10:54:58 2021 +0100) -- 4eb3fbb - Merge pull request #216 from pact-foundation/snyk-fix-fc54d9c7fe536fffe78fbd34fc5fd7ea (Elliott Murray, Sat Apr 3 10:54:10 2021 +0100) -- 47373ff - fix: docker/py37.Dockerfile to reduce vulnerabilities (snyk-bot, Fri Apr 2 03:13:49 2021 +0000) -- e572221 - fix: docker/py38.Dockerfile to reduce vulnerabilities (snyk-bot, Fri Apr 2 01:10:57 2021 +0000) -- f293dbb - Merge pull request #215 from pact-foundation/snyk-fix-ab489d8931bdf95d6ef0d217aa1b2eb6 (Elliott Murray, Wed Mar 31 09:27:02 2021 +0100) -- 5946872 - fix: docker/py36.Dockerfile to reduce vulnerabilities (snyk-bot, Tue Mar 30 21:41:35 2021 +0000) - -## 1.3.6 - -- 34160a8 - fix: publish verification results was wrong (#222) (Elliott Murray, Tue Apr 20 20:58:20 2021 +0100) -- 2c0252c - Merge pull request #219 from pact-foundation/ci/revert_snyk_36 (Elliott Murray, Sat Apr 3 11:13:49 2021 +0100) -- 1a162cf - ci: revert docker36 back (Elliott Murray, Sat Apr 3 11:00:37 2021 +0100) -- 4282de4 - Merge pull request #217 from pact-foundation/snyk-fix-8f994ea63cfa41070b04b182dbd11c74 (Elliott Murray, Sat Apr 3 10:54:58 2021 +0100) -- 4eb3fbb - Merge pull request #216 from pact-foundation/snyk-fix-fc54d9c7fe536fffe78fbd34fc5fd7ea (Elliott Murray, Sat Apr 3 10:54:10 2021 +0100) -- 47373ff - fix: docker/py37.Dockerfile to reduce vulnerabilities (snyk-bot, Fri Apr 2 03:13:49 2021 +0000) -- e572221 - fix: docker/py38.Dockerfile to reduce vulnerabilities (snyk-bot, Fri Apr 2 01:10:57 2021 +0000) -- f293dbb - Merge pull request #215 from pact-foundation/snyk-fix-ab489d8931bdf95d6ef0d217aa1b2eb6 (Elliott Murray, Wed Mar 31 09:27:02 2021 +0100) -- 5946872 - fix: docker/py36.Dockerfile to reduce vulnerabilities (snyk-bot, Tue Mar 30 21:41:35 2021 +0000) -- d6c5f4a - chore: Releasing version 1.3.5 (Elliott Murray, Sun Mar 28 15:32:45 2021 +0100) -- 5864e47 - Merge pull request #213 from pact-foundation/fix/revert_some_publish (Elliott Murray, Sun Mar 28 15:27:50 2021 +0100) -- 94e597a - fix(publish): fixing the fix. Pact Python api uses only publish_version and ensures it follows that (Elliott Murray, Sun Mar 28 15:20:04 2021 +0100) -- e00f320 - chore: Releasing version 1.3.4 (Elliott Murray, Sat Mar 27 21:26:29 2021 +0000) -- c778c71 - Merge pull request #212 from pact-foundation/fix/verify_in_provider (Elliott Murray, Sat Mar 27 18:59:25 2021 +0000) -- ea0b64a - fix: verifier should now publish (Elliott Murray, Sat Mar 27 16:21:25 2021 +0000) -- 2c8779b - chore: Releasing version 1.3.3 (Elliott Murray, Thu Mar 25 21:23:29 2021 +0000) -- 5e282ff - Merge pull request #211 from anneschuth/fix/pass-pact-dir (Elliott Murray, Thu Mar 25 21:21:56 2021 +0000) -- 987c4fc - fix: pass pact_dir to publish() (Anne Schuth, Thu Mar 25 10:06:54 2021 +0100) -- 23a5129 - chore: Releasing version 1.3.2 (Elliott Murray, Sun Mar 21 14:32:50 2021 +0000) -- 57c8ae8 - Merge pull request #209 from pact-foundation/bug/fix_test_dir (Elliott Murray, Sun Mar 21 14:29:40 2021 +0000) -- af3dadf - fix: remove pacts from examples (Elliott Murray, Sun Mar 21 14:21:10 2021 +0000) -- 579f3f8 - fix: ensure path is passed to broker and allow running from root rather than test file (Elliott Murray, Sun Mar 21 13:18:01 2021 +0000) -- 7e0feab - Merge pull request #208 from pact-foundation/dependabot/pip/examples/e2e/jinja2-2.11.3 (Elliott Murray, Sat Mar 20 12:46:32 2021 +0000) -- f82c008 - chore(deps): bump jinja2 from 2.11.2 to 2.11.3 in /examples/e2e (dependabot[bot], Sat Mar 20 04:53:58 2021 +0000) -- ea6f635 - Merge pull request #206 from pact-foundation/chore/use-testcontainers (Elliott Murray, Sun Mar 14 11:05:01 2021 +0000) -- 565bc80 - chore: added some docs about how to use the e2e example (Elliott Murray, Sun Mar 14 11:02:35 2021 +0000) -- 1b4be80 - chore: spiking testcontainers (Elliott Murray, Sat Mar 13 11:15:52 2021 +0000) -- 01e4ec4 - chore: wip on using test containers on examples (Elliott Murray, Tue Mar 2 21:30:48 2021 +0000) -- 882f4a2 - Merge pull request #204 from pact-foundation/chore/use_pytest (Elliott Murray, Sat Feb 27 10:58:19 2021 +0000) -- 261f24b - chore: more clean up (Elliott Murray, Sat Feb 27 10:10:23 2021 +0000) -- 5a0934c - chore: update ci stuff (Elliott Murray, Sat Feb 27 09:57:31 2021 +0000) -- 66e79e2 - chore: move from nose to pytests as we are now 3.6+ (Elliott Murray, Sat Feb 27 09:34:37 2021 +0000) -- 6aee3e2 - chore: Releasing version 1.3.1 (Elliott Murray, Sat Feb 27 09:16:35 2021 +0000) -- 4440022 - Merge pull request #203 from pact-foundation/fix/version_confusion (Elliott Murray, Sat Feb 27 09:15:08 2021 +0000) -- 9cac2d7 - fix: introduced and renamed specification version (Elliott Murray, Tue Feb 23 21:22:36 2021 +0000) -- 64d7bdc - chore: Releasing version 1.3.0 (Elliott Murray, Tue Jan 26 18:45:58 2021 +0000) -- eaa90e1 - Merge pull request #194 from williaminfante/feat/pact-message-2 (Elliott Murray, Mon Jan 25 08:48:00 2021 +0000) -- 5ed73db - test: consider publish to broker with no pact_dir argument (William Infante, Mon Jan 25 17:19:08 2021 +1100) -- e097153 - docs: update readme (William Infante, Mon Jan 25 17:18:35 2021 +1100) -- fc0d91c - feat: address PR comments (William Infante, Mon Jan 25 10:30:33 2021 +1100) -- 5448d8c - test: remove mock and check generated json file (William Infante, Wed Jan 20 21:07:39 2021 +1100) -- abd3574 - fix: few more tests to improve coverage (Tuan Pham, Wed Jan 20 09:23:13 2021 +1100) -- 0ef971f - fix: improve test coverage (Tuan Pham, Tue Jan 19 15:11:11 2021 +1100) -- e543f04 - chore: add missing import (William Infante, Tue Jan 19 13:58:17 2021 +1100) -- bc7ff78 - chore: pydocstyle (Tuan Pham, Tue Jan 19 10:40:12 2021 +1100) -- d4235f9 - chore: flake8, clean up deadcode (Tuan Pham, Tue Jan 19 00:53:03 2021 +1100) -- 19827d0 - chore: remove test param for provider (Tuan Pham, Mon Jan 18 16:06:42 2021 +1100) -- 912b477 - chore: flake8 revert (Tuan Pham, Mon Jan 18 16:04:00 2021 +1100) -- 4a730ae - fix: revert changes to quotes (Tuan Pham, Mon Jan 18 15:57:14 2021 +1100) -- cfe35cc - feat: update message handler to be independent of pact (William Infante, Mon Jan 18 13:12:17 2021 +1100) -- 12b4a50 - fix: flake8 warning (Tuan Pham, Mon Jan 18 12:50:50 2021 +1100) -- 7afe693 - test: update message handler condition based on content (William Infante, Mon Jan 18 12:37:50 2021 +1100) -- 79106bb - feat: move publish function to broker class (Tuan Pham, Mon Jan 18 11:30:35 2021 +1100) -- a04b954 - docs: add readme for message consumer (William Infante, Mon Jan 18 09:48:01 2021 +1100) -- 8672e2f - feat: update handler to handle error exceptions (William Infante, Fri Jan 15 16:24:38 2021 +1100) -- 47b7434 - feat: change dummy handler to a message handler (William Infante, Fri Jan 15 14:06:55 2021 +1100) -- a18ce3e - test: create external dummy handler in test (William Infante, Fri Jan 15 12:31:49 2021 +1100) -- 7358049 - chore: remove log_dir, refactor test (Tuan Pham, Fri Jan 15 09:11:55 2021 +1100) -- 6718b4c - feat: update message pact tests (William Infante, Thu Jan 14 16:12:44 2021 +1100) -- bf9864f - feat: add more test (William Infante, Thu Jan 14 16:09:52 2021 +1100) -- 84681b4 - fix: try different way to mock (Tuan Pham, Thu Jan 14 15:10:36 2021 +1100) -- 86cfe8e - chore: add generate_pact_test (Tuan Pham, Thu Jan 14 14:26:42 2021 +1100) -- a1c19b6 - fix: add missing conftest (Tuan Pham, Thu Jan 14 11:02:08 2021 +1100) -- a98850a - chore: add missing files in src (Tuan Pham, Thu Jan 14 10:53:35 2021 +1100) -- 63452aa - chore: fix bad merge (Tuan Pham, Thu Jan 14 10:47:43 2021 +1100) -- 85fc77f - feat: add pact-message integration test (Tuan Pham, Thu Jan 14 10:32:02 2021 +1100) -- 11793f5 - feat: add pact-message integration (Tuan Pham, Thu Jan 14 10:30:35 2021 +1100) -- 8546a26 - fix: linting (Tuan Pham, Thu Jan 14 10:38:52 2021 +1100) -- 65b69d7 - fix: remove publish fn for now (Tuan Pham, Thu Jan 14 10:38:10 2021 +1100) -- e31dd45 - feat: add constants test (William Infante, Wed Jan 13 17:28:45 2021 +1100) -- 955dbe1 - feat: update MessageConsumer and tests (William Infante, Wed Jan 13 16:38:30 2021 +1100) -- af5c9fb - feat: create basic tests for single pact message (William Infante, Wed Jan 13 15:52:07 2021 +1100) -- fea27c8 - feat: single message flow (William Infante, Wed Jan 13 15:03:41 2021 +1100) -- 9047855 - feat: add MessageConsumer (William Infante, Wed Jan 13 12:45:19 2021 +1100) -- 40edd39 - feat: initial interface (William Infante, Tue Jan 12 16:59:46 2021 +1100) -- 2946242 - Merge pull request #198 from pact-foundation/chore/deprecate_python35 (Elliott Murray, Sat Jan 23 15:26:01 2021 +0000) -- 0111698 - fix: add e2e example test into ci back in (Elliott Murray, Sat Jan 23 15:25:07 2021 +0000) -- 0cdc7e9 - chore: remove python35 and 34 and add 39 (Elliott Murray, Sat Jan 23 15:20:47 2021 +0000) -- 3885c60 - Merge pull request #197 from pact-foundation/fix/pull_request_trigger_workflow (Elliott Murray, Sat Jan 23 10:51:06 2021 +0000) -- 925b0ac - ci: pr not triggering workflow (Elliott Murray, Sat Jan 23 10:44:36 2021 +0000) -- 8f9a925 - Merge pull request #195 from cdambo/pass-pact-dir-to-cli (Elliott Murray, Sat Jan 23 10:39:53 2021 +0000) -- 545fc37 - fix: send to cli pact_files with the pact_dir in their path (Chanan Damboritz, Tue Jan 19 18:51:17 2021 +0200) - -## 1.3.5 - -- 5864e47 - Merge pull request #213 from pact-foundation/fix/revert_some_publish (Elliott Murray, Sun Mar 28 15:27:50 2021 +0100) -- 94e597a - fix(publish): fixing the fix. Pact Python api uses only publish_version and ensures it follows that (Elliott Murray, Sun Mar 28 15:20:04 2021 +0100) - -## 1.3.4 - -- c778c71 - Merge pull request #212 from pact-foundation/fix/verify_in_provider (Elliott Murray, Sat Mar 27 18:59:25 2021 +0000) -- ea0b64a - fix: verifier should now publish (Elliott Murray, Sat Mar 27 16:21:25 2021 +0000) - -## 1.3.3 - -- 5e282ff - Merge pull request #211 from anneschuth/fix/pass-pact-dir (Elliott Murray, Thu Mar 25 21:21:56 2021 +0000) -- 987c4fc - fix: pass pact_dir to publish() (Anne Schuth, Thu Mar 25 10:06:54 2021 +0100) - -### 1.3.2 - -- 57c8ae8 - Merge pull request #209 from pact-foundation/bug/fix_test_dir (Elliott Murray, Sun Mar 21 14:29:40 2021 +0000) -- af3dadf - fix: remove pacts from examples (Elliott Murray, Sun Mar 21 14:21:10 2021 +0000) -- 579f3f8 - fix: ensure path is passed to broker and allow running from root rather than test file (Elliott Murray, Sun Mar 21 13:18:01 2021 +0000) -- 7e0feab - Merge pull request #208 from pact-foundation/dependabot/pip/examples/e2e/jinja2-2.11.3 (Elliott Murray, Sat Mar 20 12:46:32 2021 +0000) -- f82c008 - chore(deps): bump jinja2 from 2.11.2 to 2.11.3 in /examples/e2e (dependabot[bot], Sat Mar 20 04:53:58 2021 +0000) -- ea6f635 - Merge pull request #206 from pact-foundation/chore/use-testcontainers (Elliott Murray, Sun Mar 14 11:05:01 2021 +0000) -- 565bc80 - chore: added some docs about how to use the e2e example (Elliott Murray, Sun Mar 14 11:02:35 2021 +0000) -- 1b4be80 - chore: spiking testcontainers (Elliott Murray, Sat Mar 13 11:15:52 2021 +0000) -- 01e4ec4 - chore: wip on using test containers on examples (Elliott Murray, Tue Mar 2 21:30:48 2021 +0000) -- 882f4a2 - Merge pull request #204 from pact-foundation/chore/use_pytest (Elliott Murray, Sat Feb 27 10:58:19 2021 +0000) -- 261f24b - chore: more clean up (Elliott Murray, Sat Feb 27 10:10:23 2021 +0000) -- 5a0934c - chore: update ci stuff (Elliott Murray, Sat Feb 27 09:57:31 2021 +0000) -- 66e79e2 - chore: move from nose to pytests as we are now 3.6+ (Elliott Murray, Sat Feb 27 09:34:37 2021 +0000) - -## 1.3.1 - -- 4440022 - Merge pull request #203 from pact-foundation/fix/version_confusion (Elliott Murray, Sat Feb 27 09:15:08 2021 +0000) -- 9cac2d7 - fix: introduced and renamed specification version (Elliott Murray, Tue Feb 23 21:22:36 2021 +0000) - -## 1.3.0 - -- eaa90e1 - Merge pull request #194 from williaminfante/feat/pact-message-2 (Elliott Murray, Mon Jan 25 08:48:00 2021 +0000) -- 5ed73db - test: consider publish to broker with no pact_dir argument (William Infante, Mon Jan 25 17:19:08 2021 +1100) -- e097153 - docs: update readme (William Infante, Mon Jan 25 17:18:35 2021 +1100) -- fc0d91c - feat: address PR comments (William Infante, Mon Jan 25 10:30:33 2021 +1100) -- 5448d8c - test: remove mock and check generated json file (William Infante, Wed Jan 20 21:07:39 2021 +1100) -- abd3574 - fix: few more tests to improve coverage (Tuan Pham, Wed Jan 20 09:23:13 2021 +1100) -- 0ef971f - fix: improve test coverage (Tuan Pham, Tue Jan 19 15:11:11 2021 +1100) -- e543f04 - chore: add missing import (William Infante, Tue Jan 19 13:58:17 2021 +1100) -- bc7ff78 - chore: pydocstyle (Tuan Pham, Tue Jan 19 10:40:12 2021 +1100) -- d4235f9 - chore: flake8, clean up deadcode (Tuan Pham, Tue Jan 19 00:53:03 2021 +1100) -- 19827d0 - chore: remove test param for provider (Tuan Pham, Mon Jan 18 16:06:42 2021 +1100) -- 912b477 - chore: flake8 revert (Tuan Pham, Mon Jan 18 16:04:00 2021 +1100) -- 4a730ae - fix: revert changes to quotes (Tuan Pham, Mon Jan 18 15:57:14 2021 +1100) -- cfe35cc - feat: update message handler to be independent of pact (William Infante, Mon Jan 18 13:12:17 2021 +1100) -- 12b4a50 - fix: flake8 warning (Tuan Pham, Mon Jan 18 12:50:50 2021 +1100) -- 7afe693 - test: update message handler condition based on content (William Infante, Mon Jan 18 12:37:50 2021 +1100) -- 79106bb - feat: move publish function to broker class (Tuan Pham, Mon Jan 18 11:30:35 2021 +1100) -- a04b954 - docs: add readme for message consumer (William Infante, Mon Jan 18 09:48:01 2021 +1100) -- 8672e2f - feat: update handler to handle error exceptions (William Infante, Fri Jan 15 16:24:38 2021 +1100) -- 47b7434 - feat: change dummy handler to a message handler (William Infante, Fri Jan 15 14:06:55 2021 +1100) -- a18ce3e - test: create external dummy handler in test (William Infante, Fri Jan 15 12:31:49 2021 +1100) -- 7358049 - chore: remove log_dir, refactor test (Tuan Pham, Fri Jan 15 09:11:55 2021 +1100) -- 6718b4c - feat: update message pact tests (William Infante, Thu Jan 14 16:12:44 2021 +1100) -- bf9864f - feat: add more test (William Infante, Thu Jan 14 16:09:52 2021 +1100) -- 84681b4 - fix: try different way to mock (Tuan Pham, Thu Jan 14 15:10:36 2021 +1100) -- 86cfe8e - chore: add generate_pact_test (Tuan Pham, Thu Jan 14 14:26:42 2021 +1100) -- a1c19b6 - fix: add missing conftest (Tuan Pham, Thu Jan 14 11:02:08 2021 +1100) -- a98850a - chore: add missing files in src (Tuan Pham, Thu Jan 14 10:53:35 2021 +1100) -- 63452aa - chore: fix bad merge (Tuan Pham, Thu Jan 14 10:47:43 2021 +1100) -- 85fc77f - feat: add pact-message integration test (Tuan Pham, Thu Jan 14 10:32:02 2021 +1100) -- 11793f5 - feat: add pact-message integration (Tuan Pham, Thu Jan 14 10:30:35 2021 +1100) -- 8546a26 - fix: linting (Tuan Pham, Thu Jan 14 10:38:52 2021 +1100) -- 65b69d7 - fix: remove publish fn for now (Tuan Pham, Thu Jan 14 10:38:10 2021 +1100) -- e31dd45 - feat: add constants test (William Infante, Wed Jan 13 17:28:45 2021 +1100) -- 955dbe1 - feat: update MessageConsumer and tests (William Infante, Wed Jan 13 16:38:30 2021 +1100) -- af5c9fb - feat: create basic tests for single pact message (William Infante, Wed Jan 13 15:52:07 2021 +1100) -- fea27c8 - feat: single message flow (William Infante, Wed Jan 13 15:03:41 2021 +1100) -- 9047855 - feat: add MessageConsumer (William Infante, Wed Jan 13 12:45:19 2021 +1100) -- 40edd39 - feat: initial interface (William Infante, Tue Jan 12 16:59:46 2021 +1100) -- 2946242 - Merge pull request #198 from pact-foundation/chore/deprecate_python35 (Elliott Murray, Sat Jan 23 15:26:01 2021 +0000) -- 0111698 - fix: add e2e example test into ci back in (Elliott Murray, Sat Jan 23 15:25:07 2021 +0000) -- 0cdc7e9 - chore: remove python35 and 34 and add 39 (Elliott Murray, Sat Jan 23 15:20:47 2021 +0000) -- 3885c60 - Merge pull request #197 from pact-foundation/fix/pull_request_trigger_workflow (Elliott Murray, Sat Jan 23 10:51:06 2021 +0000) -- 925b0ac - ci: pr not triggering workflow (Elliott Murray, Sat Jan 23 10:44:36 2021 +0000) -- 8f9a925 - Merge pull request #195 from cdambo/pass-pact-dir-to-cli (Elliott Murray, Sat Jan 23 10:39:53 2021 +0000) -- 545fc37 - fix: send to cli pact_files with the pact_dir in their path (Chanan Damboritz, Tue Jan 19 18:51:17 2021 +0200) - -## 1.2.11 - -- ba10318 - Merge pull request #192 from pact-foundation/fix/deploy_wheel_fix (Elliott Murray, Tue Dec 29 20:05:52 2020 +0000) -- 289e784 - fix: not creating wheel (Elliott Murray, Tue Dec 29 20:00:19 2020 +0000) -- d217e67 - chore: Releasing version 1.2.10 (Elliott Murray, Sat Dec 19 12:41:02 2020 +0000) -- 9438449 - Merge pull request #191 from pact-foundation/build-and-test-with-github-actions (Elliott Murray, Sat Dec 19 12:38:23 2020 +0000) -- 2796ef5 - docs: Added badge to README (Elliott Murray, Sat Dec 19 09:37:22 2020 +0000) -- f1b6968 - ci: add publishing actions (Matthew Balvanz, Thu Dec 17 09:09:10 2020 -0600) -- 287a32e - ci: removed Travis CI configuration (Matthew Balvanz, Wed Dec 16 21:18:09 2020 -0600) -- 77dd4d5 - ci(github actions): added Github Actions configuration for build and test (Matthew Balvanz, Wed Dec 16 21:07:06 2020 -0600) -- d6f02ca - Merge pull request #189 from noelslice/master (Elliott Murray, Fri Dec 4 08:39:33 2020 +0000) -- c24a73f - docs: typo in pact-verifier help string: PUT -> POST for --provider-states-setup-url (Noel Dawe, Thu Dec 3 22:39:31 2020 -0500) -- 4cdabd1 - Merge pull request #188 from pact-foundation/docs/example/fastapi (Elliott Murray, Sun Nov 29 14:53:24 2020 +0000) -- 6728cb9 - docs(example): created example and have relative imports kinda working. Provider not working as it cant find one of our urls (Elliott Murray, Sun Nov 29 13:34:42 2020 +0000) -- 9358097 - Merge pull request #184 from pact-foundation/chore/update_python_version (Elliott Murray, Sat Nov 28 10:06:53 2020 +0000) -- 48a2a21 - Merge pull request #186 from jstoebel/patch-2 (Elliott Murray, Sat Nov 21 10:39:48 2020 +0000) -- 74f9a4f - docs: fix small typo in `with_request` doc string (Jacob Stoebel, Wed Nov 18 14:51:03 2020 -0500) -- 4e4ed26 - chore: added run test to travis (Elliott Murray, Sun Nov 1 11:49:38 2020 +0000) -- 37e2f3a - chore: wqshell script to run flask in examples (Elliott Murray, Sun Nov 1 11:41:59 2020 +0000) -- b5d9d7b - chore(upgrade): upgrade python version to 3.8 (Elliott Murray, Sun Nov 1 11:12:10 2020 +0000) - -## 1.2.10 - -- 9438449 - Merge pull request #191 from pact-foundation/build-and-test-with-github-actions (Elliott Murray, Sat Dec 19 12:38:23 2020 +0000) -- 2796ef5 - docs: Added badge to README (Elliott Murray, Sat Dec 19 09:37:22 2020 +0000) -- f1b6968 - ci: add publishing actions (Matthew Balvanz, Thu Dec 17 09:09:10 2020 -0600) -- 287a32e - ci: removed Travis CI configuration (Matthew Balvanz, Wed Dec 16 21:18:09 2020 -0600) -- 77dd4d5 - ci(github actions): added Github Actions configuration for build and test (Matthew Balvanz, Wed Dec 16 21:07:06 2020 -0600) -- d6f02ca - Merge pull request #189 from noelslice/master (Elliott Murray, Fri Dec 4 08:39:33 2020 +0000) -- c24a73f - docs: typo in pact-verifier help string: PUT -> POST for --provider-states-setup-url (Noel Dawe, Thu Dec 3 22:39:31 2020 -0500) -- 4cdabd1 - Merge pull request #188 from pact-foundation/docs/example/fastapi (Elliott Murray, Sun Nov 29 14:53:24 2020 +0000) -- 6728cb9 - docs(example): created example and have relative imports kinda working. Provider not working as it cant find one of our urls (Elliott Murray, Sun Nov 29 13:34:42 2020 +0000) -- 9358097 - Merge pull request #184 from pact-foundation/chore/update_python_version (Elliott Murray, Sat Nov 28 10:06:53 2020 +0000) -- 48a2a21 - Merge pull request #186 from jstoebel/patch-2 (Elliott Murray, Sat Nov 21 10:39:48 2020 +0000) -- 74f9a4f - docs: fix small typo in `with_request` doc string (Jacob Stoebel, Wed Nov 18 14:51:03 2020 -0500) -- 4e4ed26 - chore: added run test to travis (Elliott Murray, Sun Nov 1 11:49:38 2020 +0000) -- 37e2f3a - chore: wqshell script to run flask in examples (Elliott Murray, Sun Nov 1 11:41:59 2020 +0000) -- b5d9d7b - chore(upgrade): upgrade python version to 3.8 (Elliott Murray, Sun Nov 1 11:12:10 2020 +0000) - -## 1.2.9 - -- 4430681 - Merge pull request #183 from thatguysimon/feat/verifier-class-consumer-version-selectors (Elliott Murray, Mon Oct 19 15:35:47 2020 +0100) -- 683a931 - fix: Fix flaky tests using OrderedDict (Simon Nizov, Mon Oct 19 17:21:21 2020 +0300) -- 33be267 - style: Fix one more linting issue (Simon Nizov, Mon Oct 19 11:22:05 2020 +0300) -- e7c87ce - style: Fix linting issues (Simon Nizov, Mon Oct 19 11:16:59 2020 +0300) -- ee2eda0 - feat(verifier): Allow setting consumer_version_selectors on Verifier (Simon Nizov, Mon Oct 19 11:01:18 2020 +0300) - -## 1.2.8 - -- 4c68fd4 - Merge pull request #182 from thatguysimon/feat/enable-wip-pacts (Elliott Murray, Sat Oct 17 16:00:50 2020 +0100) -- 9ea14d3 - refactor: Extract input validation in call_verify out into a dedicated method (Simon Nizov, Sat Oct 17 17:27:49 2020 +0300) -- 5a5969d - fix: Fix command building bug (Simon Nizov, Sat Oct 17 15:40:55 2020 +0300) -- b8c0006 - style: Fix linting (Simon Nizov, Sat Oct 17 15:18:29 2020 +0300) -- fc3d7ae - feat(verifier): Support include-wip-pacts-since in CLI (Simon Nizov, Sat Oct 17 15:03:38 2020 +0300) -- a0eca4c - Merge pull request #180 from elliottmurray/docs/example_flaskr (Elliott Murray, Fri Oct 16 11:13:11 2020 +0100) -- a8a07d4 - docs(examples): changed provider example to use atexit (Elliott Murray, Fri Oct 16 10:54:25 2020 +0100) -- 186f4f4 - Merge pull request #179 from pact-foundation/docs/example_readme (Elliott Murray, Thu Oct 15 10:13:13 2020 +0100) -- 2f66618 - docs(examples): tweak to readme (Elliott Murray, Thu Oct 15 10:08:52 2020 +0100) - -## 1.2.7 - -- 90b71d2 - Merge pull request #178 from pact-foundation/fix/custom_header_typo (Elliott Murray, Fri Oct 9 12:47:37 2020 +0100) -- b07ef69 - fix(verifier): headers not propagated properly (Elliott Murray, Fri Oct 9 12:24:25 2020 +0100) -- 0e9b71c - Merge pull request #177 from pact-foundation/docs/remove_handcrafted_broker (Elliott Murray, Fri Oct 9 12:01:24 2020 +0100) -- 2db7008 - docs(examples): removed manual publish to broker (Elliott Murray, Fri Oct 9 11:54:30 2020 +0100) - -## 1.2.6 - -- 1192bd6 - Merge pull request #173 from copalco/master (Elliott Murray, Thu Sep 10 15:30:07 2020 +0100) -- 5db7100 - feat(verifier): allow to use unauthenticated brokers (Piotr Kopalko, Thu Sep 10 14:12:12 2020 +0200) - -## 1.2.5 - -- 46372c7 - Merge pull request #171 from m-aciek/enable-pending (Elliott Murray, Wed Sep 9 10:03:02 2020 +0100) -- e840587 - fix(verifier): remove superfluous verbose mentions (Maciej Olko, Sat Sep 5 21:33:52 2020 +0200) -- c64bec1 - refactor(verifier): add enable_pending to signature of verify methods (Maciej Olko, Sat Sep 5 21:32:33 2020 +0200) -- e6c9ed0 - feat(verifier): support --enable-pending flag in CLI (Maciej Olko, Thu Sep 3 15:33:40 2020 +0200) -- 2b57446 - feat(verifier): pass enable_pending flag in Verifier's methods (Maciej Olko, Thu Sep 3 17:03:08 2020 +0200) -- d51c88d - test: bump mock to 3.0.5 (m-aciek, Thu Sep 3 23:42:00 2020 +0200) -- 39de1f3 - feat(verifier): add enable_pending argument handling in verify wrapper (Maciej Olko, Thu Sep 3 15:33:07 2020 +0200) -- fc6c365 - fix(verifier): remove superfluous option from verify CLI command (Maciej Olko, Thu Sep 3 13:30:57 2020 +0200) -- fbbd5fa - ci(pre-commit): add commitizen to pre-commit configuration (Maciej Olko, Thu Sep 3 17:19:45 2020 +0200) - -## 1.2.4 - -- a594e22 - Merge pull request #170 from alecgerona/feat/consumer-version-selector (Elliott Murray, Thu Aug 27 15:21:45 2020 +0100) -- 05c5e41 - docs(cli): improve cli help grammar (Alexandre Gerona, Thu Aug 27 06:28:56 2020 +0800) -- 49d5f7c - docs: update README.md with relevant option documentation (Alexandre Gerona, Thu Aug 27 06:22:37 2020 +0800) -- 5a99528 - feat(cli): add consumer-version-selector option (Alexandre Gerona, Thu Aug 27 06:22:07 2020 +0800) - -## 1.2.3 - -- 8188d88 - chore: fix release script (Elliott Murray, Wed Aug 26 12:46:10 2020 +0100) -- e0e5106 - Merge pull request #169 from pact-foundation/chore/update_pr_scripts (Elliott Murray, Wed Aug 26 10:24:47 2020 +0100) -- 81fd653 - chore: release script updates version automatically now (Elliott Murray, Wed Aug 26 10:16:14 2020 +0100) -- 773d3f9 - chore: script now uses gh over hub (Elliott Murray, Wed Aug 26 10:03:06 2020 +0100) -- 468e4ad - Merge pull request #168 from pact-foundation/chore/upgrade-to-pact-ruby-standalone-1-88-3 (Elliott Murray, Wed Aug 26 09:49:33 2020 +0100) -- ce944fe - feat: update standalone to 1.88.3 (Elliott Murray, Wed Aug 26 09:08:27 2020 +0100) - -## 1.2.2 - -- 2c52053 - Merge pull request #167 from pact-foundation/feat/add_env_vars_verify (Elliott Murray, Mon Aug 24 16:08:04 2020 +0100) -- ce62588 - feat: added env vars for broker verify (Elliott Murray, Mon Aug 24 16:03:44 2020 +0100) -- 880fff2 - Merge pull request #165 from pact-foundation/docs/https_fix (Elliott Murray, Thu Aug 20 12:43:12 2020 +0100) -- 1a3605e - docs: https svg (Elliott Murray, Thu Aug 20 12:37:01 2020 +0100) - -## 1.2.1 - -- 69a4a9a - Merge pull request #163 from elliottmurray/fix/custom_header (Elliott Murray, Sat Aug 8 10:17:20 2020 +0100) -- 88b7d9f - fix: custom headers had a typo (Elliott Murray, Sat Aug 1 11:08:54 2020 +0100) -- f501f19 - Merge pull request #161 from pact-foundation/docs/verifier_docs_examples (Elliott Murray, Fri Jul 24 12:30:35 2020 +0100) -- 9875c71 - docs: merged 2 examples (Elliott Murray, Fri Jul 24 12:00:37 2020 +0100) -- 6f0d3ac - docs: Example code verifier (Elliott Murray, Fri Jul 24 11:31:17 2020 +0100) - -## 1.2.0 - -- 2b844c5 - Merge pull request #159 from pact-foundation/feat/fix_provider_classs (Elliott Murray, Fri Jul 24 09:47:46 2020 +0100) -- 9c565bb - feat: fixing up tests and examples and code for provider class (Elliott Murray, Mon Jul 20 15:57:49 2020 +0100) -- d4072ed - Merge pull request #156 from pact-foundation/feat/provider_verifier (Elliott Murray, Thu Jul 16 13:31:18 2020 +0100) -- 926a611 - feat: create beta verifier class and api (Elliott Murray, Wed Jun 10 21:31:47 2020 +0100) -- 4635a07 - chore: added semantic yml for git messages (Elliott Murray, Sun Jun 28 12:43:24 2020 +0100) -- ff9894a - Merge pull request #154 from elliottmurray/style/git_message (Elliott Murray, Sat Jun 27 13:31:16 2020 +0100) -- be6697f - fix: change to head from master (Elliott Murray, Sat Jun 27 13:08:08 2020 +0100) - -## 1.1.0 - -- 1079417 - test (Elliott Murray, Thu Jun 25 10:02:14 2020 +0100) -- 7fe1ef4 - Releasing version 1.1.0 (Elliott Murray, Thu Jun 25 09:41:42 2020 +0100) -- fafc3d5 - Merge pull request #147 from pact-foundation/feat/add_logging_params (Elliott Murray, Thu Jun 25 09:24:34 2020 +0100) -- 8ce7d44 - Added logging params (Elliott Murray, Wed Jun 24 11:58:25 2020 +0100) -- b6450b8 - Merge pull request #146 from pact-foundation/chore/upgrade-to-pact-ruby-standalone-1-86-0 (Elliott Murray, Wed Jun 24 10:59:29 2020 +0100) -- bf43d8a - feat: update standalone to 1.86.0 (Beth Skurrie, Wed Jun 24 09:31:18 2020 +1000) -- 529dfb7 - Merge pull request #145 from jstoebel/patch-1 (Elliott Murray, Thu Jun 11 12:00:51 2020 +0100) -- 9359d34 - Remove typo from examples/e2e requirements.txt (Jacob Stoebel, Thu Jun 11 06:47:02 2020 -0400) -- aee95ed - Merge pull request #144 from pact-foundation/chore_cleanup (Elliott Murray, Wed Jun 10 21:38:12 2020 +0100) -- 9c71ea0 - chore: removed some files and moved a few things around (Elliott Murray, Wed Jun 10 21:33:37 2020 +0100) - -## v1.0.1 - -- 8c78ff7 - Releasing version 1.0.1 (Elliott Murray, Wed Jun 3 11:01:39 2020 +0100) -- 63f0e3e - Merge pull request #142 from elliottmurray/ssl_verify (Elliott Murray, Wed Jun 3 09:50:10 2020 +0100) -- cd43bd0 - Removed coverage (Elliott Murray, Tue Jun 2 21:41:52 2020 +0100) -- 30e6f86 - Fixed flake (Elliott Murray, Tue Jun 2 21:32:01 2020 +0100) -- 1a11320 - Fix unit tests (Elliott Murray, Tue Jun 2 21:29:56 2020 +0100) -- 353d054 - travis code coverage (Elliott Murray, Tue Jun 2 21:14:37 2020 +0100) -- c08babd - Fixing unit tests command in tox and travis (Elliott Murray, Tue Jun 2 18:30:10 2020 +0100) -- 157676c - Allowed https communication to mock. Didnt fix tests (Elliott Murray, Tue Jun 2 17:47:08 2020 +0100) -- 60c9f5a - Fix deploy to pypi2 (Elliott Murray, Fri May 22 13:50:41 2020 +0100) -- e2c7e4e - Fix deploy to pypi (Elliott Murray, Fri May 22 13:41:27 2020 +0100) - -## v1.0.0 - -- 2c6e4eb - Releasing version 1.0.0 (Elliott Murray, Fri May 22 13:30:49 2020 +0100) -- c68ccb7 - Merge pull request #140 from elliottmurray/python2_deprecate (Elliott Murray, Fri May 22 13:29:38 2020 +0100) -- 8bc6d48 - Release script to make life a bit easier (Elliott Murray, Thu May 21 12:32:27 2020 +0100) -- a845f71 - Removed 2.x support (Elliott Murray, Thu May 21 12:19:16 2020 +0100) -- 562e047 - Merge pull request #138 from pyasi/pyasi_add_matcher_regexes (Elliott Murray, Fri May 15 14:10:28 2020 +0100) -- db39d87 - remove virtualenv (Peter Yasi, Fri May 15 09:02:01 2020 -0400) -- cccd30a - Add Format to the standard pact package (Peter Yasi, Fri May 15 08:55:57 2020 -0400) -- b78ac6d - Merge branch 'master' into pyasi_add_matcher_regexes (Peter Yasi, Fri May 15 08:13:30 2020 -0400) -- 35dfa0d - add enum34 a a dep for py27-install (Peter Yasi, Thu May 14 20:52:09 2020 -0400) -- 1fcc6c1 - Pydocstyle fixes, will still need fix for no enum in 2.7 (Peter Yasi, Thu May 14 19:49:23 2020 -0400) -- fe068e5 - Add examples to e2e tests (Peter Yasi, Thu May 14 19:07:31 2020 -0400) -- 5aaa82f - README documentation (Peter Yasi, Thu May 14 18:46:42 2020 -0400) -- 0d588f7 - pydocs and formatting (Peter Yasi, Thu May 14 18:19:32 2020 -0400) -- a21118c - Use raw strings to avoid deprecated escape sequence (Peter Yasi, Thu May 14 08:54:01 2020 -0400) -- 715d10f - Initial implementation with example unit tests (Peter Yasi, Thu May 14 00:00:30 2020 -0400) - -## 0.22.0 - -- d112a4a - Merge pull request #134 from elliottmurray/multiple-custom-provider-header (Elliott Murray, Mon May 11 16:32:49 2020 +0100) -- 58f8e6b - Fix some style issues (Elliott Murray, Wed Apr 29 12:35:00 2020 +0100) -- bf9bc2d - Added multiple click options for custom headers (Elliott Murray, Tue Apr 28 18:14:02 2020 +0100) -- 254ffc5 - Merge pull request #130 from elliottmurray/examples (Elliott Murray, Sat May 9 18:02:46 2020 +0100) -- 3898aee - Created examples folder (Elliott Murray, Thu Apr 2 14:03:17 2020 +0100) -- b859443 - Merge pull request #129 from elliottmurray/docker (Elliott Murray, Sat May 9 17:54:01 2020 +0100) -- 9b83da7 - Added bash to containers (Elliott Murray, Fri Apr 10 11:52:43 2020 +0100) -- 73db8fc - Remove subprocess requirement (Elliott Murray, Fri Apr 10 11:32:38 2020 +0100) -- f3315a1 - Added 38 and created build helper script (Elliott Murray, Wed Apr 1 17:11:06 2020 +0100) -- e7743de - Some readme and python37 (Elliott Murray, Wed Apr 1 14:25:20 2020 +0100) -- 515aeb2 - Tweaked the run script (Elliott Murray, Wed Apr 1 12:57:02 2020 +0100) -- 5a6acaf - Merge pull request #131 from elliottmurray/python38 (Elliott Murray, Sat May 9 17:47:13 2020 +0100) -- bb921eb - Updated to 3.8 (Elliott Murray, Sat Apr 4 16:39:18 2020 +0100) -- 12108c4 - Merge pull request #132 from pyasi/pyasi_test_refactor (Elliott Murray, Sat May 9 17:33:59 2020 +0100) -- 48ad173 - Merge pull request #135 from m-aciek/master (Elliott Murray, Sat May 9 17:21:52 2020 +0100) -- 6948482 - Merge pull request #136 from pact-foundation/chore/upgrade-to-pact-ruby-standalone-1-84-0 (Elliott Murray, Sat May 9 15:13:07 2020 +0100) -- 14603ac - feat: update standalone to 1.84.0 (Beth Skurrie, Sat May 2 09:43:30 2020 +1000) -- 410caf1 - chore: add script to create a PR to update the pact-ruby-standalone version (Beth Skurrie, Sat May 2 09:42:55 2020 +1000) -- b5af1fc - Fix missing normalization of consumer name while publishing pact (Maciej Olko, Thu Apr 30 08:50:17 2020 +0200) -- 5785782 - Move tests to standard tests dir (Peter Yasi, Fri Apr 17 14:03:04 2020 -0400) -- 88ea23d - docs: update RELEASING.md (Beth Skurrie, Tue Feb 18 10:46:11 2020 +1100) - -## 0.21.0 - -- 6352dda - feat: update to pact-ruby-standalone-1.79.0 (#127) (Beth Skurrie, Tue Feb 18 10:25:59 2020 +1100) -- 758d6ea - Converting to kwargs (Elliott Murray, Sat Feb 1 16:24:49 2020 +1100) -- 1388b8f - feat: support using environment variables to set pact broker configuration (mikahjc, Wed Jan 29 17:52:33 2020 -0700) -- ec7ff99 - Make verify tests compatible with Click v7.x (mikahjc, Tue Jun 11 16:37:13 2019 -0600) -- 5dcb56c - Add broker_token parameter for authentication (mikahjc, Tue Jun 11 16:16:46 2019 -0600) -- 1bdfb42 - Integrate the Ruby pact broker client to allow for automatic publishing of pacts (mikahjc, Tue Jun 11 11:13:18 2019 -0600) - -## 0.20.0 - -- 978d9f3 - fix typo (Jingjing Duan, Wed May 24 15:48:43 2017 -0700) -- 4ede7d5 - Merge pull request #117 from dlmiddlecote/feature/expose-more-options (Matt Fellows, Fri Jan 17 10:00:56 2020 +1100) -- 73ae8d2 - Update docs (Daniel Middlecote, Tue Jan 14 22:11:40 2020 +0000) -- 2bffe5e - Simple test case (Daniel Middlecote, Tue Jan 14 22:11:25 2020 +0000) -- 3ba51b5 - Add --broker-token support (Daniel Middlecote, Tue Jan 14 22:04:39 2020 +0000) -- d3a8ba6 - Update pact-ruby-standalone (Daniel Middlecote, Tue Jan 14 21:45:01 2020 +0000) -- 0cbb9d4 - Merge pull request #115 from ejrb/patch-1 (Matthew Balvanz, Sat Dec 14 20:49:56 2019 -0600) -- 0c85502 - match platforms like 'macOS-\*' to osx suffix (ejrb, Mon Dec 9 11:13:19 2019 +0000) -- 9a0eaa7 - Merge pull request #109 from pact-foundation/dependabot/pip/flask-1.0 (Matthew Balvanz, Mon Sep 30 21:35:20 2019 -0500) -- 6f70a28 - Bump flask from 0.11.1 to 1.0 (dependabot[bot], Sat Sep 28 19:20:11 2019 +0000) - -## 0.19.0 - -- fed5fba - Start testing in Python 3.7 (Matthew Balvanz, Sat Sep 28 15:18:17 2019 -0500) -- 19aa689 - Adjust tests to support click 2.0.0 to 7.0.0 (Matthew Balvanz, Sat Sep 28 15:04:53 2019 -0500) -- 9d4d6f3 - Merge pull request #94 from yangineer/optional_given (Matthew Balvanz, Sat Sep 28 14:52:49 2019 -0500) -- b286b30 - Merge branch 'master' into optional_given (Matthew Balvanz, Sat Sep 28 14:50:02 2019 -0500) -- 68e792a - Merge pull request #93 from francoiscampbell/pass_file_write_mode (Matthew Balvanz, Sat Sep 28 14:18:59 2019 -0500) -- 8927df6 - Updated the tests for Click v7 (Yang Wang, Sat Oct 20 00:34:37 2018 -0400) -- 125a1de - Changed given to be optional (Yang Wang, Sat Oct 20 00:27:07 2018 -0400) -- 68527e0 - max out click at 6.7 because 7.0 fails tests (Francois Campbell, Thu Oct 4 10:57:13 2018 -0400) -- 2452f42 - update tests (Francois Campbell, Thu Oct 4 10:56:42 2018 -0400) -- 1116601 - Add param docs (Francois Campbell, Thu Oct 4 10:04:50 2018 -0400) -- 48a7591 - Pass file_write_mode from Consumer to Pact (Francois Campbell, Thu Oct 4 10:00:37 2018 -0400) -- 6d39609 - Merge pull request #91 from szekar1/small_updates_to_docs (Matthew Balvanz, Fri Aug 24 13:49:05 2018 -0500) -- a5c8146 - Update README.md (bvccaneer, Fri Aug 24 19:23:26 2018 +0200) -- 4d40485 - adding documentation around #52 and fixing dead link for Matching docs (szekar1, Fri Aug 24 19:19:10 2018 +0200) - -## 0.18.0 - -- 4e8bb85 - Upgrade pact-ruby-standalone (Matthew Balvanz, Tue Aug 21 08:56:53 2018 -0500) -- 8a44feb - chore(docs): update contact information (Matt Fellows, Thu Aug 2 17:18:43 2018 +1000) - -## 0.17.0 - -- cf5d5bc - Merge pull request #87 from acabelloj/custom-provider-header-support (Matthew Balvanz, Fri Jul 20 22:27:33 2018 -0500) -- cc61427 - Fixes #83 The verifier always returns exit code 0 (Matthew Balvanz, Fri Jul 20 22:08:26 2018 -0500) -- 239da1c - Remove Python 3.3 from Travis builds (Matthew Balvanz, Wed Jul 4 10:39:12 2018 -0500) -- 273b3fd - Remove Python 3.3 testing (Matthew Balvanz, Wed Jul 4 10:36:01 2018 -0500) -- 01c6763 - Add support to custom provider header (Alejandro Cabello Jiménez, Fri Jun 1 11:40:32 2018 +0200) - -## 0.16.1 - -- eecbb60 - Merge pull request #79 from shahha/fix-stopping-mock-service-on-windows (Matthew Balvanz, Fri Mar 16 08:45:19 2018 -0500) -- 4115264 - Added windows specific code to check if mock service is stopped. (Hardik Shah, Wed Mar 7 10:44:33 2018 +1100) - -## 0.16.0 - -- 30af240 - Merge pull request #78 from pact-foundation/standalone-1-29-2 (Matthew Balvanz☃, Fri Mar 2 22:05:12 2018 -0600) -- d428951 - Update to pact-ruby-standalone 1.29.2 (Matthew Balvanz, Fri Mar 2 21:59:08 2018 -0600) - -## 0.15.0 - -- eb925c3 - Merge pull request #77 from pact-foundation/standalone-1-9-1 (Matthew Balvanz☃, Fri Mar 2 21:22:35 2018 -0600) -- 2a2dcd1 - Upgrade to pact-ruby-standalone 1.9.1 (Matthew Balvanz, Fri Mar 2 21:18:25 2018 -0600) -- 53545be - Merge pull request #72 from fabianbuechler/reduce-server-start-timeout (Matthew Balvanz☃, Fri Mar 2 21:04:03 2018 -0600) -- b782e43 - Merge pull request #76 from pact-foundation/hide-ruby-stacks (Matthew Balvanz☃, Fri Mar 2 21:03:14 2018 -0600) -- 589224a - Hide Ruby stack traces by default (Matthew Balvanz, Fri Mar 2 20:56:59 2018 -0600) -- e952b37 - Reduce timeout in \_wait_for_server_start to 25s (Fabian Büchler, Fri Feb 9 11:04:01 2018 +0100) - -## 0.14.0 - -- 3070638 - Merge pull request #71 from pact-foundation/update-standalone-1-9-0 (Matthew Balvanz, Sat Feb 3 23:25:37 2018 -0600) -- 475703c - Resolves #58: Update to pact-ruby-standalone 1.9.0 (Matthew Balvanz, Sat Feb 3 23:12:22 2018 -0600) - -## 0.13.0 - -- 3316743 - Merge pull request #69 from jawu/#52-helper-function-for-assertion-with-matchers (Matthew Balvanz, Sat Jan 20 16:43:56 2018 -0600) -- ae7f333 - Merge pull request #70 from bethesque/issues/pact-provider-verifier-19 (Matthew Balvanz, Sat Jan 20 16:40:31 2018 -0600) -- 81597d9 - docs: remove reference to v3 pact in provider-states-setup-url (Beth Skurrie, Tue Jan 9 12:27:18 2018 +1100) -- 8bedfd4 - removed local files (Janneck Wullschleger, Wed Dec 20 05:12:08 2017 +0100) -- 5ab2648 - solves #52 added get_generated_values to resolve Mathers to their generated value for assertion (Janneck Wullschleger, Wed Dec 20 05:06:33 2017 +0100) - -## 0.12.0 - -- 149dfc7 - Merge pull request #67 from jawu/enable-possibility-to-use-mathers-in-path (Matthew Balvanz, Sun Dec 17 10:32:34 2017 -0600) -- fb97d2f - fixed doc string of Request (Janneck Wullschleger, Sat Dec 16 20:44:11 2017 +0100) -- c2c24cc - adjusted doc string of Request class to allow str and Matcher as path parameter (Janneck Wullschleger, Sat Dec 16 20:40:35 2017 +0100) -- 697a6a2 - fixed port parameter in e2e test for python 2.7 (Janneck Wullschleger, Thu Dec 14 15:08:05 2017 +0100) -- ca2eb92 - added from_term call in Request constructor to process path property for json transport (Janneck Wullschleger, Thu Dec 14 14:45:11 2017 +0100) - -## 0.11.0 - -- ad69039 - Merge pull request #63 from pact-foundation/run-specific-interactions (Matthew Balvanz, Sun Dec 17 09:53:35 2017 -0600) -- eb63864 - Output a rerun command when a verification fails (Matthew Balvanz, Sun Nov 19 20:44:06 2017 -0600) -- 7c7bc7d - Merge pull request #62 from dhoomakethu/master (Matthew Balvanz, Sun Nov 19 19:53:48 2017 -0600) -- c27a7a9 - #62 Fix flake8 issues 2 (sanjay, Sun Nov 19 11:18:15 2017 +0530) -- 382c46c - #62 fix flake issues (sanjay, Sun Nov 19 11:13:58 2017 +0530) -- cdcc85d - Add support to publish verification result to pact broker (sanjay, Tue Oct 31 12:41:52 2017 +0530) -- c1a5402 - Merge pull request #2 from pact-foundation/master (dhoomakethu, Tue Oct 31 12:15:53 2017 +0530) -- b91f6c3 - Merge pull request #1 from pact-foundation/master (dhoomakethu, Mon Aug 21 12:36:15 2017 +0530) - -## 0.10.0 - -- 821671e - Merge pull request #53 from pact-foundation/verify-directories (Matthew Balvanz, Sat Nov 18 23:26:05 2017 -0600) -- 8291bb7 - Resolve #22: --pact-url accepts directories (Matthew Balvanz, Sat Oct 7 11:35:37 2017 -0500) - -## 0.9.0 - -- 735aa87 - Set new project minimum requirements (Matthew Balvanz, Sun Oct 22 16:30:12 2017 -0500) -- 295f17c - Merge pull request #60 from ftobia/requirements (Matthew Balvanz, Sun Oct 22 16:09:59 2017 -0500) -- 1dc72da - Merge pull request #48 from bassdread/allow-later-versions-of-requests (Matthew Balvanz, Sun Oct 22 16:09:39 2017 -0500) -- 3265b45 - add suggestion (Chris Hannam, Fri Oct 20 09:33:05 2017 +0100) -- 33504a6 - Resolve #51 verify outputs text instead of bytes (Matthew Balvanz, Thu Oct 19 21:28:39 2017 -0500) -- 51dcda3 - Merge pull request #57 from jceplaras/fix-e2e-test-incorrect-number-of-arg (Matthew Balvanz, Thu Oct 19 20:57:49 2017 -0500) -- 1a4d136 - Relax version requirements in setup.py (vs requirements.txt) (ftobia, Fri Oct 13 19:42:46 2017 -0400) -- 8ece1d6 - Fix incorrect indent on test_incorrect_number_of_arguments on test_e2e (James Plaras, Fri Oct 13 12:54:56 2017 +0800) -- 5f8257b - Resolve #50: Note which version of the Pact specification is supported (Matthew Balvanz, Sat Oct 7 14:05:26 2017 -0500) -- e728301 - Resolve #45: Document request query parameter (Matthew Balvanz, Sat Oct 7 13:58:07 2017 -0500) -- 5de7200 - Merge pull request #49 from pact-foundation/rename-somethinglike (Matt Fellows, Wed Oct 4 22:36:21 2017 +1100) -- d73aa1c - Resolve #43: Rename SomethingLike to Like (Matthew Balvanz, Mon Sep 4 15:49:13 2017 -0500) -- a07c8b6 - Merge pull request #46 from bassdread/fix-setup-url-name (Matthew Balvanz, Mon Sep 4 15:44:45 2017 -0500) -- b5e1f95 - allow later versions of requests (Chris Hannam, Tue Aug 29 13:38:42 2017 +0100) -- 08fe123 - make setup-url name format match above reference (Chris Hannam, Fri Aug 25 11:03:35 2017 +0100) - -## 0.8.0 - -- edb6c72 - Merge pull request #41 from pact-foundation/fix-running-on-windows (Matthew Balvanz, Thu Aug 10 21:39:27 2017 -0500) -- 244fff1 - Merge pull request #42 from pact-foundation/deprecate-provider-states-url (Matthew Balvanz, Thu Aug 10 21:38:44 2017 -0500) -- 447b8bb - Resolve #17: Deprecate --provider-states-url (Matthew Balvanz, Sat Jul 29 11:53:05 2017 -0500) -- 4661406 - Move to using the `service` command with pact-mock-service (Matthew Balvanz, Sat Jul 29 10:00:47 2017 -0500) -- 04107db - Remove the PyPi server declaration to use the defaults (Matthew Balvanz, Sun Jul 16 09:05:30 2017 -0500) - -## v0.7.0 - -- 223ea76 - Merge pull request #32 from SimKev2/pacturls (Matthew Balvanz, Sun Jul 16 08:41:14 2017 -0500) -- e382eb4 - Add tests for #36 SomethingLike not supporting Terms (Matthew Balvanz, Sun Jul 16 08:36:58 2017 -0500) -- 05b4d70 - Merge pull request #37 from jeanbaptistepriez/fix-somethinglike (Matthew Balvanz, Sun Jul 16 08:30:28 2017 -0500) -- 29a2518 - Fix json generation of SomethingLike (https://github.com/pact-foundation/pact-python/issues/36) (jean-baptiste.priez, Wed Jul 12 20:01:58 2017 +0200) -- b6e1a8b - Issue: Cannot supply multiple files to pact-verifier - PR: Added deprecation warning instead of making api-breaking change (simkev2, Sat Jun 24 20:05:05 2017 -0500) -- 17aa15b - Issue: Cannot supply multiple files to pact-verifier - Updated '--pact-urls' to be a single comma separated string argument - Added '--pact-url' which can be specified multiple times (simkev2, Sat Jun 24 12:57:51 2017 -0500) -- 65b493d - Merge pull request #33 from bethesque/reamde (Matthew Balvanz, Tue Jun 27 08:58:08 2017 -0500) -- f5a5958 - Update README.md (Beth Skurrie, Sun Jun 25 10:37:03 2017 +1000) - -## v0.6.2 - -- 69caa40 - Merge pull request #35 from pact-foundation/fix-broker-credentials (Matt Fellows, Tue Jun 27 20:49:35 2017 +1000) -- d60f37f - Fix the use of broker credentials (Matthew Balvanz, Mon Jun 26 21:14:53 2017 -0500) - -## v0.6.1 - -- 14968ea - Merge pull request #34 from hartror/rh_version_fix (Matthew Balvanz, Mon Jun 26 20:23:29 2017 -0500) -- aca520f - pydocstyle is fussy, should have run it before pushing (Rory Hart, Sun Jun 25 20:11:26 2017 +1000) -- b70103c - Added docstring for **version**.py (Rory Hart, Sun Jun 25 20:08:50 2017 +1000) -- 2076e34 - Disabled flake8 F401 for **version** import (Rory Hart, Sun Jun 25 20:05:24 2017 +1000) -- 2912e07 - Version in setup.py reading **version**.py directly (Rory Hart, Sun Jun 25 19:40:08 2017 +1000) -- d137a21 - Split tox environments into test & install to replicate installation issue #31 (Rory Hart, Sun Jun 25 19:16:57 2017 +1000) -- f549ddf - Merge pull request #30 from bethesque/contributing (Matthew Balvanz, Sat Jun 24 12:43:30 2017 -0500) -- 1f19a0e - Update CONTRIBUTING.md (Beth Skurrie, Thu Jun 22 08:51:35 2017 +1000) -- 3198817 - Update CONTRIBUTING.md (Beth Skurrie, Thu Jun 22 08:36:57 2017 +1000) -- 7a08bb2 - Update CONTRIBUTING.md (Beth Skurrie, Thu Jun 22 08:35:27 2017 +1000) - -## v0.6.0 - -- 10aaaf6 - Merge pull request #27 from pact-foundation/download-pre-package-mock-service-and-verifier (Matthew Balvanz, Tue Jun 20 21:51:40 2017 -0500) -- a9b991b - Update to pact-ruby-standalone 1.0.0 (Matthew Balvanz, Mon Jun 19 10:17:09 2017 -0500) -- ab43c8b - Switch to installing the packages from pact-ruby-standalone (Matthew Balvanz, Wed May 31 21:00:51 2017 -0500) -- db3e7c3 - Use the compiled Ruby applications from pact-mock-service and pact-provider-verifier (Matthew Balvanz, Mon May 29 22:18:47 2017 -0500) - -## v0.5.0 - -- c085a01 - Merge pull request #26 from AnObfuscator/stub-multiple-requests (Matthew Balvanz, Mon Jun 19 09:14:51 2017 -0500) -- 22c0272 - Add support for stubbing multiple requests at the same time (AnObfuscator, Fri Jun 16 23:18:01 2017 -0500) - -## v0.4.1 - -- 66cf151 - Add RELEASING.md closes #18 (Matthew Balvanz, Tue May 30 22:41:06 2017 -0500) -- 3f61c91 - Add support for request bodies that are False in Python (Matthew Balvanz, Tue May 30 21:57:46 2017 -0500) -- a39c62f - Merge pull request #19 from ftobia/patch-1 (Matthew Balvanz, Tue May 30 21:42:41 2017 -0500) -- 95aa93a - Allow falsy responses (e.g. 0 not as a string). (Frank Tobia, Mon May 29 19:22:13 2017 -0400) -- dd3c703 - Merge pull request #16 from jduan/master (Jose Salvatierra, Thu May 25 09:20:10 2017 +0100) -- 978d9f3 - fix typo (Jingjing Duan, Wed May 24 15:48:43 2017 -0700) - -## v0.4.0 - -- 8bec271 - Setup Travis CI to publish to PyPi (Matthew Balvanz, Wed May 24 16:51:05 2017 -0500) -- d67a015 - Merge pull request #14 from pact-foundation/verify-pacts (Matthew Balvanz, Wed May 24 16:46:49 2017 -0500) -- 78bd029 - Add CONTRIBUTING.md file resolves #4 (Matthew Balvanz, Mon May 22 20:41:09 2017 -0500) -- d7c32c4 - Repository badges (Matthew Balvanz, Mon May 22 20:22:14 2017 -0500) -- 97122f1 - Merge pull request #13 from pact-foundation/update-developer-documentation (Matthew Balvanz, Sat May 20 20:55:06 2017 -0500) -- ea015eb - Increment project to v0.4.0 (Matthew Balvanz, Fri May 19 23:46:00 2017 -0500) -- 51eb338 - Command line application for verifying pacts (Matthew Balvanz, Fri May 19 22:24:06 2017 -0500) -- 4b0bbd7 - Update the developer instructions (Matthew Balvanz, Fri May 19 22:05:54 2017 -0500) - -## v0.3.0 - -- 3130f9a - Merge pull request #11 from pact-foundation/update-mock-service (Matthew Balvanz, Sun May 14 09:03:43 2017 -0500) -- 9b20d36 - Updated Versions of Pact Ruby applications (Matthew Balvanz, Sat May 13 09:43:44 2017 -0500) - -## v0.2.0 - -- 140f583 - Merge pull request #8 from pact-foundation/manage-mock-service (Matthew Balvanz, Sat May 13 09:18:40 2017 -0500) -- 5994c3a - pact-python manages the mock service for the user (Matthew Balvanz, Tue May 9 21:58:08 2017 -0500) -- 4bf7b8b - pact-python manages the mock service for the user (Matthew Balvanz, Mon May 1 20:12:53 2017 -0500) -- 0a278af - pact-python manages the mock service for the user (Matthew Balvanz, Tue Apr 18 21:23:18 2017 -0500) -- fd68b41 - Merge pull request #2 from pact-foundation/package-ruby-apps (Matthew Balvanz, Sat Apr 22 10:55:48 2017 -0500) -- 75a96dc - Package the Ruby Mock Service and Verifier (Matthew Balvanz, Tue Apr 4 23:14:11 2017 -0500) - -## v0.1.0 - -- 189c647 - Merge pull request #3 from pact-foundation/travis-ci (Jose Salvatierra, Fri Apr 7 21:40:02 2017 +0100) -- 559efb8 - Add Travis CI build (Matthew Balvanz, Fri Apr 7 11:12:01 2017 -0500) -- 8f074a0 - Merge pull request #1 from pact-foundation/initial-framework (Matthew Balvanz, Fri Apr 7 09:55:34 2017 -0500) -- f5caf9c - Initial pact-python implementation (Matthew Balvanz, Mon Apr 3 09:16:59 2017 -0500) -- bfb8380 - Initial pact-python implementation (Matthew Balvanz, Thu Mar 30 20:41:05 2017 -0500) + diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 000000000..356d8a452 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,105 @@ +#:schema https://json.schemastore.org/any.json +# git-cliff ~ default configuration file +# https://git-cliff.org/docs/configuration + +[changelog] +# template for the changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file. + + + + + +""" +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] _{{ timestamp | date(format="%Y-%m-%d") }}_ +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}_({{ commit.scope }})_ {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }}\ + {% if commit.breaking and commit.breaking_description %} + {{ " " }}\ + > {{ + commit.breaking_description + | split(pat="\n") + | join(sep=" ") + | replace(from=" ", to=" ") + | replace(from=" ", to=" ") + | replace(from=" ", to=" ") + | upper_first + }}\ + {% endif %}\ + {% endfor %} +{% endfor %} +{% if github.contributors %}\ + ### Contributors + {% for contributor in github.contributors %}\ + {% if contributor.username and contributor.username is ending_with("[bot]") %}{% continue %}{% endif %} + - @{{ contributor.username }}\ + {% endfor %} +{% endif %} + +"""# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing s +trim = true +# postprocessors +postprocessors = [] +# render body even when there are no releases to process +# render_always = true +# output file path +output = "CHANGELOG.md" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # Remove the PR number added by GitHub when merging PR in UI + { pattern = '\s*\(#([0-9]+)\)$', replace = "" }, + # Check spelling of the commit with https://github.com/crate-ci/typos + { pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# regex for parsing and grouping commits +commit_parsers = [ + # Ignore deps commits from the changelog + { message = "^(chore|fix)\\(deps.*\\)", skip = true }, + { message = "^chore: update changelog.*", skip = true }, + # Group commits by type + { group = "🚀 Features", message = "^feat" }, + { group = "🐛 Bug Fixes", message = "^fix" }, + { group = "🚜 Refactor", message = "^refactor" }, + { group = "⚡ Performance", message = "^perf" }, + { group = "🎨 Styling", message = "^style" }, + { group = "📚 Documentation", message = "^docs" }, + { group = "🧪 Testing", message = "^test" }, + { group = "◀️ Revert", message = "^revert" }, + { group = "⚙️ Miscellaneous Tasks", message = "^chore" }, + { group = "� Other", message = ".*" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = false +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" + +[remote.github] +owner = "pact-foundation" +repo = "pact-python" From 0aee1e1ad54e513dba64aca8844958116eb6cce5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 23 Jan 2025 12:20:00 +1100 Subject: [PATCH 0694/1376] chore: add taplo Taplo is an excellent tool to format/lint TOML files, and ensure compliance with available JSON Schemas. Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 6 + .taplo.toml | 10 + cliff.toml | 8 +- committed.toml | 1 + docs/scripts/.ruff.toml | 7 + docs/scripts/ruff.toml | 6 - examples/.ruff.toml | 13 +- pyproject.toml | 433 ++++++++++++++++---------------- tests/{ruff.toml => .ruff.toml} | 1 + 9 files changed, 255 insertions(+), 230 deletions(-) create mode 100644 .taplo.toml create mode 100644 docs/scripts/.ruff.toml delete mode 100644 docs/scripts/ruff.toml rename tests/{ruff.toml => .ruff.toml} (81%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82d8c3da0..38dabe315 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,6 +41,12 @@ repos: additional_dependencies: - '@biomejs/biome@1.9.4' + - repo: https://github.com/ComPWA/taplo-pre-commit + rev: v0.9.3 + hooks: + - id: taplo-format + - id: taplo-lint + - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.2 hooks: diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 000000000..14ddb3d98 --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,10 @@ +[schema] +path = "taplo://taplo.toml" + +[formatting] +align_entries = true +indent_entries = false +indent_tables = true +reorder_arrays = true +reorder_inline_tables = true +reorder_keys = true diff --git a/cliff.toml b/cliff.toml index 356d8a452..59890048a 100644 --- a/cliff.toml +++ b/cliff.toml @@ -13,6 +13,9 @@ All notable changes to this project will be documented in this file. """ + +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction body = """ {% if version %}\ ## [{{ version | trim_start_matches(pat="v") }}] _{{ timestamp | date(format="%Y-%m-%d") }}_ @@ -47,12 +50,13 @@ body = """ {% endfor %} {% endif %} -"""# template for the changelog body -# https://keats.github.io/tera/docs/#introduction +""" + # template for the changelog footer footer = """ """ + # remove the leading and trailing s trim = true # postprocessors diff --git a/committed.toml b/committed.toml index 86b00c78f..9cc157ebf 100644 --- a/committed.toml +++ b/committed.toml @@ -1,3 +1,4 @@ +#:schema https://raw.githubusercontent.com/crate-ci/committed/refs/heads/master/config.schema.json ## Configuration for committed ## ## See diff --git a/docs/scripts/.ruff.toml b/docs/scripts/.ruff.toml new file mode 100644 index 000000000..1dfa5d3e3 --- /dev/null +++ b/docs/scripts/.ruff.toml @@ -0,0 +1,7 @@ +#:schema https://json.schemastore.org/ruff.json +extend = "../../pyproject.toml" + +[lint] +ignore = [ + "INP001", # Forbid implicit namespaces +] diff --git a/docs/scripts/ruff.toml b/docs/scripts/ruff.toml deleted file mode 100644 index 7a981ad6f..000000000 --- a/docs/scripts/ruff.toml +++ /dev/null @@ -1,6 +0,0 @@ -extend = "../../pyproject.toml" - -[lint] -ignore = [ - "INP001", # Forbid implicit namespaces -] diff --git a/examples/.ruff.toml b/examples/.ruff.toml index 6b2ed4c15..7d112f1bc 100644 --- a/examples/.ruff.toml +++ b/examples/.ruff.toml @@ -1,15 +1,16 @@ +#:schema https://json.schemastore.org/ruff.json extend = "../pyproject.toml" [lint] ignore = [ - "S101", # Forbid assert statements "D103", # Require docstring in public function "D104", # Require docstring in public package "PLR2004", # Forbid Magic Numbers + "S101", # Forbid assert statements ] -[lint.per-file-ignores] -"tests/**.py" = [ - "INP001", # Forbid implicit namespaces - "PLR2004", # Forbid magic values -] + [lint.per-file-ignores] + "tests/**.py" = [ + "INP001", # Forbid implicit namespaces + "PLR2004", # Forbid magic values + ] diff --git a/pyproject.toml b/pyproject.toml index 75619b780..5cda0f2b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,19 @@ +#:schema https://json.schemastore.org/pyproject.json [project] -name = "pact-python" description = "Tool for creating and verifying consumer-driven contracts using the Pact framework." -dynamic = ["version"] +name = "pact-python" + +dynamic = ["version"] +keywords = ["contract-testing", "pact", "testing"] +license = { file = "LICENSE" } +readme = "README.md" authors = [ - { name = "Matthew Balvanz", email = "matthew.balvanz@workiva.com" }, { name = "Joshua Ellis", email = "josh@jpellis.me" }, + { 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", @@ -20,13 +22,13 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", - "Programming Language :: Python", - "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python", "Topic :: Software Development :: Testing", ] @@ -49,53 +51,46 @@ dependencies = [ "yarl ~=1.0", ] -[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/main/CHANGELOG.md" - -[project.scripts] -pact-verifier = "pact.cli.verify:main" - -[project.optional-dependencies] -# Linting and formatting tools use a more narrow specification to ensure -# developper consistency. All other dependencies are as above. -devel-types = [ - "mypy ==1.14.1", - "types-cffi ~=1.0", - "types-requests ~=2.0", -] -devel-docs = [ - "mkdocs ~= 1.5", - "mkdocs-material[imaging] ~= 9.4", - "mkdocs_gen_files ~= 0.5", - "mkdocs-literate-nav ~= 0.6", - "mkdocs-section-index ~= 0.3", - "mkdocstrings[python] ~= 0.23", -] -devel-test = [ - "aiohttp[speedups] ~=3.0", - "coverage[toml] ~=7.0", - "flask[async] ~=3.0", - "httpx ~=0.0", - "mock ~=5.0", - "pytest-asyncio ~=0.0", - "pytest-bdd ~=8.0", - "pytest-cov ~=6.0", - "pytest-rerunfailures ~=15.0", - "pytest-xdist ~=3.0", - "pytest ~=8.0", - "testcontainers ~=4.0", -] -devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.9.2"] - -################################################################################ -## Hatch Build Configuration -################################################################################ + [project.urls] + "Bug Tracker" = "https://github.com/pact-foundation/pact-python/issues" + "Changelog" = "https://github.com/pact-foundation/pact-python/blob/main/CHANGELOG.md" + "Documentation" = "https://docs.pact.io" + "Homepage" = "https://pact.io" + "Repository" = "https://github.com/pact-foundation/pact-python" + + [project.scripts] + pact-verifier = "pact.cli.verify:main" + + [project.optional-dependencies] + # Linting and formatting tools use a more narrow specification to ensure + # developper consistency. All other dependencies are as above. + devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.9.2"] + devel-docs = [ + "mkdocs-literate-nav~=0.6", + "mkdocs-material[imaging]~=9.4", + "mkdocs-section-index~=0.3", + "mkdocs_gen_files~=0.5", + "mkdocstrings[python]~=0.23", + "mkdocs~=1.5", + ] + devel-test = [ + "aiohttp[speedups]~=3.0", + "coverage[toml]~=7.0", + "flask[async]~=3.0", + "httpx~=0.0", + "mock~=5.0", + "pytest-asyncio~=0.0", + "pytest-bdd~=8.0", + "pytest-cov~=6.0", + "pytest-rerunfailures~=15.0", + "pytest-xdist~=3.0", + "pytest~=8.0", + "testcontainers~=4.0", + ] + devel-types = ["mypy==1.14.1", "types-cffi~=1.0", "types-requests~=2.0"] [build-system] +build-backend = "hatchling.build" requires = [ "cffi", "hatch-vcs", @@ -104,141 +99,149 @@ requires = [ "requests", "setuptools ; python_version >= '3.12'", ] -build-backend = "hatchling.build" - -[tool.hatch.version] -source = "vcs" - -[tool.hatch.build.hooks.vcs] -version-file = "src/pact/__version__.py" - -[tool.hatch.build.targets.sdist] -include = [ - # Source - "/src/pact/**/*.py", - "/src/pact/**/*.pyi", - "/src/pact/**/py.typed", - - # Metadata - "*.md", - "LICENSE", -] - -[tool.hatch.build.targets.wheel] -packages = ["/src/pact"] -include = [ - # Source - "/src/pact/**/*.py", - "/src/pact/**/*.pyi", - "/src/pact/**/py.typed", -] -artifacts = [ - "/src/pact/bin/*", # Ruby executables - "/src/pact/lib/*", # Ruby library - "/src/pact/v3/_ffi.*", # Rust library -] - -[tool.hatch.build.targets.wheel.hooks.custom] ################################################################################ -## Hatch Environment Configuration +## Hatch Configuration ################################################################################ - -# Install dev dependencies in the default environment to simplify the developer -# workflow. -[tool.hatch.envs.default] -installer = "uv" -features = ["devel"] -extra-dependencies = [ - "hatchling", - "packaging", - "requests", - "cffi", - "setuptools ; python_version >= '3.12'", -] - -[tool.hatch.envs.default.scripts] -lint = "ruff check --output-format=full --show-fixes {args}" -typecheck = "mypy {args:.}" -format = "ruff format {args}" -test = "pytest tests/ {args}" -example = "pytest --numprocesses=1 examples/ {args}" -all = ["format", "lint", "typecheck", "test", "example"] -docs = "mkdocs serve {args}" -docs-build = "mkdocs build {args}" - -# Test environment for running unit tests. This automatically tests against all -# supported Python versions. -[tool.hatch.envs.test] -installer = "uv" -features = ["devel-test"] - -[[tool.hatch.envs.test.matrix]] -python = ["3.9", "3.10", "3.11", "3.12", "3.13"] +[tool.hatch] + + [tool.hatch.version] + source = "vcs" + + [tool.hatch.build] + + [tool.hatch.build.hooks.vcs] + version-file = "src/pact/__version__.py" + + [tool.hatch.build.targets.sdist] + include = [ + # Source + "/src/pact/**/*.py", + "/src/pact/**/*.pyi", + "/src/pact/**/py.typed", + + # Metadata + "*.md", + "LICENSE", + ] + + [tool.hatch.build.targets.wheel] + artifacts = [ + "/src/pact/bin/*", # Ruby executables + "/src/pact/lib/*", # Ruby library + "/src/pact/v3/_ffi.*", # Rust library + ] + include = [ + # Source + "/src/pact/**/*.py", + "/src/pact/**/*.pyi", + "/src/pact/**/py.typed", + ] + packages = ["/src/pact"] + + [tool.hatch.build.targets.wheel.hooks.custom] + + ######################################## + ## Hatch Environment Configuration + ######################################## + [tool.hatch.envs] + + # Install dev dependencies in the default environment to simplify the developer + # workflow. + [tool.hatch.envs.default] + extra-dependencies = [ + "cffi", + "hatchling", + "packaging", + "requests", + "setuptools ; python_version >= '3.12'", + ] + features = ["devel"] + installer = "uv" + + [tool.hatch.envs.default.scripts] + all = ["example", "format", "lint", "test", "typecheck"] + docs = "mkdocs serve {args}" + docs-build = "mkdocs build {args}" + example = "pytest --numprocesses=1 examples/ {args}" + format = "ruff format {args}" + lint = "ruff check --output-format=full --show-fixes {args}" + test = "pytest tests/ {args}" + typecheck = "mypy {args:.}" + + # Test environment for running unit tests. This automatically tests against all + # supported Python versions. + [tool.hatch.envs.test] + features = ["devel-test"] + installer = "uv" + + [[tool.hatch.envs.test.matrix]] + python = ["3.10", "3.11", "3.12", "3.13", "3.9"] ################################################################################ ## PyTest Configuration ################################################################################ - -[tool.pytest.ini_options] -pythonpath = "." -asyncio_default_fixture_loop_scope = "session" -addopts = [ - "--import-mode=importlib", - # Coverage options - "--cov-config=pyproject.toml", - "--cov=pact", - "--cov-report=xml", - # Xdist options - "--numprocesses=logical", - "--dist=worksteal", - # Rerun options - "--reruns=3", - "--rerun-except=assert", -] -filterwarnings = [ - "ignore::DeprecationWarning:examples", - "ignore::DeprecationWarning:pact", - "ignore::DeprecationWarning:tests", - "ignore::PendingDeprecationWarning:examples", - "ignore::PendingDeprecationWarning:pact", - "ignore::PendingDeprecationWarning:tests", -] - -log_level = "NOTSET" -log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" -log_date_format = "%H:%M:%S" - -markers = [ - # Marker for tests that require a container - "container", - - # Markers for the compatibility suite - "consumer", - "provider", - "message", -] +[tool.pytest] + + [tool.pytest.ini_options] + addopts = [ + "--import-mode=importlib", + # Coverage options + "--cov-config=pyproject.toml", + "--cov-report=xml", + "--cov=pact", + # Xdist options + "--dist=worksteal", + "--numprocesses=logical", + # Rerun options + "--rerun-except=assert", + "--reruns=3", + ] + asyncio_default_fixture_loop_scope = "session" + filterwarnings = [ + "ignore::DeprecationWarning:examples", + "ignore::DeprecationWarning:pact", + "ignore::DeprecationWarning:tests", + "ignore::PendingDeprecationWarning:examples", + "ignore::PendingDeprecationWarning:pact", + "ignore::PendingDeprecationWarning:tests", + ] + pythonpath = "." + + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" + + markers = [ + # Marker for tests that require a container + "container", + + # Markers for the compatibility suite + "consumer", + "message", + "provider", + ] ################################################################################ ## Coverage ################################################################################ +[tool.coverage] -[tool.coverage.paths] -pact = ["/src/pact"] -tests = ["/examples", "/tests"] + [tool.coverage.paths] + pact = ["/src/pact"] + tests = ["/examples", "/tests"] -[tool.coverage.report] -exclude_lines = [ - "if __name__ == .__main__.:", # Ignore non-runnable code - "if TYPE_CHECKING:", # Ignore typing - "raise NotImplementedError", # Ignore defensive assertions - "@(abc\\.)?abstractmethod", # Ignore abstract methods -] + [tool.coverage.report] + exclude_lines = [ + "@(abc\\.)?abstractmethod", # Ignore abstract methods + "if TYPE_CHECKING:", # Ignore typing + "if __name__ == .__main__.:", # Ignore non-runnable code + "raise NotImplementedError", # Ignore defensive assertions + ] ################################################################################ ## Ruff Configuration ################################################################################ - [tool.ruff] # TODO: Remove the explicity extend-exclude once astral-sh/ruff#6262 is fixed. @@ -279,41 +282,40 @@ extend-exclude = [ "tests/test_verify_wrapper.py", ] -[tool.ruff.lint] -select = ["ALL"] + [tool.ruff.lint] + select = ["ALL"] -ignore = [ - "D200", # Require single line docstrings to be on one line. - "D203", # Require blank line before class docstring - "D212", # Multi-line docstring summary must start at the first line - "FIX002", # Forbid TODO in comments - "TD002", # Assign someone to 'TODO' comments + ignore = [ + "D200", # Require single line docstrings to be on one line. + "D203", # Require blank line before class docstring + "D212", # Multi-line docstring summary must start at the first line + "FIX002", # Forbid TODO in comments + "TD002", # Assign someone to 'TODO' comments - # The following are disabled for compatibility with the formatter - "COM812", # enforce trailing commas - "ISC001", # require imports to be sorted -] + # The following are disabled for compatibility with the formatter + "COM812", # enforce trailing commas + "ISC001", # require imports to be sorted + ] -[tool.ruff.lint.pyupgrade] -keep-runtime-typing = true + [tool.ruff.lint.pyupgrade] + keep-runtime-typing = true -[tool.ruff.lint.pydocstyle] -convention = "google" + [tool.ruff.lint.pydocstyle] + convention = "google" -[tool.ruff.lint.isort] -known-first-party = ["pact"] + [tool.ruff.lint.isort] + known-first-party = ["pact"] -[tool.ruff.lint.flake8-tidy-imports] -ban-relative-imports = "all" + [tool.ruff.lint.flake8-tidy-imports] + ban-relative-imports = "all" -[tool.ruff.format] -preview = true -docstring-code-format = true + [tool.ruff.format] + docstring-code-format = true + preview = true ################################################################################ ## Mypy Configuration ################################################################################ - [tool.mypy] exclude = '^(src/pact|tests|examples|examples/tests)/(?!v3).+\.py$' @@ -321,7 +323,6 @@ exclude = '^(src/pact|tests|examples|examples/tests)/(?!v3).+\.py$' ## CI Build Wheel ################################################################################ [tool.cibuildwheel] -skip = "pp*" before-build = """ rm -rvf src/pact/v3/bin rm -rvf src/pact/v3/data @@ -330,6 +331,7 @@ mv -v src/pact/v3/_ffi.pyi _ffi.pyi rm -rvf src/pact/v3/_ffi.* mv -v _ffi.pyi src/pact/v3/_ffi.pyi """ +skip = "pp*" test-command = """ python -c \ "from pact import EachLike; \ @@ -339,33 +341,32 @@ assert \ import pact.v3.ffi; \ assert isinstance(pact.v3.ffi.version(), str);\"""" -[tool.cibuildwheel.macos] -# The repair tool unfortunately did not like the bundled Ruby distributable. -# TODO: Check whether delocate-wheel can be configured. -repair-wheel-command = "" - -[tool.cibuildwheel.windows] -before-build = [ - 'FOR /R src\pact\v3 %G IN (_ffi.*) DO IF NOT %~nxG == _ffi.pyi DEL /F /Q "%G"', - 'IF EXIST src\pact\v3\bin\ RMDIR /S /Q src\pact\v3\bin', - 'IF EXIST src\pact\v3\data\ RMDIR /S /Q src\pact\v3\data', - 'IF EXIST src\pact\v3\lib\ RMDIR /S /Q src\pact\v3\lib', -] + [tool.cibuildwheel.macos] + # The repair tool unfortunately did not like the bundled Ruby distributable. + # TODO: Check whether delocate-wheel can be configured. + repair-wheel-command = "" + + [tool.cibuildwheel.windows] + before-build = [ + 'FOR /R src\pact\v3 %G IN (_ffi.*) DO IF NOT %~nxG == _ffi.pyi DEL /F /Q "%G"', + 'IF EXIST src\pact\v3\bin\ RMDIR /S /Q src\pact\v3\bin', + 'IF EXIST src\pact\v3\data\ RMDIR /S /Q src\pact\v3\data', + 'IF EXIST src\pact\v3\lib\ RMDIR /S /Q src\pact\v3\lib', + ] ################################################################################ ## Typos ################################################################################ +[tool.typos] -[tool.typos.default] -extend-ignore-re = [ - "(?Rm)^.*(#|//| +The function arguments must include the relevant keys from the [`StateHandlerArgs`][pact.v3.types.StateHandlerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments. + This snippet showcases a way to set up the provider state with a function that is fully parameterized. The `state_handler` method also handles the following scenarios: -- If teardowns are never required, then one should specify `teardown=False` in which case the `action` parameter is _not_ passed to the callback function. +- If teardowns are never required, then one should specify `teardown=False` in which case the `action` parameter can be omitted from the signature of the callback function. This is useful when the provider state does not require any cleanup after the test has run. ??? example @@ -135,7 +137,7 @@ This snippet showcases a way to set up the provider state with a function that i def provider_state_callback( name: str, - params: dict[str, Any] | None, + parameters: dict[str, Any] | None, ) -> None: ... @@ -155,13 +157,13 @@ This snippet showcases a way to set up the provider state with a function that i def user_state_callback( action: Literal["setup", "teardown"], - params: dict[str, Any] | None, + parameters: dict[str, Any] | None, ) -> None: ... def no_users_state_callback( action: Literal["setup", "teardown"], - params: dict[str, Any] | None, + parameters: dict[str, Any] | None, ) -> None: ... @@ -176,7 +178,7 @@ This snippet showcases a way to set up the provider state with a function that i ``` -- Both scenarios can be combined, in which a mapping of provide state names to functions is provided, and the `teardown=False` option is specified. In this case, the function should expect only one argument: the `params` dictionary (which itself may be `None`). +- Both scenarios can be combined, in which a mapping of provide state names to functions is provided, and the `teardown=False` option is specified. In this case, the function should expect only one argument: the `parameters` dictionary (which itself may be `None`). ??? example @@ -185,12 +187,12 @@ This snippet showcases a way to set up the provider state with a function that i from pact.v3 import Verifier def user_state_callback( - params: dict[str, Any] | None, + parameters: dict[str, Any] | None, ) -> None: ... def no_users_state_callback( - params: dict[str, Any] | None, + parameters: dict[str, Any] | None, ) -> None: ... @@ -220,7 +222,7 @@ With the update to 2.3.0, the `Verifier` class has a new `message_handler` metho def message_producer_callback( name: str, # (1) - params: dict[str, Any] | None, # (2) + metadata: dict[str, Any] | None, # (2) ) -> Message: """ Callback to produce the message that the consumer expects. @@ -229,10 +231,8 @@ With the update to 2.3.0, the `Verifier` class has a new `message_handler` metho name: The name of the message. For example `"request to delete a user"`. - params: - If the message has additional parameters, they will be passed here. - For example, one could specify the user ID to delete in the - parameters instead of the message. + metadata: + Metadata that is passed along with the message. This could include information about the queue name, message type, creation timestamp, etc. Returns: The message that the consumer expects. @@ -247,6 +247,7 @@ With the update to 2.3.0, the `Verifier` class has a new `message_handler` metho 1. The `name` parameter is the name of the message. For example, `"request to delete a user"`. If you instead use a mapping of message names to functions, this parameter is not passed to the function. 2. The `params` parameter is a dictionary of additional parameters that the message requires. For example, one could specify the user ID to delete in the parameters instead of the message. Note that `params` is always present, but may be `None` if no parameters are specified by the consumer. +The function arguments must include the relevant keys from the [`MessageProducerArgs`][pact.v3.types.MessageProducerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments. The output of the callback function should be an instance of the `Message` type. This is a simple [TypedDict][typing.TypedDict] that represents the message that the consumer expects and can be specified as a simple dictionary, or with typing hints through the `Message` constructor: @@ -289,7 +290,7 @@ The output of the callback function should be an instance of the `Message` type. } ``` -In much the same way as the `state_handler` method, the `message_handler` method can also accept a mapping of message names to functions or raw messages. The function should expect only one argument: the `params` dictionary (which itself may be `None`); or if the message is static, the message can be provided directly: +In much the same way as the `state_handler` method, the `message_handler` method can also accept a mapping of message names to functions or raw messages. The function should expect only one argument: the `metadata` dictionary (which itself may be `None`); or if the message is static, the message can be provided directly: ???+ example @@ -298,7 +299,7 @@ In much the same way as the `state_handler` method, the `message_handler` method from pact.v3 import Verifier from pact.v3.types import Message - def delete_user_message(params: dict[str, Any] | None) -> Message: + def delete_user_message(metadata: dict[str, Any] | None) -> Message: ... def test_provider(): @@ -315,3 +316,12 @@ In much the same way as the `state_handler` method, the `message_handler` method ) ``` + +---- + + +28 March 2025 +: This blog post was updated on 28 March 2025 to reflect changes to the way functional arguments are handled. Instead of requiring positional arguments, Pact Python now inspects the function signature in order to determine whether to pass the arguments as positional or keyword arguments. It will fallback to passing the arguments through variadic arguments (`*args` and `**kwargs`) if present. This was done specific to allow for functions with optional arguments. + + For this added flexibility, the function signatures must have parameters that align with the [`StateHandlerArgs`][pact.v3.types.StateHandlerArgs] and [`MessageProducerArgs`][pact.v3.types.MessageProducerArgs] typed dictionaries. This allows Pact Python to match a `parameters=...` argument with the `parameters` key in the dictionary. Using an alternative name (e.g., `params`) will not work. + From aefeba9d3970b04c45c61d085472f069511d3b43 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 28 Mar 2025 19:36:53 +1100 Subject: [PATCH 0791/1376] docs: rename params -> parameters Thanks to @lotruheawea for spotting some missed renames. Co-authored-by: lotruheawea --- docs/blog/posts/2024/12-30 functional arguments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blog/posts/2024/12-30 functional arguments.md b/docs/blog/posts/2024/12-30 functional arguments.md index fe5bb75e1..3949e24cc 100644 --- a/docs/blog/posts/2024/12-30 functional arguments.md +++ b/docs/blog/posts/2024/12-30 functional arguments.md @@ -120,7 +120,7 @@ The new `state_handler` method replaces the `set_state` method and simplifies th 1. The `name` parameter is the name of the provider state. For example, `"a user with ID 123 exists"` or `"no users exist"`. If you instead use a mapping of provider state names to functions, this parameter is not passed to the function. 2. The `action` parameter is either `"setup"` or `"teardown"`. The setup action should create the provider state, and the teardown action should remove it. If you specify `teardown=False`, then the `action` parameter is _not_ passed to the callback function. - 3. The `params` parameter is a dictionary of additional parameters that the provider state requires. For example, instead of `"a user with ID 123 exists"`, the provider state might be `"a user with the given ID exists"` and the specific ID would be passed in the `params` dictionary. Note that `params` is always present, but may be `None` if no parameters are specified by the consumer. + 3. The `parameters` parameter is a dictionary of additional parameters that the provider state requires. For example, instead of `"a user with ID 123 exists"`, the provider state might be `"a user with the given ID exists"` and the specific ID would be passed in the `parameters` dictionary. Note that `parameters` is always present, but may be `None` if no parameters are specified by the consumer. The function arguments must include the relevant keys from the [`StateHandlerArgs`][pact.v3.types.StateHandlerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments. From 3a87db7baf5301840b177113b71811faa5e3d010 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 1 Apr 2025 11:40:01 +1100 Subject: [PATCH 0792/1376] chore(tests): use consistent return value Instead of combining things into strings or lists, use a consistent `NamedTuple` to capture the mixture of arguments. Signed-off-by: JP-Ellis --- tests/v3/test_util.py | 257 +++++++++++++++++++++++++++++++----------- 1 file changed, 189 insertions(+), 68 deletions(-) diff --git a/tests/v3/test_util.py b/tests/v3/test_util.py index 14d900e56..5a0f86a2a 100644 --- a/tests/v3/test_util.py +++ b/tests/v3/test_util.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, NamedTuple import pytest @@ -46,39 +46,87 @@ def test_convert_python_to_java_datetime_format_with_single_quote() -> None: assert strftime_to_simple_date_format("%Y'%m'%d") == "yyyy''MM''dd" +class Args(NamedTuple): + """ + Named tuple to hold the arguments passed to a function. + """ + + args: dict[str, Any] + kwargs: dict[str, Any] + variadic_args: list[Any] + variadic_kwargs: dict[str, Any] + + def no_annotations(a, b, c, d=b"d"): # noqa: ANN001, ANN201 # type: ignore[reportUnknownArgumentType] - return f"{a}:{b}:{c}:{d!r}" + return Args( + args={"a": a, "b": b, "c": c, "d": d}, + kwargs={}, + variadic_args=[], + variadic_kwargs={}, + ) -def annotated(a: int, b: str, c: float, d: bytes = b"d") -> str: - return f"{a}:{b}:{c}:{d!r}" +def annotated(a: int, b: str, c: float, d: bytes = b"d") -> Args: + return Args( + args={"a": a, "b": b, "c": c, "d": d}, + kwargs={}, + variadic_args=[], + variadic_kwargs={}, + ) -def mixed(a: int, /, b: str, *, c: float, d: bytes = b"d") -> str: - return f"{a}:{b}:{c}:{d!r}" +def mixed(a: int, /, b: str, *, c: float, d: bytes = b"d") -> Args: + return Args( + args={"a": a, "b": b, "c": c, "d": d}, + kwargs={}, + variadic_args=[], + variadic_kwargs={}, + ) -def variadic_args(*args: Any) -> str: # noqa: ANN401 - return ":".join(str(arg) for arg in args) +def variadic_args(*args: Any) -> Args: # noqa: ANN401 + return Args( + args={}, + kwargs={}, + variadic_args=list(args), + variadic_kwargs={}, + ) -def variadic_kwargs(**kwargs: Any) -> str: # noqa: ANN401 - return ":".join(str(v) for v in kwargs.values()) +def variadic_kwargs(**kwargs: Any) -> Args: # noqa: ANN401 + return Args( + args={}, + kwargs=kwargs, + variadic_args=[], + variadic_kwargs={**kwargs}, + ) -def variadic_args_kwargs(*args: Any, **kwargs: Any) -> list[str]: # noqa: ANN401 - return [ - ":".join(str(arg) for arg in args), - ":".join(str(v) for v in kwargs.values()), - ] +def variadic_args_kwargs(*args: Any, **kwargs: Any) -> Args: # noqa: ANN401 + return Args( + args={}, + kwargs=kwargs, + variadic_args=list(args), + variadic_kwargs={**kwargs}, + ) -def mixed_variadic_args(a: int, *args: Any, d: bytes = b"d") -> list[str]: # noqa: ANN401 - return [f"{a}:{d!r}", ":".join(str(arg) for arg in args)] +def mixed_variadic_args(a: int, *args: Any, d: bytes = b"d") -> Args: # noqa: ANN401 + return Args( + args={"a": a, "d": d}, + kwargs={}, + variadic_args=list(args), + variadic_kwargs={}, + ) -def mixed_variadic_kwargs(a: int, d: bytes = b"d", **kwargs: Any) -> list[str]: # noqa: ANN401 - return [f"{a}:{d!r}", ":".join(str(v) for v in kwargs.values())] +def mixed_variadic_kwargs(a: int, d: bytes = b"d", **kwargs: Any) -> Args: # noqa: ANN401 + return Args( + args={"a": a, "d": d}, + kwargs=kwargs, + variadic_args=[], + variadic_kwargs={**kwargs}, + ) def mixed_variadic_args_kwargs( @@ -86,119 +134,192 @@ def mixed_variadic_args_kwargs( *args: Any, # noqa: ANN401 d: bytes = b"d", **kwargs: Any, # noqa: ANN401 -) -> list[str]: - return [ - f"{a}:{d!r}", - ":".join(str(arg) for arg in args), - ":".join(str(v) for v in kwargs.values()), - ] +) -> Args: + return Args( + args={"a": a, "d": d}, + kwargs=kwargs, + variadic_args=list(args), + variadic_kwargs={**kwargs}, + ) class Foo: # noqa: D101 def __init__(self) -> None: # noqa: D107 pass - def __call__(self, a: int, b: str, c: float, d: bytes = b"d") -> str: # noqa: D102 - return f"{a}:{b}:{c}:{d!r}" - - def method(self, a: int, b: str, c: float, d: bytes = b"d") -> str: # noqa: D102 - return f"{a}:{b}:{c}:{d!r}" + def __call__(self, a: int, b: str, c: float, d: bytes = b"d") -> Args: # noqa: D102 + return Args( + args={"a": a, "b": b, "c": c, "d": d}, + kwargs={}, + variadic_args=[], + variadic_kwargs={}, + ) + + def method(self, a: int, b: str, c: float, d: bytes = b"d") -> Args: # noqa: D102 + return Args( + args={"a": a, "b": b, "c": c, "d": d}, + kwargs={}, + variadic_args=[], + variadic_kwargs={}, + ) @classmethod - def class_method(cls, a: int, b: str, c: float, d: bytes = b"d") -> str: # noqa: D102 - return f"{a}:{b}:{c}:{d!r}" + def class_method(cls, a: int, b: str, c: float, d: bytes = b"d") -> Args: # noqa: D102 + return Args( + args={"a": a, "b": b, "c": c, "d": d}, + kwargs={}, + variadic_args=[], + variadic_kwargs={}, + ) @staticmethod - def static_method(a: int, b: str, c: float, d: bytes = b"d") -> str: # noqa: D102 - return f"{a}:{b}:{c}:{d!r}" + def static_method(a: int, b: str, c: float, d: bytes = b"d") -> Args: # noqa: D102 + return Args( + args={"a": a, "b": b, "c": c, "d": d}, + kwargs={}, + variadic_args=[], + variadic_kwargs={}, + ) @pytest.mark.parametrize( ("func", "args", "expected"), [ - (no_annotations, {"a": 1, "b": "b", "c": 3.14}, "1:b:3.14:b'd'"), - (no_annotations, {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, "1:b:3.14:b'e'"), - (annotated, {"a": 1, "b": "b", "c": 3.14}, "1:b:3.14:b'd'"), - (annotated, {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, "1:b:3.14:b'e'"), - (mixed, {"a": 1, "b": "b", "c": 3.14}, "1:b:3.14:b'd'"), - (mixed, {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, "1:b:3.14:b'e'"), - (variadic_args, {"a": 1, "b": "b", "c": 3.14}, "1:b:3.14"), - (variadic_args, {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, "1:b:3.14:b'e'"), - (variadic_kwargs, {"a": 1, "b": "b", "c": 3.14}, "1:b:3.14"), - (variadic_kwargs, {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, "1:b:3.14:b'e'"), - (variadic_args_kwargs, {"a": 1, "b": "b", "c": 3.14}, ["", "1:b:3.14"]), + ( + no_annotations, + {"a": 1, "b": "b", "c": 3.14}, + Args({"a": 1, "b": "b", "c": 3.14, "d": b"d"}, {}, [], {}), + ), + ( + no_annotations, + {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, + Args({"a": 1, "b": "b", "c": 3.14, "d": b"e"}, {}, [], {}), + ), + ( + annotated, + {"a": 1, "b": "b", "c": 3.14}, + Args({"a": 1, "b": "b", "c": 3.14, "d": b"d"}, {}, [], {}), + ), + ( + annotated, + {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, + Args({"a": 1, "b": "b", "c": 3.14, "d": b"e"}, {}, [], {}), + ), + ( + mixed, + {"a": 1, "b": "b", "c": 3.14}, + Args({"a": 1, "b": "b", "c": 3.14, "d": b"d"}, {}, [], {}), + ), + ( + mixed, + {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, + Args({"a": 1, "b": "b", "c": 3.14, "d": b"e"}, {}, [], {}), + ), + ( + variadic_args, + {"a": 1, "b": "b", "c": 3.14}, + Args({}, {}, [1, "b", 3.14], {}), + ), + ( + variadic_args, + {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, + Args({}, {}, [1, "b", 3.14, b"e"], {}), + ), + ( + variadic_kwargs, + {"a": 1, "b": "b", "c": 3.14}, + Args({}, {"a": 1, "b": "b", "c": 3.14}, [], {"a": 1, "b": "b", "c": 3.14}), + ), + ( + variadic_kwargs, + {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, + Args( + {}, + {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, + [], + {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, + ), + ), + ( + variadic_args_kwargs, + {"a": 1, "b": "b", "c": 3.14}, + Args({}, {"a": 1, "b": "b", "c": 3.14}, [], {"a": 1, "b": "b", "c": 3.14}), + ), ( variadic_args_kwargs, {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, - ["", "1:b:3.14:b'e'"], + Args( + {}, + {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, + [], + {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, + ), ), - (mixed_variadic_args, {"a": 1, "b": "b", "c": 3.14}, ["1:b'd'", "b:3.14"]), ( mixed_variadic_args, - {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, - ["1:b'e'", "b:3.14"], + {"a": 1, "b": "b", "c": 3.14}, + Args({"a": 1, "d": b"d"}, {}, ["b", 3.14], {}), ), - (mixed_variadic_kwargs, {"a": 1, "b": "b", "c": 3.14}, ["1:b'd'", "b:3.14"]), ( - mixed_variadic_kwargs, + mixed_variadic_args, {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, - ["1:b'e'", "b:3.14"], + Args({"a": 1, "d": b"e"}, {}, ["b", 3.14], {}), ), ( - mixed_variadic_args_kwargs, + mixed_variadic_kwargs, {"a": 1, "b": "b", "c": 3.14}, - ["1:b'd'", "", "b:3.14"], + Args({"a": 1, "d": b"d"}, {"b": "b", "c": 3.14}, [], {"b": "b", "c": 3.14}), ), ( - mixed_variadic_args_kwargs, + mixed_variadic_kwargs, {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, - ["1:b'e'", "", "b:3.14"], + Args({"a": 1, "d": b"e"}, {"b": "b", "c": 3.14}, [], {"b": "b", "c": 3.14}), ), ( mixed_variadic_args_kwargs, - {"a": 1, "b": "b", "c": 3.14, "e": "f"}, - ["1:b'd'", "", "b:3.14:f"], + {"a": 1, "b": "b", "c": 3.14}, + Args({"a": 1, "d": b"d"}, {"b": "b", "c": 3.14}, [], {"b": "b", "c": 3.14}), ), ( mixed_variadic_args_kwargs, - {"a": 1, "b": "b", "c": 3.14, "e": "f", "d": b"e"}, - ["1:b'e'", "", "b:3.14:f"], + {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, + Args({"a": 1, "d": b"e"}, {"b": "b", "c": 3.14}, [], {"b": "b", "c": 3.14}), ), ( Foo(), {"a": 1, "b": "b", "c": 3.14}, - "1:b:3.14:b'd'", + Args({"a": 1, "b": "b", "c": 3.14, "d": b"d"}, {}, [], {}), ), ( Foo(), {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, - "1:b:3.14:b'e'", + Args({"a": 1, "b": "b", "c": 3.14, "d": b"e"}, {}, [], {}), ), ( Foo().class_method, {"a": 1, "b": "b", "c": 3.14}, - "1:b:3.14:b'd'", + Args({"a": 1, "b": "b", "c": 3.14, "d": b"d"}, {}, [], {}), ), ( Foo().class_method, {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, - "1:b:3.14:b'e'", + Args({"a": 1, "b": "b", "c": 3.14, "d": b"e"}, {}, [], {}), ), ( Foo().static_method, {"a": 1, "b": "b", "c": 3.14}, - "1:b:3.14:b'd'", + Args({"a": 1, "b": "b", "c": 3.14, "d": b"d"}, {}, [], {}), ), ( Foo().static_method, {"a": 1, "b": "b", "c": 3.14, "d": b"e"}, - "1:b:3.14:b'e'", + Args({"a": 1, "b": "b", "c": 3.14, "d": b"e"}, {}, [], {}), ), ], # type: ignore[reportUnknownArgumentType] ) def test_apply_expected( - func: Callable[..., Any], + func: Callable[..., Args], args: dict[str, Any], - expected: str | list[str], + expected: Args, ) -> None: assert apply_args(func, args) == expected From 9787a9607be6d1566307c4b4d0645101cdfacf03 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 1 Apr 2025 11:42:03 +1100 Subject: [PATCH 0793/1376] chore(test): tweak type signature Signed-off-by: JP-Ellis --- tests/v3/compatibility_suite/util/provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index b5d0cc953..7473cc9ef 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -896,7 +896,7 @@ def _( def _callback( state: str, action: str, - parameters: dict[str, str] | None, + parameters: dict[str, Any] | None, ) -> None: pass From 8672fd9a6d2ce946d7dae6ec805f7185b382d688 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 1 Apr 2025 11:48:29 +1100 Subject: [PATCH 0794/1376] chore(examples): fix state handler args Signed-off-by: JP-Ellis --- examples/tests/v3/test_01_fastapi_provider.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/tests/v3/test_01_fastapi_provider.py b/examples/tests/v3/test_01_fastapi_provider.py index 6bdecf580..3484fdabe 100644 --- a/examples/tests/v3/test_01_fastapi_provider.py +++ b/examples/tests/v3/test_01_fastapi_provider.py @@ -146,7 +146,7 @@ def test_provider(server: str) -> None: def provider_state_handler( state: str, action: str, - _parameters: dict[str, Any] | None, + parameters: dict[str, Any] | None = None, # noqa: ARG001 ) -> None: """ Handler for the provider state callback. @@ -178,6 +178,10 @@ def provider_state_handler( state: The name of the state to set up or tear down. + parameters: + A dictionary of parameters to pass to the state handler. This is + not used in this example, but is included for completeness. + Returns: A dictionary containing the result of the action. """ From 773627241fcf3856929e035f5b7d8c7d38080704 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 1 Apr 2025 11:53:30 +1100 Subject: [PATCH 0795/1376] docs(example): elaborate on state handler --- examples/tests/v3/test_01_fastapi_provider.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/tests/v3/test_01_fastapi_provider.py b/examples/tests/v3/test_01_fastapi_provider.py index 3484fdabe..08bf78a38 100644 --- a/examples/tests/v3/test_01_fastapi_provider.py +++ b/examples/tests/v3/test_01_fastapi_provider.py @@ -170,6 +170,11 @@ def provider_state_handler( also be used to reset the mock, or in the case were a real database is used, to clean up any side effects. + This example showcases how a _full_ provider state handler can be + implemented. The handler can also be specified through a mapping of provider + states to functions. See the documentation of the + [`state_handler`][pact.v3.Verifier.state_handler] method for more details. + Args: action: One of `setup` or `teardown`. Determines whether the provider state From 090576abb6924f3eb16405a4a2420394cf857028 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:08:14 +1000 Subject: [PATCH 0796/1376] fix(deps): update ruff to v0.11.5 (#1043) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5bb751dbb..acf69403e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.4 + rev: v0.11.5 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 4f31d232e..3e7ff7a6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ dependencies = [ [project.optional-dependencies] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. - devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.4"] + devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.5"] devel-docs = [ "mkdocs-literate-nav~=0.6", "mkdocs-material[imaging]~=9.4", From 2e2a0861c57618c316ae50d345d603a9c9071783 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:08:22 +1000 Subject: [PATCH 0797/1376] chore(deps): update pactfoundation/pact-broker:latest docker digest to b1cdf96 (#1042) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 046dd4811..6aae5a0fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,7 +54,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:83f738a8de09903f93a35dea51461ff7b21547b67c0e15564a0c59922fd7ed7e + image: pactfoundation/pact-broker:latest@sha256:b1cdf96a80dba0fb655dd325bb0f99e715f62b33463c3bc9612a793287af85d3 ports: - 9292:9292 env: @@ -187,7 +187,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:83f738a8de09903f93a35dea51461ff7b21547b67c0e15564a0c59922fd7ed7e + image: pactfoundation/pact-broker:latest@sha256:b1cdf96a80dba0fb655dd325bb0f99e715f62b33463c3bc9612a793287af85d3 ports: - 9292:9292 env: From 6eff39bbf6cfaaccda965391c5cf0185b47bcc10 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 15:44:16 +1000 Subject: [PATCH 0798/1376] chore(deps): update taiki-e/install-action action to v2.49.49 (#1044) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3ca5024c0..7c27d8d5c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2.49.39 + uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2.49.49 with: tool: git-cliff,typos From dfd7d505e700f3fbdcee5b3c028acf9554b759ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 00:04:10 +0000 Subject: [PATCH 0799/1376] chore(deps): update codecov/codecov-action action to v5.4.2 (#1045) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6aae5a0fb..06d3dc756 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -120,7 +120,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0 + uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests @@ -232,7 +232,7 @@ jobs: hatch run example --broker-url=http://pactbroker:pactbroker@localhost:9292 - name: Upload coverage - uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0 + uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} flags: examples From 76de7fb0addaac2050528a6254d1c7d2b6130db5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Apr 2025 09:57:47 +1000 Subject: [PATCH 0800/1376] chore(deps): update astral-sh/setup-uv action to v5.4.2 (#1046) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c27d8d5c..5b5398f8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 72df48499..b8339be64 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 06d3dc756..10f27f717 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,7 +90,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: enable-cache: true cache-dependency-glob: | @@ -164,7 +164,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: enable-cache: true cache-dependency-glob: | @@ -202,7 +202,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: enable-cache: true cache-dependency-glob: | @@ -252,7 +252,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: enable-cache: true cache-dependency-glob: | @@ -277,7 +277,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: enable-cache: true cache-dependency-glob: | @@ -302,7 +302,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: enable-cache: true cache-dependency-glob: | @@ -350,7 +350,7 @@ jobs: ${{ runner.os }}-pre-commit- - name: Set up uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: enable-cache: true cache-suffix: pre-commit From 6d520ea13ba5179a969f870bfbe553254d2fbc65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 08:09:38 +1000 Subject: [PATCH 0801/1376] fix(deps): update ruff to v0.11.6 (#1047) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index acf69403e..241ac991f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.5 + rev: v0.11.6 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 3e7ff7a6f..c0af2e724 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ dependencies = [ [project.optional-dependencies] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. - devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.5"] + devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.6"] devel-docs = [ "mkdocs-literate-nav~=0.6", "mkdocs-material[imaging]~=9.4", From e1031e9c340e43e481c772787f6532b0f533b676 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 19 Apr 2025 18:52:17 +1000 Subject: [PATCH 0802/1376] chore(deps): update softprops/action-gh-release action to v2.2.2 (#1048) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b5398f8a..461e0eee1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -261,7 +261,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 + uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 with: files: wheels/* body_path: ${{ runner.temp }}/release-changelog.md From ecdaaedbad317c561c183e0e5dc30f68c878ed1b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:06:41 +1000 Subject: [PATCH 0803/1376] chore(deps): update taiki-e/install-action action to v2.49.50 (#1049) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 461e0eee1..cbb7f62a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2.49.49 + uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2.49.50 with: tool: git-cliff,typos From 1cfe8a09db057ebefc137af33735d7f4c84eb416 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 06:45:04 +1000 Subject: [PATCH 0804/1376] chore(deps): update actions/download-artifact action to v4.3.0 (#1052) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cbb7f62a4..2ece770ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -237,7 +237,7 @@ jobs: tool: git-cliff,typos - name: Download wheels and sdist - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: wheels merge-multiple: true From 4bb1d2bd8c64f43ccbd3292d439a7a2442570ef9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 06:45:19 +1000 Subject: [PATCH 0805/1376] fix(deps): update ruff to v0.11.7 (#1051) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 241ac991f..403a8ed5e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.6 + rev: v0.11.7 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index c0af2e724..d61794670 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ dependencies = [ [project.optional-dependencies] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. - devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.6"] + devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.7"] devel-docs = [ "mkdocs-literate-nav~=0.6", "mkdocs-material[imaging]~=9.4", From 3501e0825655d63723130af6a5741e7cb26109aa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 06:48:49 +1000 Subject: [PATCH 0806/1376] chore(deps): update astral-sh/setup-uv action to v6 (#1050) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ece770ae..041a6673f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b8339be64..41a0ee900 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 10f27f717..4238f0110 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,7 +90,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 with: enable-cache: true cache-dependency-glob: | @@ -164,7 +164,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 with: enable-cache: true cache-dependency-glob: | @@ -202,7 +202,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 with: enable-cache: true cache-dependency-glob: | @@ -252,7 +252,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 with: enable-cache: true cache-dependency-glob: | @@ -277,7 +277,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 with: enable-cache: true cache-dependency-glob: | @@ -302,7 +302,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 with: enable-cache: true cache-dependency-glob: | @@ -350,7 +350,7 @@ jobs: ${{ runner.os }}-pre-commit- - name: Set up uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 with: enable-cache: true cache-suffix: pre-commit From 504804d9cae18643fe49db640eab48170c353917 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 27 Apr 2025 08:11:43 +1000 Subject: [PATCH 0807/1376] chore(deps): update pypa/cibuildwheel action to v2.23.3 (#1053) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 041a6673f..0a1b009de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,7 +126,7 @@ jobs: echo "MACOSX_DEPLOYMENT_TARGET=10.12" >> "$GITHUB_ENV" - name: Create wheels - uses: pypa/cibuildwheel@d04cacbc9866d432033b1d09142936e6a0e2121a # v2.23.2 + uses: pypa/cibuildwheel@faf86a6ed7efa889faf6996aa23820831055001a # v2.23.3 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} @@ -189,7 +189,7 @@ jobs: platforms: arm64 - name: Create wheels - uses: pypa/cibuildwheel@d04cacbc9866d432033b1d09142936e6a0e2121a # v2.23.2 + uses: pypa/cibuildwheel@faf86a6ed7efa889faf6996aa23820831055001a # v2.23.3 env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} From 7cd007c7e1d180ae85889c8a001636aa22a5e396 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:51:59 +1000 Subject: [PATCH 0808/1376] chore(deps): update taiki-e/install-action action to v2.50.3 (#1054) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0a1b009de..cd169d2fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2.49.50 + uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2.50.3 with: tool: git-cliff,typos From a4e334e616340dca7cfe07727129a3e959fb6546 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 00:49:25 +0000 Subject: [PATCH 0809/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.31.2 (#1055) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4238f0110..12fbc378a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -327,7 +327,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@b1a1ef3893ff35ade0cfa71523852a49bfd05d19 # v1.31.1 + uses: crate-ci/typos@3be83342e28b9421997e9f781f713f8dde8453d2 # v1.31.2 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 403a8ed5e..daa26c722 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,7 +76,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.31.1 + rev: v1.31.2 hooks: - id: typos From 3720e2ca04c0317684629b0f24b932f8a12db88f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:25:11 +1000 Subject: [PATCH 0810/1376] chore(deps): update astral-sh/setup-uv action to v6.0.1 (#1056) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd169d2fd..1aaf2f7a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 + uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 41a0ee900..2aab55111 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 + uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12fbc378a..92dcac264 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,7 +90,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 + uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true cache-dependency-glob: | @@ -164,7 +164,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 + uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true cache-dependency-glob: | @@ -202,7 +202,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 + uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true cache-dependency-glob: | @@ -252,7 +252,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 + uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true cache-dependency-glob: | @@ -277,7 +277,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 + uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true cache-dependency-glob: | @@ -302,7 +302,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 + uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true cache-dependency-glob: | @@ -350,7 +350,7 @@ jobs: ${{ runner.os }}-pre-commit- - name: Set up uv - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 + uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true cache-suffix: pre-commit From 341ecf6f2c87358c598ba2f71a695262314c999a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 09:28:55 +1000 Subject: [PATCH 0811/1376] fix(deps): update ruff to v0.11.8 (#1057) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index daa26c722..cc2de6b1b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.7 + rev: v0.11.8 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index d61794670..f9c779e30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ dependencies = [ [project.optional-dependencies] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. - devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.7"] + devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.8"] devel-docs = [ "mkdocs-literate-nav~=0.6", "mkdocs-material[imaging]~=9.4", From a60a09d24deac8a36f9cccbcaae4d891a1f0b7ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 09:29:11 +1000 Subject: [PATCH 0812/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.32.0 (#1058) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 92dcac264..527878aa0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -327,7 +327,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@3be83342e28b9421997e9f781f713f8dde8453d2 # v1.31.2 + uses: crate-ci/typos@0f0ccba9ed1df83948f0c15026e4f5ccfce46109 # v1.32.0 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc2de6b1b..ab06ad323 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,7 +76,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.31.2 + rev: v1.32.0 hooks: - id: typos From 297b7881419ab31565c3696e22e7658e9e86ba3f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 03:08:07 +0000 Subject: [PATCH 0813/1376] chore(deps): update taiki-e/install-action action to v2.50.7 (#1059) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1aaf2f7a0..452d1e628 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2.50.3 + uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2.50.7 with: tool: git-cliff,typos From 17876ccedecbab37d1c8756f767f120a5c35710e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:02:50 +1000 Subject: [PATCH 0814/1376] chore(deps): update codecov/codecov-action action to v5.4.3 (#1062) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 527878aa0..7be1d9e62 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -120,7 +120,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests @@ -232,7 +232,7 @@ jobs: hatch run example --broker-url=http://pactbroker:pactbroker@localhost:9292 - name: Upload coverage - uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} flags: examples From 9aa596f5f1f7492e96b2d7477d2da24b9052488f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:11:09 +1000 Subject: [PATCH 0815/1376] chore(deps): update codecov/test-results-action action to v1.1.1 (#1064) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7be1d9e62..c22f390d3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -127,7 +127,7 @@ jobs: - name: Upload test results if: ${{ !cancelled() }} - uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0 + uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -239,7 +239,7 @@ jobs: - name: Upload test results if: ${{ !cancelled() }} - uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0 + uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} From 7bca2362d3011128fa0d3cf641ed47b3f78618a2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:11:22 +1000 Subject: [PATCH 0816/1376] chore(deps): update astral-sh/setup-uv action to v6.1.0 (#1065) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 452d1e628..5f3d118e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2aab55111..9c1092907 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c22f390d3..d6261e053 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,7 +90,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 with: enable-cache: true cache-dependency-glob: | @@ -164,7 +164,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 with: enable-cache: true cache-dependency-glob: | @@ -202,7 +202,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 with: enable-cache: true cache-dependency-glob: | @@ -252,7 +252,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 with: enable-cache: true cache-dependency-glob: | @@ -277,7 +277,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 with: enable-cache: true cache-dependency-glob: | @@ -302,7 +302,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 with: enable-cache: true cache-dependency-glob: | @@ -350,7 +350,7 @@ jobs: ${{ runner.os }}-pre-commit- - name: Set up uv - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 with: enable-cache: true cache-suffix: pre-commit From 000f66128a8eda017efbe27c0df7028b961d3139 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:11:32 +1000 Subject: [PATCH 0817/1376] fix(deps): update dependency pytest-asyncio to v1 (#1066) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f9c779e30..d867e6388 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ dependencies = [ "flask[async]~=3.0", "httpx~=0.0", "mock~=5.0", - "pytest-asyncio~=0.0", + "pytest-asyncio~=1.0", "pytest-bdd~=8.0", "pytest-cov~=6.0", "pytest-rerunfailures~=15.0", From 75338375c0a8bbde9766547ce8b4a13b24d2dd15 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:11:55 +1000 Subject: [PATCH 0818/1376] fix(deps): update ruff to v0.11.12 (#1060) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ab06ad323..0d872e538 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.8 + rev: v0.11.12 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index d867e6388..100252de9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ dependencies = [ [project.optional-dependencies] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. - devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.8"] + devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.12"] devel-docs = [ "mkdocs-literate-nav~=0.6", "mkdocs-material[imaging]~=9.4", From 872883a63c2d9d3d2e30be8197919d8718a9a574 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:12:07 +1000 Subject: [PATCH 0819/1376] fix(deps): update dependency mypy to v1.16.0 (#1067) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 100252de9..df1cafb4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ dependencies = [ "pytest~=8.0", "testcontainers~=4.0", ] - devel-types = ["mypy==1.15.0", "types-cffi~=1.0", "types-requests~=2.0"] + devel-types = ["mypy==1.16.0", "types-cffi~=1.0", "types-requests~=2.0"] [build-system] build-backend = "hatchling.build" From 6b3e50b66cf765184e6ac3404d972c67d10c60d3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:12:24 +1000 Subject: [PATCH 0820/1376] chore(deps): update taiki-e/install-action action to v2.52.4 (#1061) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f3d118e4..c334b4112 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2.50.7 + uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 with: tool: git-cliff,typos From 1fb7309efb0578b5d2af5d6bdcdd7814de920e3a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:51:19 +1000 Subject: [PATCH 0821/1376] chore(deps): update pre-commit hook igorshubovych/markdownlint-cli to v0.45.0 (#1063) Signed-off-by: JP-Ellis Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- README.md | 2 +- .../2024/05-02 integrating rust ffi with pact python.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d872e538..195cd4703 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: committed - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.44.0 + rev: v0.45.0 hooks: - id: markdownlint exclude: | diff --git a/README.md b/README.md index efebabdc1..5ba0a2a63 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ This readme provides a high-level overview of the Pact Python library. For detai - [Provider testing](docs/provider.md) - [Examples](examples/README.md) -Documentation for the API is generated from the docstrings in the code which you can view [here](https://pact-foundation.github.io/pact-python/pact). Please be aware that only the [`pact.v3` module][pact.v3] is thoroughly documented at this time. +Documentation for the API is generated from the docstrings in the code which you can view at [`pact-foundation.github.io/pact-python/pact`](https://pact-foundation.github.io/pact-python/pact). Please be aware that only the [`pact.v3` module][pact.v3] is thoroughly documented at this time. ### Need Help diff --git a/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md b/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md index 54515dad3..8289964be 100644 --- a/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md +++ b/docs/blog/posts/2024/05-02 integrating rust ffi with pact python.md @@ -19,12 +19,12 @@ In this blog post, I will delve into how this is all achieved. From explaining h Python, known for its dynamic typing and automated memory management, is fundamentally an interpreted language. Despite not having innate capabilities to directly interact with binary libraries, most Python interpreters bridge this gap efficiently. For instance, CPython—the principal interpreter—enables the creation of binary extensions[^binary_extension] and similarly, PyPy—a widely-used alternative—offers comparable functionalities[^pypy]. -[^binary_extension]: You can find extensive documentation on building extensions for CPython [here](https://docs.python.org/3/extending/extending.html). -[^pypy]: PyPy extension-building documentation is available [here](https://doc.pypy.org/en/latest/extending.html). +[^binary_extension]: You can find extensive documentation on building extensions for CPython [in the official documentation](https://docs.python.org/3/extending/extending.html). +[^pypy]: Refer to the [PyPy extension-building documentation](https://doc.pypy.org/en/latest/extending.html). However, each interpreter has a distinct API tailored for crafting these binary extensions, which unfortunately leads to a lack of universal solutions across different environments. Furthermore, interpreters like [Jython](https://jython.org) and [Pyodide](https://pyodide.org/en/stable/), which are based on Java and WebAssembly respectively, present unique challenges that often preclude the straightforward use of such extensions due to their distinct runtime architectures.[^pyodide] -[^pyodide]: It would appear that Pyodide can support C extensions as explained [here](https://pyodide.org/en/stable/development/new-packages.html), though by and large Pyodide appears to be intended for pure Python packages. +[^pyodide]: It would appear that Pyodide [can support C extensions](https://pyodide.org/en/stable/development/new-packages.html), though by and large Pyodide appears to be intended for pure Python packages. While it is possible for the extension to contain all the logic, our specific requirement is merely to provide a bridge between Python and the Rust core library. This is the niche that [Python C Foreign Function Interface (CFFI)](https://cffi.readthedocs.io/en/stable/) fills. By parsing a C header file, CFFI automates the generation of extension code needed for Python to interface with the binary library. Consequently, this library can be imported into Python as if it were any standard module—streamlining development and potentially improving performance by leveraging optimized native code. From 60e22d8f3345ff20d5340bd140d73c26f7fe6b68 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:10:34 +1000 Subject: [PATCH 0822/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.33.1 (#1068) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6261e053..5eebf7355 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -327,7 +327,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@0f0ccba9ed1df83948f0c15026e4f5ccfce46109 # v1.32.0 + uses: crate-ci/typos@b1ae8d918b6e85bd611117d3d9a3be4f903ee5e4 # v1.33.1 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 195cd4703..71337236c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,7 +76,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.32.0 + rev: v1.33.1 hooks: - id: typos From 1d39a43ec5823c7fd9217cd6e44e3064c4b4c914 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 5 Jun 2025 15:09:59 +1000 Subject: [PATCH 0823/1376] chore(ci): remove pre-commit cache restore key Pre-commit does not clean up any files, resulting in the cache bloating indefinitely if the cache is restored from a previous key. With this change, the pre-commit cache is cleared whenever the pre-commit config is updated. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5eebf7355..bf2a530ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -346,8 +346,6 @@ jobs: path: | ${{ env.PRE_COMMIT_HOME }} key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: | - ${{ runner.os }}-pre-commit- - name: Set up uv uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 From 0f3471e70289554b93538e1fb2607e52f1c95669 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Jun 2025 10:54:22 +1000 Subject: [PATCH 0824/1376] fix(deps): update ruff to v0.11.13 (#1072) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 71337236c..733d4b71f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.12 + rev: v0.11.13 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index df1cafb4b..f345a3631 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ dependencies = [ [project.optional-dependencies] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. - devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.12"] + devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.13"] devel-docs = [ "mkdocs-literate-nav~=0.6", "mkdocs-material[imaging]~=9.4", From da9779c7b14abd920338c49b7c136faf3a1cd1cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Jun 2025 20:36:49 +1000 Subject: [PATCH 0825/1376] chore(deps): update pactfoundation/pact-broker:latest docker digest to 1cbd614 (#1071) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf2a530ee..6871aac01 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,7 +54,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:b1cdf96a80dba0fb655dd325bb0f99e715f62b33463c3bc9612a793287af85d3 + image: pactfoundation/pact-broker:latest@sha256:1cbd6145671095fba7b4d865f5bce8bc6db1248f32f9945fb2101e7d8d3ff15a ports: - 9292:9292 env: @@ -187,7 +187,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:b1cdf96a80dba0fb655dd325bb0f99e715f62b33463c3bc9612a793287af85d3 + image: pactfoundation/pact-broker:latest@sha256:1cbd6145671095fba7b4d865f5bce8bc6db1248f32f9945fb2101e7d8d3ff15a ports: - 9292:9292 env: From 4113521aec496ad5f77dab08dfde92ef0ce180dc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Jun 2025 09:10:55 +1000 Subject: [PATCH 0826/1376] chore(deps): update taiki-e/install-action action to v2.52.7 (#1073) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c334b4112..5b423911f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 + uses: taiki-e/install-action@92f69c195229fe62d58b4d697ab4bc75def98e76 # v2.52.7 with: tool: git-cliff,typos From 9676c2fef4160aae4e11a7485fdb09c27a09047d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 20:18:02 +1000 Subject: [PATCH 0827/1376] chore(deps): update softprops/action-gh-release action to v2.3.2 (#1074) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b423911f..60a4ad91c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -261,7 +261,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 with: files: wheels/* body_path: ${{ runner.temp }}/release-changelog.md From b52a7f6b477fc9db9ef1008d5809bcc20c9c4b32 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 12:26:38 +1000 Subject: [PATCH 0828/1376] chore(deps): update tests/v3/compatibility_suite/definition digest to 1acfa1e (#975) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- tests/v3/compatibility_suite/definition | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/v3/compatibility_suite/definition b/tests/v3/compatibility_suite/definition index cc76eac3c..1acfa1ecb 160000 --- a/tests/v3/compatibility_suite/definition +++ b/tests/v3/compatibility_suite/definition @@ -1 +1 @@ -Subproject commit cc76eac3ca649e863c9d28aad572605922545759 +Subproject commit 1acfa1ecbd9d63e4465c687b3cdd7e0d3ac5811c From 036967e6a53204d7f8b54031534522734b075157 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:28:36 +1000 Subject: [PATCH 0829/1376] chore(deps): update taiki-e/install-action action to v2.52.8 (#1076) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60a4ad91c..0bac347b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@92f69c195229fe62d58b4d697ab4bc75def98e76 # v2.52.7 + uses: taiki-e/install-action@7b20dfd705618832f20d29066e34aa2f2f6194c2 # v2.52.8 with: tool: git-cliff,typos From f0caff0cc15fa2c282bf12198e070c5c20fd8e0d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 12:35:59 +1000 Subject: [PATCH 0830/1376] fix(deps): update dependency mypy to v1.16.1 (#1077) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f345a3631..038cadd74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ dependencies = [ "pytest~=8.0", "testcontainers~=4.0", ] - devel-types = ["mypy==1.16.0", "types-cffi~=1.0", "types-requests~=2.0"] + devel-types = ["mypy==1.16.1", "types-cffi~=1.0", "types-requests~=2.0"] [build-system] build-backend = "hatchling.build" From b5a2481e19dae602c8bac1369fef6dc1765416f6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:20:59 +1000 Subject: [PATCH 0831/1376] fix(deps): update ruff to v0.12.0 (#1078) Co-authored-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- examples/tests/test_01_provider_fastapi.py | 9 +-------- examples/tests/test_01_provider_flask.py | 9 +-------- examples/tests/v3/conftest.py | 4 ++-- examples/tests/v3/test_01_fastapi_provider.py | 17 +---------------- pyproject.toml | 2 +- src/pact/v3/ffi.py | 15 +++++++++++---- tests/v3/conftest.py | 4 ++-- 8 files changed, 20 insertions(+), 42 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 733d4b71f..19daf28c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.13 + rev: v0.12.0 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/tests/test_01_provider_fastapi.py index 3fab5d9c3..7855d342b 100644 --- a/examples/tests/test_01_provider_fastapi.py +++ b/examples/tests/test_01_provider_fastapi.py @@ -35,6 +35,7 @@ from pydantic import BaseModel from yarl import URL +import examples.src.fastapi from examples.src.fastapi import User, app from pact import Verifier # type: ignore[import-untyped] @@ -108,8 +109,6 @@ def verifier() -> Generator[Verifier, Any, None]: def mock_user_123_doesnt_exist() -> None: """Mock the database for the user 123 doesn't exist state.""" - import examples.src.fastapi - examples.src.fastapi.FAKE_DB = MagicMock() examples.src.fastapi.FAKE_DB.get.return_value = None @@ -128,8 +127,6 @@ def mock_user_123_exists() -> None: and removing fields) without fear of breaking the interactions with the consumers. """ - import examples.src.fastapi - mock_db = MagicMock() mock_db.get.return_value = User( id=123, @@ -147,8 +144,6 @@ def mock_post_request_to_create_user() -> None: """ Mock the database for the post request to create a user. """ - import examples.src.fastapi - local_db: dict[int, User] = {} def local_setitem(key: int, value: User) -> None: @@ -168,8 +163,6 @@ def mock_delete_request_to_delete_user() -> None: """ Mock the database for the delete request to delete a user. """ - import examples.src.fastapi - local_db = { 123: User( id=123, diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py index 347d57e7f..49e719f28 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/tests/test_01_provider_flask.py @@ -33,6 +33,7 @@ import pytest from yarl import URL +import examples.src.flask from examples.src.flask import User, app from flask import request from pact import Verifier # type: ignore[import-untyped] @@ -102,8 +103,6 @@ def verifier() -> Generator[Verifier, Any, None]: def mock_user_123_doesnt_exist() -> None: """Mock the database for the user 123 doesn't exist state.""" - import examples.src.flask - examples.src.flask.FAKE_DB = MagicMock() examples.src.flask.FAKE_DB.get.return_value = None @@ -122,8 +121,6 @@ def mock_user_123_exists() -> None: and removing fields) without fear of breaking the interactions with the consumers. """ - import examples.src.flask - examples.src.flask.FAKE_DB = MagicMock() examples.src.flask.FAKE_DB.get.return_value = User( id=123, @@ -140,8 +137,6 @@ def mock_post_request_to_create_user() -> None: """ Mock the database for the post request to create a user. """ - import examples.src.flask - local_db: dict[int, User] = {} def local_setitem(key: int, value: User) -> None: @@ -161,8 +156,6 @@ def mock_delete_request_to_delete_user() -> None: """ Mock the database for the delete request to delete a user. """ - import examples.src.flask - local_db = { 123: User( id=123, diff --git a/examples/tests/v3/conftest.py b/examples/tests/v3/conftest.py index 485ff82bb..e961c9b43 100644 --- a/examples/tests/v3/conftest.py +++ b/examples/tests/v3/conftest.py @@ -4,12 +4,12 @@ import pytest +from pact.v3 import ffi + @pytest.fixture(scope="session", autouse=True) def _setup_pact_logging() -> None: """ Set up logging for the pact package. """ - from pact.v3 import ffi - ffi.log_to_stderr("INFO") diff --git a/examples/tests/v3/test_01_fastapi_provider.py b/examples/tests/v3/test_01_fastapi_provider.py index 08bf78a38..e8f4fc59e 100644 --- a/examples/tests/v3/test_01_fastapi_provider.py +++ b/examples/tests/v3/test_01_fastapi_provider.py @@ -37,6 +37,7 @@ import uvicorn from yarl import URL +import examples.src.fastapi from examples.src.fastapi import User from pact.v3 import Verifier @@ -211,8 +212,6 @@ def mock_user_doesnt_exist() -> None: """ Mock the database for the user doesn't exist state. """ - import examples.src.fastapi - mock_db = MagicMock() mock_db.get.return_value = None examples.src.fastapi.FAKE_DB = mock_db @@ -232,8 +231,6 @@ def mock_user_exists() -> None: and removing fields) without fear of breaking the interactions with the consumers. """ - import examples.src.fastapi - mock_db = MagicMock() mock_db.get.return_value = User( id=123, @@ -263,8 +260,6 @@ def mock_post_request_to_create_user() -> None: also be used to reset the mock, or in the case were a real database is used, to clean up any side effects. """ - import examples.src.fastapi - local_db: dict[int, User] = {} def local_setitem(key: int, value: User) -> None: @@ -288,8 +283,6 @@ def mock_delete_request_to_delete_user() -> None: local dictionary to avoid side effects. This function replaces the calls to the database with a local dictionary to avoid side effects. """ - import examples.src.fastapi - local_db = { 123: User( id=123, @@ -331,8 +324,6 @@ def verify_user_doesnt_exist_mock() -> None: that it returned `None`, and ensures that it was called with an integer argument. It then resets the mock for future tests. """ - import examples.src.fastapi - if TYPE_CHECKING: # During setup, the `FAKE_DB` is replaced with a MagicMock object. # We need to inform the type checker that this has happened. @@ -357,8 +348,6 @@ def verify_user_exists_mock() -> None: that it returned the expected user data, and ensures that it was called with the integer argument `1`. It then resets the mock for future tests. """ - import examples.src.fastapi - if TYPE_CHECKING: examples.src.fastapi.FAKE_DB = MagicMock() @@ -374,8 +363,6 @@ def verify_user_exists_mock() -> None: def verify_mock_post_request_to_create_user() -> None: - import examples.src.fastapi - if TYPE_CHECKING: examples.src.fastapi.FAKE_DB = MagicMock() @@ -396,8 +383,6 @@ def verify_mock_post_request_to_create_user() -> None: def verify_mock_delete_request_to_delete_user() -> None: - import examples.src.fastapi - if TYPE_CHECKING: examples.src.fastapi.FAKE_DB = MagicMock() diff --git a/pyproject.toml b/pyproject.toml index 038cadd74..67c274a7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ dependencies = [ [project.optional-dependencies] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. - devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.11.13"] + devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.12.0"] devel-docs = [ "mkdocs-literate-nav~=0.6", "mkdocs-material[imaging]~=9.4", diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 6993f684a..6266524c3 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -87,6 +87,7 @@ from __future__ import annotations import gc +import inspect import json import logging import typing @@ -94,6 +95,7 @@ from enum import Enum from typing import TYPE_CHECKING, Any, Literal +from pact import __version__ from pact.v3._ffi import ffi, lib # type: ignore[import] if TYPE_CHECKING: @@ -1880,6 +1882,15 @@ def __eq__(self, other: object) -> bool: return self._string == other return super().__eq__(other) + def __hash__(self) -> int: + """ + Hash the Owned String. + + Returns: + The hash of the Owned String. + """ + return hash(self._string) + def version() -> str: """ @@ -1977,8 +1988,6 @@ def log_message( if isinstance(log_level, str): log_level = LevelFilter[log_level.upper()] if source is None: - import inspect - source = inspect.stack()[1].function lib.pactffi_log_message( source.encode("utf-8"), @@ -6750,8 +6759,6 @@ def verifier_new_for_application() -> VerifierHandle: [Rust `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_new_for_application) """ - from pact import __version__ - result: cffi.FFI.CData = lib.pactffi_verifier_new_for_application( b"pact-python", __version__.encode("utf-8"), diff --git a/tests/v3/conftest.py b/tests/v3/conftest.py index b75fc4792..4ee15b215 100644 --- a/tests/v3/conftest.py +++ b/tests/v3/conftest.py @@ -7,12 +7,12 @@ import pytest +from pact.v3 import ffi + @pytest.fixture(scope="session", autouse=True) def _setup_pact_logging() -> None: """ Set up logging for the pact package. """ - from pact.v3 import ffi - ffi.log_to_stderr("INFO") From dd18a131f6c3a2abd140236967bda7dd6b333576 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:07:31 +1000 Subject: [PATCH 0832/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2 (#1079) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19daf28c7..434a59b3d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v1.9.4 + rev: v2.0.0 hooks: - id: biome-check additional_dependencies: From a041ed32c3ba64905d53e9666f4e7d22c823aa78 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 09:20:36 +1000 Subject: [PATCH 0833/1376] chore(deps): update astral-sh/setup-uv action to v6.2.1 (#1081) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0bac347b5..c9282910b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9c1092907..a8e8bae2f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6871aac01..f127d4430 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,7 +90,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 with: enable-cache: true cache-dependency-glob: | @@ -164,7 +164,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 with: enable-cache: true cache-dependency-glob: | @@ -202,7 +202,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 with: enable-cache: true cache-dependency-glob: | @@ -252,7 +252,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 with: enable-cache: true cache-dependency-glob: | @@ -277,7 +277,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 with: enable-cache: true cache-dependency-glob: | @@ -302,7 +302,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 with: enable-cache: true cache-dependency-glob: | @@ -348,7 +348,7 @@ jobs: key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Set up uv - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 with: enable-cache: true cache-suffix: pre-commit From f6adbf61ed151cd85043300a0e174406f90d5ca0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:48:04 +1000 Subject: [PATCH 0834/1376] chore(deps): update astral-sh/setup-uv action to v6.3.0 (#1083) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c9282910b..a65ce2889 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 + uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a8e8bae2f..b59d00fde 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 + uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f127d4430..f11861392 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,7 +90,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 + uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 with: enable-cache: true cache-dependency-glob: | @@ -164,7 +164,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 + uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 with: enable-cache: true cache-dependency-glob: | @@ -202,7 +202,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 + uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 with: enable-cache: true cache-dependency-glob: | @@ -252,7 +252,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 + uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 with: enable-cache: true cache-dependency-glob: | @@ -277,7 +277,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 + uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 with: enable-cache: true cache-dependency-glob: | @@ -302,7 +302,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 + uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 with: enable-cache: true cache-dependency-glob: | @@ -348,7 +348,7 @@ jobs: key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Set up uv - uses: astral-sh/setup-uv@a02a550bdd3185dba2ebb6aa98d77047ce54ad21 # v6.2.1 + uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 with: enable-cache: true cache-suffix: pre-commit From 7c6c2f68226d71457a2686f5bd3981f8f370d776 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:06:41 +1000 Subject: [PATCH 0835/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.0.2 (#1085) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 434a59b3d..2e382971e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.0.0 + rev: v2.0.2 hooks: - id: biome-check additional_dependencies: From 3cc7d43feadb95cfb76506985a7e900ac19d6e02 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 13:22:40 +1000 Subject: [PATCH 0836/1376] chore(deps): update taiki-e/install-action action to v2.54.0 (#1086) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a65ce2889..814be5db4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@7b20dfd705618832f20d29066e34aa2f2f6194c2 # v2.52.8 + uses: taiki-e/install-action@9ba3ac3fd006a70c6e186a683577abc1ccf0ff3a # v2.54.0 with: tool: git-cliff,typos From 6e80190def0fcb72caa4f3f9efea6326c7b75bf1 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 24 Jun 2025 11:55:24 +1000 Subject: [PATCH 0837/1376] chore: udpate biome Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- biome.json | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2e382971e..98f68af79 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: hooks: - id: biome-check additional_dependencies: - - '@biomejs/biome@1.9.4' + - '@biomejs/biome@2.0.4' - repo: https://github.com/ComPWA/taplo-pre-commit rev: v0.9.3 diff --git a/biome.json b/biome.json index ad37d8b7c..d4d0fb13d 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,9 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "organizeImports": { - "enabled": true + "$schema": "https://biomejs.dev/schemas/2.0.4/schema.json", + "assist": { + "actions": { + "source": { "organizeImports": "on" } + } }, "linter": { "enabled": true, From 333726248f6e8756b4d6524d68fd36ad78189be2 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 25 Jun 2025 09:47:43 +1000 Subject: [PATCH 0838/1376] fix(v3): avoid error if there's no mismatch type Signed-off-by: JP-Ellis --- src/pact/v3/error.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pact/v3/error.py b/src/pact/v3/error.py index 2ab3efdcd..5db830b39 100644 --- a/src/pact/v3/error.py +++ b/src/pact/v3/error.py @@ -128,7 +128,8 @@ def from_dict(cls, data: dict[str, Any]) -> Mismatch: # noqa: C901, PLR0911 Returns: A new Mismatch object. """ - if mismatch_type := data.pop("type"): + if "type" in data: + mismatch_type = data.pop("type") # Pact mismatches if mismatch_type in ["MissingRequest", "missing-request"]: return MissingRequest(**data) @@ -196,7 +197,7 @@ def __str__(self) -> str: """ Informal string representation of the GenericMismatch. """ - return f"Generic mismatch: {self.type}" + return f"Generic mismatch ({self.type}): {self._data}" class MissingRequest(Mismatch): From 7c0f6788f87b91085c19d73f1c4d1432e37634e3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 25 Jun 2025 14:58:45 +1000 Subject: [PATCH 0839/1376] feat(v3): add will_respond_with for sync Signed-off-by: JP-Ellis --- .../interaction/_sync_message_interaction.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/pact/v3/interaction/_sync_message_interaction.py b/src/pact/v3/interaction/_sync_message_interaction.py index 92072d114..7836fbe41 100644 --- a/src/pact/v3/interaction/_sync_message_interaction.py +++ b/src/pact/v3/interaction/_sync_message_interaction.py @@ -4,6 +4,8 @@ from __future__ import annotations +from typing_extensions import Self + import pact.v3.ffi from pact.v3.interaction._base import Interaction @@ -59,3 +61,36 @@ def _handle(self) -> pact.v3.ffi.InteractionHandle: @property def _interaction_part(self) -> pact.v3.ffi.InteractionPart: return self.__interaction_part + + def will_respond_with(self) -> Self: + """ + Begin the response part of the interaction. + + This method is a convenience method to separate the request and response + parts of the interaction. This function is analogous to the + [`will_respond_with()`][pact.v3.pact.HttpInteraction.will_respond_with] + method of the [`HttpInteraction`][pact.v3.pact.HttpInteraction] class, + albeit more generic for synchronous message interactions. + + For example, the following two snippets are + equivalent: + + ```python + Pact(...).upon_receiving("A sync request", interaction="Sync") + .with_body("request body", part="Request") + .with_body("response body", part="Response") + ``` + + ```python + Pact(...).upon_receiving("A sync request", interaction="Sync") + .with_body("request body") + .will_respond_with() + .with_body("response body") + ``` + + Returns: + The current instance of the interaction. + + """ + self.__interaction_part = pact.v3.ffi.InteractionPart.RESPONSE + return self From e8836edb7d0bc7cadc43253f50712c923556d327 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:19:18 +1000 Subject: [PATCH 0840/1376] chore(deps): update astral-sh/setup-uv action to v6.3.1 (#1091) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 814be5db4..18b8dc869 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b59d00fde..5ad598a8b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f11861392..4590c006d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,7 +90,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 with: enable-cache: true cache-dependency-glob: | @@ -164,7 +164,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 with: enable-cache: true cache-dependency-glob: | @@ -202,7 +202,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 with: enable-cache: true cache-dependency-glob: | @@ -252,7 +252,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 with: enable-cache: true cache-dependency-glob: | @@ -277,7 +277,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 with: enable-cache: true cache-dependency-glob: | @@ -302,7 +302,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 with: enable-cache: true cache-dependency-glob: | @@ -348,7 +348,7 @@ jobs: key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Set up uv - uses: astral-sh/setup-uv@445689ea25e0de0a23313031f5fe577c74ae45a1 # v6.3.0 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 with: enable-cache: true cache-suffix: pre-commit From 52f1b5e1e49b8b458c0df4442ecba5d6703ec876 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 25 Jun 2025 13:51:22 +1000 Subject: [PATCH 0841/1376] chore(examples): add protobuf example Add a new example showcasing how to use the Protobuf plugin to validate Protobuf over HTTP content. Signed-off-by: JP-Ellis --- examples/.ruff.toml | 1 + examples/conftest.py | 10 + examples/tests/v3/conftest.py | 15 -- examples/v3/__init__.py | 0 examples/v3/plugins/__init__.py | 0 examples/v3/plugins/protobuf/__init__.py | 103 ++++++++ examples/v3/plugins/protobuf/person.proto | 27 ++ examples/v3/plugins/protobuf/person.py | 44 ++++ examples/v3/plugins/protobuf/person.pyi | 63 +++++ examples/v3/plugins/protobuf/test_consumer.py | 140 ++++++++++ examples/v3/plugins/protobuf/test_provider.py | 247 ++++++++++++++++++ pyproject.toml | 16 +- 12 files changed, 649 insertions(+), 17 deletions(-) delete mode 100644 examples/tests/v3/conftest.py create mode 100644 examples/v3/__init__.py create mode 100644 examples/v3/plugins/__init__.py create mode 100644 examples/v3/plugins/protobuf/__init__.py create mode 100644 examples/v3/plugins/protobuf/person.proto create mode 100644 examples/v3/plugins/protobuf/person.py create mode 100644 examples/v3/plugins/protobuf/person.pyi create mode 100644 examples/v3/plugins/protobuf/test_consumer.py create mode 100644 examples/v3/plugins/protobuf/test_provider.py diff --git a/examples/.ruff.toml b/examples/.ruff.toml index 7d112f1bc..cc0d509c7 100644 --- a/examples/.ruff.toml +++ b/examples/.ruff.toml @@ -7,6 +7,7 @@ ignore = [ "D104", # Require docstring in public package "PLR2004", # Forbid Magic Numbers "S101", # Forbid assert statements + "TID252", # Require absolute imports ] [lint.per-file-ignores] diff --git a/examples/conftest.py b/examples/conftest.py index b715b60bf..80b35807b 100644 --- a/examples/conftest.py +++ b/examples/conftest.py @@ -22,6 +22,8 @@ from testcontainers.compose import DockerCompose # type: ignore[import-untyped] from yarl import URL +from pact.v3 import ffi + if TYPE_CHECKING: from collections.abc import Generator, Sequence @@ -93,3 +95,11 @@ def pytest_xdist_setupnodes( "`--numprocesses=1` or using `hatch run example`.", stacklevel=1, ) + + +@pytest.fixture(scope="session", autouse=True) +def _setup_pact_logging() -> None: + """ + Set up logging for the pact package. + """ + ffi.log_to_stderr("INFO") diff --git a/examples/tests/v3/conftest.py b/examples/tests/v3/conftest.py deleted file mode 100644 index e961c9b43..000000000 --- a/examples/tests/v3/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Common Pytest configuration for the V3 examples. -""" - -import pytest - -from pact.v3 import ffi - - -@pytest.fixture(scope="session", autouse=True) -def _setup_pact_logging() -> None: - """ - Set up logging for the pact package. - """ - ffi.log_to_stderr("INFO") diff --git a/examples/v3/__init__.py b/examples/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/v3/plugins/__init__.py b/examples/v3/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/v3/plugins/protobuf/__init__.py b/examples/v3/plugins/protobuf/__init__.py new file mode 100644 index 000000000..52c9214d4 --- /dev/null +++ b/examples/v3/plugins/protobuf/__init__.py @@ -0,0 +1,103 @@ +""" +Protocol Buffers Plugin Example for Pact Python v3. + +This module provides an example of how to use Pact plugins to handle different +content types in contract testing. Specifically, this example demonstrates the +use of the protobuf plugin to test interactions involving Protocol Buffers +(protobuf) message serialization, building on the [protobuf.dev Python +tutorial](https://protobuf.dev/getting-started/pythontutorial/). + +## What are Protocol Buffers? + +Protocol Buffers (protobuf) is a language-neutral, platform-neutral extensible +mechanism for serializing structured data developed by Google. It can be thought +of as similar to XML or JSON, but with pre-defined schemas and a binary format +that is more efficient for both storage and transmission. + +The data structure is defined in a `.proto` file, which specifies the messages, +fields, and types. This is then compiled into source code in various programming +languages, allowing you to work with structured data in a type-safe manner. This +examples defines a simple address book and person schema within `person.proto` +and the `person.py` and `person.pyi` files have been generated from it using + +```console +protoc --python_out=. --pyi_out=. person.proto +``` + +## Pact and the Plugin Ecosystem + +Pact is traditionally focused on HTTP-based interactions with text-based +(primarily JSON) payloads. However, modern microservices architectures often use +various content types and transport mechanisms beyond simple text over HTTP. To +address this, Pact allows for extensibility through a plugin system that supports +different content types and protocols. + +Pact plugins extend the core functionality of Pact to support different content +types, transport protocols, and matching strategies. The plugin system allows +Pact to: + +- Handle different content types (e.g., protobuf) +- Support various transport mechanisms (e.g., gRPC) +- Provide specialized matching rules for different data formats +- Enable extensibility without modifying the core Pact library + +## The Protobuf Plugin Example + +This example builds an address book application using the same domain model as +the [protobuf.dev +tutorial](https://protobuf.dev/getting-started/pythontutorial/). + +It defines a simple address book schema using Protocol Buffers and demonstrates +how to use the Pact protobuf plugin to test interactions involving protobuf +messages. It is assumed that you have a basic understanding of Pact and Protocol +Buffers. +""" + +from .person import AddressBook, Person + + +def address_book() -> AddressBook: + """ + Create a sample address book. + + This function constructs an `AddressBook` instance containing three + `Person` instances: + + - Alice with ID 1 + - Bob with ID 2 + - Charlie with ID 3 + """ + alice = Person( + name="Alice", + id=1, + email="alice@gmail.com", + phones=[ + Person.PhoneNumber( + number="123-456-7890", type=Person.PhoneType.PHONE_TYPE_HOME + ), + Person.PhoneNumber( + number="987-654-3210", type=Person.PhoneType.PHONE_TYPE_MOBILE + ), + ], + ) + bob = Person( + name="Bob", + id=2, + email="bob@work.com", + phones=[ + Person.PhoneNumber( + number="555-555-5555", type=Person.PhoneType.PHONE_TYPE_WORK + ) + ], + ) + charlie = Person( + name="Charlie", + id=3, + email="charlie@example.com", + phones=[ + Person.PhoneNumber( + number="111-222-3333", type=Person.PhoneType.PHONE_TYPE_UNSPECIFIED + ) + ], + ) + return AddressBook(people=[alice, bob, charlie]) diff --git a/examples/v3/plugins/protobuf/person.proto b/examples/v3/plugins/protobuf/person.proto new file mode 100644 index 000000000..ff2343f10 --- /dev/null +++ b/examples/v3/plugins/protobuf/person.proto @@ -0,0 +1,27 @@ +edition = "2023"; + +package example; + +message Person { + string name = 1; + int32 id = 2; + string email = 3; + + enum PhoneType { + PHONE_TYPE_UNSPECIFIED = 0; + PHONE_TYPE_MOBILE = 1; + PHONE_TYPE_HOME = 2; + PHONE_TYPE_WORK = 3; + } + + message PhoneNumber { + string number = 1; + PhoneType type = 2 [default = PHONE_TYPE_HOME]; + } + + repeated PhoneNumber phones = 4; +} + +message AddressBook { + repeated Person people = 1; +} diff --git a/examples/v3/plugins/protobuf/person.py b/examples/v3/plugins/protobuf/person.py new file mode 100644 index 000000000..ed161885d --- /dev/null +++ b/examples/v3/plugins/protobuf/person.py @@ -0,0 +1,44 @@ +# ruff: noqa: PGH004 +# ruff: noqa +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: person.proto +# Protobuf Python Version: 6.31.1 +""" +Generated protocol buffer code. + +This file is auto-generated by the protocol buffer compiler, and should not be edited manually. +""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, 6, 31, 1, "", "person.proto" +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x0cperson.proto\x12\x07\x65xample"\xa1\x02\n\x06Person\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\x05\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12+\n\x06phones\x18\x04 \x03(\x0b\x32\x1b.example.Person.PhoneNumber\x1aW\n\x0bPhoneNumber\x12\x0e\n\x06number\x18\x01 \x01(\t\x12\x38\n\x04type\x18\x02 \x01(\x0e\x32\x19.example.Person.PhoneType:\x0fPHONE_TYPE_HOME"h\n\tPhoneType\x12\x1a\n\x16PHONE_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11PHONE_TYPE_MOBILE\x10\x01\x12\x13\n\x0fPHONE_TYPE_HOME\x10\x02\x12\x13\n\x0fPHONE_TYPE_WORK\x10\x03".\n\x0b\x41\x64\x64ressBook\x12\x1f\n\x06people\x18\x01 \x03(\x0b\x32\x0f.example.Personb\x08\x65\x64itionsp\xe8\x07' +) + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "person_pb2", _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals["_PERSON"]._serialized_start = 26 + _globals["_PERSON"]._serialized_end = 315 + _globals["_PERSON_PHONENUMBER"]._serialized_start = 122 + _globals["_PERSON_PHONENUMBER"]._serialized_end = 209 + _globals["_PERSON_PHONETYPE"]._serialized_start = 211 + _globals["_PERSON_PHONETYPE"]._serialized_end = 315 + _globals["_ADDRESSBOOK"]._serialized_start = 317 + _globals["_ADDRESSBOOK"]._serialized_end = 363 +# @@protoc_insertion_point(module_scope) diff --git a/examples/v3/plugins/protobuf/person.pyi b/examples/v3/plugins/protobuf/person.pyi new file mode 100644 index 000000000..fdfebc197 --- /dev/null +++ b/examples/v3/plugins/protobuf/person.pyi @@ -0,0 +1,63 @@ +# ruff: noqa: PGH004 +# ruff: noqa +""" +This file is auto-generated by the Protobuf plugin. +""" + +from collections.abc import Iterable as _Iterable +from collections.abc import Mapping as _Mapping +from typing import ClassVar as _ClassVar + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper + +DESCRIPTOR: _descriptor.FileDescriptor + +class Person(_message.Message): + __slots__ = ("name", "id", "email", "phones") + class PhoneType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + PHONE_TYPE_UNSPECIFIED: _ClassVar[Person.PhoneType] + PHONE_TYPE_MOBILE: _ClassVar[Person.PhoneType] + PHONE_TYPE_HOME: _ClassVar[Person.PhoneType] + PHONE_TYPE_WORK: _ClassVar[Person.PhoneType] + + PHONE_TYPE_UNSPECIFIED: Person.PhoneType + PHONE_TYPE_MOBILE: Person.PhoneType + PHONE_TYPE_HOME: Person.PhoneType + PHONE_TYPE_WORK: Person.PhoneType + class PhoneNumber(_message.Message): + __slots__ = ("number", "type") + NUMBER_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + number: str + type: Person.PhoneType + def __init__( + self, + number: str | None = ..., + type: Person.PhoneType | str | None = ..., + ) -> None: ... + + NAME_FIELD_NUMBER: _ClassVar[int] + ID_FIELD_NUMBER: _ClassVar[int] + EMAIL_FIELD_NUMBER: _ClassVar[int] + PHONES_FIELD_NUMBER: _ClassVar[int] + name: str + id: int + email: str + phones: _containers.RepeatedCompositeFieldContainer[Person.PhoneNumber] + def __init__( + self, + name: str | None = ..., + id: int | None = ..., + email: str | None = ..., + phones: _Iterable[Person.PhoneNumber | _Mapping] | None = ..., + ) -> None: ... + +class AddressBook(_message.Message): + __slots__ = ("people",) + PEOPLE_FIELD_NUMBER: _ClassVar[int] + people: _containers.RepeatedCompositeFieldContainer[Person] + def __init__(self, people: _Iterable[Person | _Mapping] | None = ...) -> None: ... diff --git a/examples/v3/plugins/protobuf/test_consumer.py b/examples/v3/plugins/protobuf/test_consumer.py new file mode 100644 index 000000000..ba4b495c9 --- /dev/null +++ b/examples/v3/plugins/protobuf/test_consumer.py @@ -0,0 +1,140 @@ +""" +Consumer test using Protobuf plugin with Pact Python v3. + +This module demonstrates how to write a consumer test using the Pact protobuf +plugin with Pact Python's v3 API. The protobuf plugin allows Pact to handle +Protocol Buffer messages as request and response payloads, enabling contract +testing for services that communicate using protobuf serialization. + +This example builds on the address book domain model from the [protobuf.dev +tutorial](https://protobuf.dev/getting-started/pythontutorial/) and shows how to +test a consumer that retrieves Person data from a provider service using +protobuf-serialized messages over HTTP. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest +import requests + +from pact.v3 import Pact + +from . import address_book +from .person import Person + +if TYPE_CHECKING: + from collections.abc import Generator + + +@pytest.fixture +def pact() -> Generator[Pact, None, None]: + """ + Set up the Pact fixture with protobuf plugin. + + This fixture configures a Pact instance for consumer testing with the + protobuf plugin enabled. The protobuf plugin allows Pact to understand and + handle Protocol Buffer message serialization for both request and response + payloads. + + The fixture uses the V4 specification which provides full support for + plugins and content type matching. + + Yields: + The configured Pact instance for protobuf consumer tests. + """ + pact_dir = Path(__file__).parents[3] / "pacts" + pact = ( + Pact("protobuf_consumer", "protobuf_provider") + .with_specification("V4") + .using_plugin("protobuf", "0.3.15") + ) + yield pact + pact.write_file(pact_dir) + + +def test_get_person_by_id(pact: Pact) -> None: + """ + Test retrieving a Person by ID using protobuf serialization. + + This test defines the expected interaction for a GET request to retrieve a + specific person from the address book. The response will be a protobuf- + serialized Person message. + + The test demonstrates: + + - Using the protobuf plugin to handle binary protobuf content + - Matching on protobuf message structure and content + - Deserializing the protobuf response for validation + + The provider state ensures that a person with ID 1 exists in the system, + corresponding to Alice from our sample address book. + """ + sample_address_book = address_book() + alice = sample_address_book.people[0] + expected_protobuf_data = alice.SerializeToString() + + ( + pact.upon_receiving("a request to get person by ID") + .given("person with ID 1 exists") + .with_request("GET", "/person/1") + .will_respond_with(200) + .with_header("Content-Type", "application/x-protobuf") + .with_binary_body(expected_protobuf_data, "application/x-protobuf") + ) + + with pact.serve() as srv: + # NOTE: We use the `requests` library here to demonstrate the + # principles; however, in a real-world scenario, you would be using the + # actual client code that interacts with the provider service. This + # ensures that you are testing the consumer's behaviour. + response = requests.get(f"{srv.url}/person/1", timeout=5) + + # Verify response + assert response.status_code == 200 + assert response.headers["Content-Type"] == "application/x-protobuf" + + # Deserialize the protobuf response and then verify its content + person = Person() + person.ParseFromString(response.content) + + assert person.id == 1 + assert person.name == "Alice" + assert person.email == "alice@gmail.com" + assert len(person.phones) == 2 + + assert person.phones[0].number == "123-456-7890" + assert person.phones[0].type == Person.PhoneType.PHONE_TYPE_HOME + assert person.phones[1].number == "987-654-3210" + assert person.phones[1].type == Person.PhoneType.PHONE_TYPE_MOBILE + + +def test_get_nonexistent_person(pact: Pact) -> None: + """ + Test retrieving a non-existent Person by ID. + + This test verifies the provider's behavior when requesting a person that + doesn't exist in the address book. The provider should return a 404 status + code with an appropriate error message as a JSON response. + """ + ( + pact.upon_receiving("a request to get non-existent person") + .given("person with ID 999 does not exist") + .with_request("GET", "/person/999") + .will_respond_with(404) + .with_header("Content-Type", "application/json") + .with_body({"detail": "Person not found"}) + ) + + with pact.serve() as srv: + # NOTE: Again, we use the `requests` library to simulate the consumer's + # request to the provider service. A real-world consumer would instead + # use its own client and check that the appropriate error message is + # raised and/or handled. + response = requests.get(f"{srv.url}/person/999", timeout=5) + + assert response.status_code == 404 + assert response.headers["Content-Type"] == "application/json" + assert response.json() == {"detail": "Person not found"} diff --git a/examples/v3/plugins/protobuf/test_provider.py b/examples/v3/plugins/protobuf/test_provider.py new file mode 100644 index 000000000..3ed4c3dee --- /dev/null +++ b/examples/v3/plugins/protobuf/test_provider.py @@ -0,0 +1,247 @@ +""" +Provider test using Protobuf plugin with Pact Python v3. + +This module demonstrates how to write a provider test using the Pact protobuf +plugin with Pact Python's v3 API. The provider test verifies that the provider +service correctly handles the contract defined by the consumer test. + +The provider test runs the actual provider service and uses Pact to replay the +consumer's interactions against the provider, verifying that the provider +responds correctly with protobuf-serialized messages. + +This example shows how to: + +- Set up a FastAPI provider that handles protobuf responses +- Use the Pact Verifier with the protobuf plugin +- Handle provider states for setting up test data +- Verify protobuf serialization in the provider responses +""" + +from __future__ import annotations + +import contextlib +import time +from pathlib import Path +from threading import Thread +from typing import TYPE_CHECKING, Any, Literal + +import pytest +import uvicorn +from yarl import URL + +from fastapi import FastAPI, HTTPException +from fastapi.responses import Response +from pact.v3 import Verifier + +from . import address_book +from .person import AddressBook + +if TYPE_CHECKING: + from collections.abc import Generator + +PROVIDER_URL = URL("http://localhost:8001") + +# Global variable to hold our mock address book data +# In a real application, this would be a database or other data store +MOCK_ADDRESS_BOOK: AddressBook | None = None + + +class Server(uvicorn.Server): + """ + Custom server class to run the FastAPI server in a separate thread. + + This allows the provider test to run the FastAPI server in the background + while Pact verifies the interactions against it. + """ + + def install_signal_handlers(self) -> None: + """ + Prevent the server from installing signal handlers. + + This is required to run the FastAPI server in a separate process. + """ + + @contextlib.contextmanager + def run_in_thread(self) -> Generator[str, None, None]: + """ + Run the FastAPI server in a separate thread. + + Yields: + The URL of the running server. + """ + thread = Thread(target=self.run) + thread.start() + try: + while not self.started: + time.sleep(0.01) + yield f"http://{self.config.host}:{self.config.port}" + finally: + self.should_exit = True + thread.join() + + +app = FastAPI(title="Protobuf Address Book API") +""" +FastAPI application + +This application serves as the provider for the address book service, +handling requests to retrieve person data by ID. It uses Protocol Buffers for +serialization of the response data. + +This code would typically be in a separate module within your application, but +for the sake of this example, it is included directly within the test module. +""" + + +@app.get("/person/{person_id}") +async def get_person(person_id: int) -> Response: + """ + Get a person by ID, returning protobuf-serialized data. + + Args: + person_id: The ID of the person to retrieve. + + Returns: + Response containing protobuf-serialized Person data. + + Raises: + HTTPException: If person is not found. + """ + if MOCK_ADDRESS_BOOK is None: + raise HTTPException(status_code=404, detail="Person not found") + + # Find person by ID + for person in MOCK_ADDRESS_BOOK.people: + if person.id == person_id: + # Serialize person to protobuf bytes + protobuf_data = person.SerializeToString() + return Response( + content=protobuf_data, + media_type="application/x-protobuf", + ) + + raise HTTPException(status_code=404, detail="Person not found") + + +@pytest.fixture(scope="session") +def server() -> Generator[str, None, None]: + """ + Fixture to start the FastAPI server for testing. + + Yields: + The URL of the running server. + """ + assert PROVIDER_URL.host is not None + assert PROVIDER_URL.port is not None + server = Server( + uvicorn.Config( + app, + host=PROVIDER_URL.host, + port=PROVIDER_URL.port, + ) + ) + with server.run_in_thread() as url: + yield url + + +def test_provider(server: str) -> None: + """ + Test the protobuf provider against the consumer contract. + + This test uses the Pact Verifier to replay the consumer's interactions + against the running provider service. It verifies that the provider + correctly handles protobuf serialization and responds appropriately + to both successful and error scenarios. + + The test: + + 1. Configures the Verifier with the protobuf plugin + 2. Points the verifier to the pact file generated by the consumer + 3. Sets up state handlers to prepare test data + 4. Verifies all interactions match the contract + """ + pact_file = ( + Path(__file__).parents[3] / "pacts" / "protobuf_consumer-protobuf_provider.json" + ) + + verifier = ( + Verifier("protobuf_provider") + .add_transport(url=server) + .add_source(pact_file) + .state_handler(provider_state_handler, teardown=True) + ) + + verifier.verify() + + +def provider_state_handler( + state: str, + action: Literal["setup", "teardown"], + parameters: dict[str, Any] | None = None, # noqa: ARG001 +) -> None: + """ + Handle provider state setup and teardown. + + This function is called by Pact to set up the provider's internal state + before each interaction is replayed. It ensures that the provider has + the necessary data to respond correctly to the consumer's requests. + + Args: + state: + The provider state name from the consumer test. + + action: + Either `"setup"` or `"teardown"`. + + parameters: + Additional parameters (not used in this example). + """ + if action == "setup": + { + "person with ID 1 exists": setup_person_exists, + "person with ID 999 does not exist": setup_person_doesnt_exist, + }[state]() + + if action == "teardown": + { + "person with ID 1 exists": teardown_person_exists, + "person with ID 999 does not exist": teardown_person_doesnt_exist, + }[state]() + + +def setup_person_exists() -> None: + """ + Set up the provider state where person with ID 1 exists. + + This creates a mock address book containing Alice (ID 1) so that + the provider can return the expected protobuf data. + """ + global MOCK_ADDRESS_BOOK # noqa: PLW0603 + MOCK_ADDRESS_BOOK = address_book() + + +def setup_person_doesnt_exist() -> None: + """ + Set up the provider state where person with ID 999 doesn't exist. + + This ensures the mock address book is empty so the provider will + return a 404 error as expected. + """ + global MOCK_ADDRESS_BOOK # noqa: PLW0603 + MOCK_ADDRESS_BOOK = AddressBook() # Empty address book + + +def teardown_person_exists() -> None: + """ + Clean up after testing person exists scenario. + """ + global MOCK_ADDRESS_BOOK # noqa: PLW0603 + MOCK_ADDRESS_BOOK = None + + +def teardown_person_doesnt_exist() -> None: + """ + Clean up after testing person doesn't exist scenario. + """ + global MOCK_ADDRESS_BOOK # noqa: PLW0603 + MOCK_ADDRESS_BOOK = None diff --git a/pyproject.toml b/pyproject.toml index 67c274a7c..4df5ef835 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,12 @@ dependencies = [ [project.optional-dependencies] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. - devel = ["pact-python[devel-types,devel-docs,devel-test]", "ruff==0.12.0"] + devel = [ + "pact-python[devel-docs]", + "pact-python[devel-test]", + "pact-python[devel-types]", + "ruff==0.12.0", + ] devel-docs = [ "mkdocs-literate-nav~=0.6", "mkdocs-material[imaging]~=9.4", @@ -79,6 +84,7 @@ dependencies = [ "flask[async]~=3.0", "httpx~=0.0", "mock~=5.0", + "protobuf~=6.0", "pytest-asyncio~=1.0", "pytest-bdd~=8.0", "pytest-cov~=6.0", @@ -86,8 +92,14 @@ dependencies = [ "pytest-xdist~=3.0", "pytest~=8.0", "testcontainers~=4.0", + "uvicorn[standard]~=0.0", + ] + devel-types = [ + "mypy==1.16.1", + "types-cffi~=1.0", + "types-protobuf~=6.0", + "types-requests~=2.0", ] - devel-types = ["mypy==1.16.1", "types-cffi~=1.0", "types-requests~=2.0"] [build-system] build-backend = "hatchling.build" From af1a89c06af0ca5f9761719516dee4bab9bcc6a6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 25 Jun 2025 14:35:11 +1000 Subject: [PATCH 0842/1376] chore: add version stub file Signed-off-by: JP-Ellis --- src/pact/__version__.pyi | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/pact/__version__.pyi diff --git a/src/pact/__version__.pyi b/src/pact/__version__.pyi new file mode 100644 index 000000000..a8b247d06 --- /dev/null +++ b/src/pact/__version__.pyi @@ -0,0 +1,10 @@ +from typing_extensions import TypeAlias + +__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] + +VERSION_TUPLE: TypeAlias = tuple[int | str, ...] + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE From 06c439d6c7f2ba820fd08ae22a6cc2b830b32836 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:28:36 +1000 Subject: [PATCH 0843/1376] fix(deps): update ruff to v0.12.1 (#1093) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 98f68af79..cfe41fa17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.0 + rev: v0.12.1 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index 4df5ef835..8ae7573a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ dependencies = [ "pact-python[devel-docs]", "pact-python[devel-test]", "pact-python[devel-types]", - "ruff==0.12.0", + "ruff==0.12.1", ] devel-docs = [ "mkdocs-literate-nav~=0.6", From 047433d5dbf117ac6c333543ff4dc5fa32bc33fd Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 27 Jun 2025 12:09:51 +1000 Subject: [PATCH 0844/1376] chore(examples): parametrize protobuf example Also a minor refactor creating a share `proto` folder with the Protobuf definition in preparation for a gRPC example. Signed-off-by: JP-Ellis --- examples/v3/plugins/proto/__init__.py | 0 examples/v3/plugins/proto/person.proto | 72 +++++++ examples/v3/plugins/proto/person_pb2.py | 54 +++++ examples/v3/plugins/proto/person_pb2.pyi | 99 +++++++++ examples/v3/plugins/proto/person_pb2_grpc.py | 204 ++++++++++++++++++ examples/v3/plugins/protobuf/__init__.py | 2 +- examples/v3/plugins/protobuf/person.proto | 27 --- examples/v3/plugins/protobuf/person.py | 44 ---- examples/v3/plugins/protobuf/person.pyi | 63 ------ examples/v3/plugins/protobuf/test_consumer.py | 8 +- examples/v3/plugins/protobuf/test_provider.py | 99 ++++----- pyproject.toml | 2 + 12 files changed, 481 insertions(+), 193 deletions(-) create mode 100644 examples/v3/plugins/proto/__init__.py create mode 100644 examples/v3/plugins/proto/person.proto create mode 100644 examples/v3/plugins/proto/person_pb2.py create mode 100644 examples/v3/plugins/proto/person_pb2.pyi create mode 100644 examples/v3/plugins/proto/person_pb2_grpc.py delete mode 100644 examples/v3/plugins/protobuf/person.proto delete mode 100644 examples/v3/plugins/protobuf/person.py delete mode 100644 examples/v3/plugins/protobuf/person.pyi diff --git a/examples/v3/plugins/proto/__init__.py b/examples/v3/plugins/proto/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/v3/plugins/proto/person.proto b/examples/v3/plugins/proto/person.proto new file mode 100644 index 000000000..1f6b2fd7c --- /dev/null +++ b/examples/v3/plugins/proto/person.proto @@ -0,0 +1,72 @@ +// examples/v3/plugins/proto/person.proto +edition = "2023"; + +package person; + +// The person message definition +message Person { + string name = 1; + int32 id = 2; + string email = 3; + + enum PhoneType { + PHONE_TYPE_UNSPECIFIED = 0; + PHONE_TYPE_MOBILE = 1; + PHONE_TYPE_HOME = 2; + PHONE_TYPE_WORK = 3; + } + + message PhoneNumber { + string number = 1; + PhoneType type = 2 [default = PHONE_TYPE_HOME]; + } + + repeated PhoneNumber phones = 4; +} + +// The address book message definition +message AddressBook { + repeated Person people = 1; +} + +// Request message for getting a person by ID +message GetPersonRequest { + int32 person_id = 1; +} + +// Response message for getting a person +message GetPersonResponse { + Person person = 1; +} + +// Request message for listing all people +message ListPeopleRequest { + // Can add pagination parameters here in the future +} + +// Response message for listing people +message ListPeopleResponse { + repeated Person people = 1; +} + +// Request message for adding a person +message AddPersonRequest { + Person person = 1; +} + +// Response message for adding a person +message AddPersonResponse { + Person person = 1; +} + +// The AddressBook service definition +service AddressBookService { + // Get a person by ID + rpc GetPerson(GetPersonRequest) returns (GetPersonResponse); + + // List all people in the address book + rpc ListPeople(ListPeopleRequest) returns (ListPeopleResponse); + + // Add a new person to the address book + rpc AddPerson(AddPersonRequest) returns (AddPersonResponse); +} diff --git a/examples/v3/plugins/proto/person_pb2.py b/examples/v3/plugins/proto/person_pb2.py new file mode 100644 index 000000000..95741a653 --- /dev/null +++ b/examples/v3/plugins/proto/person_pb2.py @@ -0,0 +1,54 @@ +# ruff: noqa: PGH004 +# ruff: noqa +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: person.proto +# Protobuf Python Version: 6.31.0 +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, 6, 31, 0, "", "person.proto" +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x0cperson.proto\x12\x06person"\x9f\x02\n\x06Person\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\x05\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12*\n\x06phones\x18\x04 \x03(\x0b\x32\x1a.person.Person.PhoneNumber\x1aV\n\x0bPhoneNumber\x12\x0e\n\x06number\x18\x01 \x01(\t\x12\x37\n\x04type\x18\x02 \x01(\x0e\x32\x18.person.Person.PhoneType:\x0fPHONE_TYPE_HOME"h\n\tPhoneType\x12\x1a\n\x16PHONE_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11PHONE_TYPE_MOBILE\x10\x01\x12\x13\n\x0fPHONE_TYPE_HOME\x10\x02\x12\x13\n\x0fPHONE_TYPE_WORK\x10\x03"-\n\x0b\x41\x64\x64ressBook\x12\x1e\n\x06people\x18\x01 \x03(\x0b\x32\x0e.person.Person"%\n\x10GetPersonRequest\x12\x11\n\tperson_id\x18\x01 \x01(\x05"3\n\x11GetPersonResponse\x12\x1e\n\x06person\x18\x01 \x01(\x0b\x32\x0e.person.Person"\x13\n\x11ListPeopleRequest"4\n\x12ListPeopleResponse\x12\x1e\n\x06people\x18\x01 \x03(\x0b\x32\x0e.person.Person"2\n\x10\x41\x64\x64PersonRequest\x12\x1e\n\x06person\x18\x01 \x01(\x0b\x32\x0e.person.Person"3\n\x11\x41\x64\x64PersonResponse\x12\x1e\n\x06person\x18\x01 \x01(\x0b\x32\x0e.person.Person2\xdd\x01\n\x12\x41\x64\x64ressBookService\x12@\n\tGetPerson\x12\x18.person.GetPersonRequest\x1a\x19.person.GetPersonResponse\x12\x43\n\nListPeople\x12\x19.person.ListPeopleRequest\x1a\x1a.person.ListPeopleResponse\x12@\n\tAddPerson\x12\x18.person.AddPersonRequest\x1a\x19.person.AddPersonResponseb\x08\x65\x64itionsp\xe8\x07' +) + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "person_pb2", _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals["_PERSON"]._serialized_start = 25 + _globals["_PERSON"]._serialized_end = 312 + _globals["_PERSON_PHONENUMBER"]._serialized_start = 120 + _globals["_PERSON_PHONENUMBER"]._serialized_end = 206 + _globals["_PERSON_PHONETYPE"]._serialized_start = 208 + _globals["_PERSON_PHONETYPE"]._serialized_end = 312 + _globals["_ADDRESSBOOK"]._serialized_start = 314 + _globals["_ADDRESSBOOK"]._serialized_end = 359 + _globals["_GETPERSONREQUEST"]._serialized_start = 361 + _globals["_GETPERSONREQUEST"]._serialized_end = 398 + _globals["_GETPERSONRESPONSE"]._serialized_start = 400 + _globals["_GETPERSONRESPONSE"]._serialized_end = 451 + _globals["_LISTPEOPLEREQUEST"]._serialized_start = 453 + _globals["_LISTPEOPLEREQUEST"]._serialized_end = 472 + _globals["_LISTPEOPLERESPONSE"]._serialized_start = 474 + _globals["_LISTPEOPLERESPONSE"]._serialized_end = 526 + _globals["_ADDPERSONREQUEST"]._serialized_start = 528 + _globals["_ADDPERSONREQUEST"]._serialized_end = 578 + _globals["_ADDPERSONRESPONSE"]._serialized_start = 580 + _globals["_ADDPERSONRESPONSE"]._serialized_end = 631 + _globals["_ADDRESSBOOKSERVICE"]._serialized_start = 634 + _globals["_ADDRESSBOOKSERVICE"]._serialized_end = 855 +# @@protoc_insertion_point(module_scope) diff --git a/examples/v3/plugins/proto/person_pb2.pyi b/examples/v3/plugins/proto/person_pb2.pyi new file mode 100644 index 000000000..0d1547245 --- /dev/null +++ b/examples/v3/plugins/proto/person_pb2.pyi @@ -0,0 +1,99 @@ +# ruff: noqa: PGH004 +# ruff: noqa +from collections.abc import Iterable as _Iterable +from collections.abc import Mapping as _Mapping +from typing import ClassVar as _ClassVar +from typing import Optional as _Optional +from typing import Union as _Union + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper + +DESCRIPTOR: _descriptor.FileDescriptor + +class Person(_message.Message): + __slots__ = ("email", "id", "name", "phones") + class PhoneType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + PHONE_TYPE_UNSPECIFIED: _ClassVar[Person.PhoneType] + PHONE_TYPE_MOBILE: _ClassVar[Person.PhoneType] + PHONE_TYPE_HOME: _ClassVar[Person.PhoneType] + PHONE_TYPE_WORK: _ClassVar[Person.PhoneType] + + PHONE_TYPE_UNSPECIFIED: Person.PhoneType + PHONE_TYPE_MOBILE: Person.PhoneType + PHONE_TYPE_HOME: Person.PhoneType + PHONE_TYPE_WORK: Person.PhoneType + class PhoneNumber(_message.Message): + __slots__ = ("number", "type") + NUMBER_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + number: str + type: Person.PhoneType + def __init__( + self, + number: _Optional[str] = ..., + type: _Optional[_Union[Person.PhoneType, str]] = ..., + ) -> None: ... + + NAME_FIELD_NUMBER: _ClassVar[int] + ID_FIELD_NUMBER: _ClassVar[int] + EMAIL_FIELD_NUMBER: _ClassVar[int] + PHONES_FIELD_NUMBER: _ClassVar[int] + name: str + id: int + email: str + phones: _containers.RepeatedCompositeFieldContainer[Person.PhoneNumber] + def __init__( + self, + name: _Optional[str] = ..., + id: _Optional[int] = ..., + email: _Optional[str] = ..., + phones: _Optional[_Iterable[_Union[Person.PhoneNumber, _Mapping]]] = ..., + ) -> None: ... + +class AddressBook(_message.Message): + __slots__ = ("people",) + PEOPLE_FIELD_NUMBER: _ClassVar[int] + people: _containers.RepeatedCompositeFieldContainer[Person] + def __init__( + self, people: _Optional[_Iterable[_Union[Person, _Mapping]]] = ... + ) -> None: ... + +class GetPersonRequest(_message.Message): + __slots__ = ("person_id",) + PERSON_ID_FIELD_NUMBER: _ClassVar[int] + person_id: int + def __init__(self, person_id: _Optional[int] = ...) -> None: ... + +class GetPersonResponse(_message.Message): + __slots__ = ("person",) + PERSON_FIELD_NUMBER: _ClassVar[int] + person: Person + def __init__(self, person: _Optional[_Union[Person, _Mapping]] = ...) -> None: ... + +class ListPeopleRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class ListPeopleResponse(_message.Message): + __slots__ = ("people",) + PEOPLE_FIELD_NUMBER: _ClassVar[int] + people: _containers.RepeatedCompositeFieldContainer[Person] + def __init__( + self, people: _Optional[_Iterable[_Union[Person, _Mapping]]] = ... + ) -> None: ... + +class AddPersonRequest(_message.Message): + __slots__ = ("person",) + PERSON_FIELD_NUMBER: _ClassVar[int] + person: Person + def __init__(self, person: _Optional[_Union[Person, _Mapping]] = ...) -> None: ... + +class AddPersonResponse(_message.Message): + __slots__ = ("person",) + PERSON_FIELD_NUMBER: _ClassVar[int] + person: Person + def __init__(self, person: _Optional[_Union[Person, _Mapping]] = ...) -> None: ... diff --git a/examples/v3/plugins/proto/person_pb2_grpc.py b/examples/v3/plugins/proto/person_pb2_grpc.py new file mode 100644 index 000000000..5325331de --- /dev/null +++ b/examples/v3/plugins/proto/person_pb2_grpc.py @@ -0,0 +1,204 @@ +# ruff: noqa: PGH004 +# ruff: noqa +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" + +import grpc +import warnings + +from . import person_pb2 as person__pb2 + +GRPC_GENERATED_VERSION = "1.73.1" +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + + _version_not_supported = first_version_is_lower( + GRPC_VERSION, GRPC_GENERATED_VERSION + ) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f"The grpc package installed is at version {GRPC_VERSION}," + + f" but the generated code in person_pb2_grpc.py depends on" + + f" grpcio>={GRPC_GENERATED_VERSION}." + + f" Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}" + + f" or downgrade your generated code using grpcio-tools<={GRPC_VERSION}." + ) + + +class AddressBookServiceStub(object): + """The AddressBook service definition""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetPerson = channel.unary_unary( + "/person.AddressBookService/GetPerson", + request_serializer=person__pb2.GetPersonRequest.SerializeToString, + response_deserializer=person__pb2.GetPersonResponse.FromString, + _registered_method=True, + ) + self.ListPeople = channel.unary_unary( + "/person.AddressBookService/ListPeople", + request_serializer=person__pb2.ListPeopleRequest.SerializeToString, + response_deserializer=person__pb2.ListPeopleResponse.FromString, + _registered_method=True, + ) + self.AddPerson = channel.unary_unary( + "/person.AddressBookService/AddPerson", + request_serializer=person__pb2.AddPersonRequest.SerializeToString, + response_deserializer=person__pb2.AddPersonResponse.FromString, + _registered_method=True, + ) + + +class AddressBookServiceServicer(object): + """The AddressBook service definition""" + + def GetPerson(self, request, context): + """Get a person by ID""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def ListPeople(self, request, context): + """List all people in the address book""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def AddPerson(self, request, context): + """Add a new person to the address book""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + +def add_AddressBookServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + "GetPerson": grpc.unary_unary_rpc_method_handler( + servicer.GetPerson, + request_deserializer=person__pb2.GetPersonRequest.FromString, + response_serializer=person__pb2.GetPersonResponse.SerializeToString, + ), + "ListPeople": grpc.unary_unary_rpc_method_handler( + servicer.ListPeople, + request_deserializer=person__pb2.ListPeopleRequest.FromString, + response_serializer=person__pb2.ListPeopleResponse.SerializeToString, + ), + "AddPerson": grpc.unary_unary_rpc_method_handler( + servicer.AddPerson, + request_deserializer=person__pb2.AddPersonRequest.FromString, + response_serializer=person__pb2.AddPersonResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + "person.AddressBookService", rpc_method_handlers + ) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers( + "person.AddressBookService", rpc_method_handlers + ) + + +# This class is part of an EXPERIMENTAL API. +class AddressBookService(object): + """The AddressBook service definition""" + + @staticmethod + def GetPerson( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/person.AddressBookService/GetPerson", + person__pb2.GetPersonRequest.SerializeToString, + person__pb2.GetPersonResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True, + ) + + @staticmethod + def ListPeople( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/person.AddressBookService/ListPeople", + person__pb2.ListPeopleRequest.SerializeToString, + person__pb2.ListPeopleResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True, + ) + + @staticmethod + def AddPerson( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/person.AddressBookService/AddPerson", + person__pb2.AddPersonRequest.SerializeToString, + person__pb2.AddPersonResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True, + ) diff --git a/examples/v3/plugins/protobuf/__init__.py b/examples/v3/plugins/protobuf/__init__.py index 52c9214d4..a3d328123 100644 --- a/examples/v3/plugins/protobuf/__init__.py +++ b/examples/v3/plugins/protobuf/__init__.py @@ -53,7 +53,7 @@ Buffers. """ -from .person import AddressBook, Person +from ..proto.person_pb2 import AddressBook, Person def address_book() -> AddressBook: diff --git a/examples/v3/plugins/protobuf/person.proto b/examples/v3/plugins/protobuf/person.proto deleted file mode 100644 index ff2343f10..000000000 --- a/examples/v3/plugins/protobuf/person.proto +++ /dev/null @@ -1,27 +0,0 @@ -edition = "2023"; - -package example; - -message Person { - string name = 1; - int32 id = 2; - string email = 3; - - enum PhoneType { - PHONE_TYPE_UNSPECIFIED = 0; - PHONE_TYPE_MOBILE = 1; - PHONE_TYPE_HOME = 2; - PHONE_TYPE_WORK = 3; - } - - message PhoneNumber { - string number = 1; - PhoneType type = 2 [default = PHONE_TYPE_HOME]; - } - - repeated PhoneNumber phones = 4; -} - -message AddressBook { - repeated Person people = 1; -} diff --git a/examples/v3/plugins/protobuf/person.py b/examples/v3/plugins/protobuf/person.py deleted file mode 100644 index ed161885d..000000000 --- a/examples/v3/plugins/protobuf/person.py +++ /dev/null @@ -1,44 +0,0 @@ -# ruff: noqa: PGH004 -# ruff: noqa -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: person.proto -# Protobuf Python Version: 6.31.1 -""" -Generated protocol buffer code. - -This file is auto-generated by the protocol buffer compiler, and should not be edited manually. -""" - -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder - -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, 6, 31, 1, "", "person.proto" -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\x0cperson.proto\x12\x07\x65xample"\xa1\x02\n\x06Person\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\x05\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12+\n\x06phones\x18\x04 \x03(\x0b\x32\x1b.example.Person.PhoneNumber\x1aW\n\x0bPhoneNumber\x12\x0e\n\x06number\x18\x01 \x01(\t\x12\x38\n\x04type\x18\x02 \x01(\x0e\x32\x19.example.Person.PhoneType:\x0fPHONE_TYPE_HOME"h\n\tPhoneType\x12\x1a\n\x16PHONE_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11PHONE_TYPE_MOBILE\x10\x01\x12\x13\n\x0fPHONE_TYPE_HOME\x10\x02\x12\x13\n\x0fPHONE_TYPE_WORK\x10\x03".\n\x0b\x41\x64\x64ressBook\x12\x1f\n\x06people\x18\x01 \x03(\x0b\x32\x0f.example.Personb\x08\x65\x64itionsp\xe8\x07' -) - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "person_pb2", _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals["_PERSON"]._serialized_start = 26 - _globals["_PERSON"]._serialized_end = 315 - _globals["_PERSON_PHONENUMBER"]._serialized_start = 122 - _globals["_PERSON_PHONENUMBER"]._serialized_end = 209 - _globals["_PERSON_PHONETYPE"]._serialized_start = 211 - _globals["_PERSON_PHONETYPE"]._serialized_end = 315 - _globals["_ADDRESSBOOK"]._serialized_start = 317 - _globals["_ADDRESSBOOK"]._serialized_end = 363 -# @@protoc_insertion_point(module_scope) diff --git a/examples/v3/plugins/protobuf/person.pyi b/examples/v3/plugins/protobuf/person.pyi deleted file mode 100644 index fdfebc197..000000000 --- a/examples/v3/plugins/protobuf/person.pyi +++ /dev/null @@ -1,63 +0,0 @@ -# ruff: noqa: PGH004 -# ruff: noqa -""" -This file is auto-generated by the Protobuf plugin. -""" - -from collections.abc import Iterable as _Iterable -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar - -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf.internal import containers as _containers -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper - -DESCRIPTOR: _descriptor.FileDescriptor - -class Person(_message.Message): - __slots__ = ("name", "id", "email", "phones") - class PhoneType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - PHONE_TYPE_UNSPECIFIED: _ClassVar[Person.PhoneType] - PHONE_TYPE_MOBILE: _ClassVar[Person.PhoneType] - PHONE_TYPE_HOME: _ClassVar[Person.PhoneType] - PHONE_TYPE_WORK: _ClassVar[Person.PhoneType] - - PHONE_TYPE_UNSPECIFIED: Person.PhoneType - PHONE_TYPE_MOBILE: Person.PhoneType - PHONE_TYPE_HOME: Person.PhoneType - PHONE_TYPE_WORK: Person.PhoneType - class PhoneNumber(_message.Message): - __slots__ = ("number", "type") - NUMBER_FIELD_NUMBER: _ClassVar[int] - TYPE_FIELD_NUMBER: _ClassVar[int] - number: str - type: Person.PhoneType - def __init__( - self, - number: str | None = ..., - type: Person.PhoneType | str | None = ..., - ) -> None: ... - - NAME_FIELD_NUMBER: _ClassVar[int] - ID_FIELD_NUMBER: _ClassVar[int] - EMAIL_FIELD_NUMBER: _ClassVar[int] - PHONES_FIELD_NUMBER: _ClassVar[int] - name: str - id: int - email: str - phones: _containers.RepeatedCompositeFieldContainer[Person.PhoneNumber] - def __init__( - self, - name: str | None = ..., - id: int | None = ..., - email: str | None = ..., - phones: _Iterable[Person.PhoneNumber | _Mapping] | None = ..., - ) -> None: ... - -class AddressBook(_message.Message): - __slots__ = ("people",) - PEOPLE_FIELD_NUMBER: _ClassVar[int] - people: _containers.RepeatedCompositeFieldContainer[Person] - def __init__(self, people: _Iterable[Person | _Mapping] | None = ...) -> None: ... diff --git a/examples/v3/plugins/protobuf/test_consumer.py b/examples/v3/plugins/protobuf/test_consumer.py index ba4b495c9..79620fddb 100644 --- a/examples/v3/plugins/protobuf/test_consumer.py +++ b/examples/v3/plugins/protobuf/test_consumer.py @@ -22,8 +22,8 @@ from pact.v3 import Pact +from ..proto.person_pb2 import Person from . import address_book -from .person import Person if TYPE_CHECKING: from collections.abc import Generator @@ -49,7 +49,7 @@ def pact() -> Generator[Pact, None, None]: pact = ( Pact("protobuf_consumer", "protobuf_provider") .with_specification("V4") - .using_plugin("protobuf", "0.3.15") + .using_plugin("protobuf") ) yield pact pact.write_file(pact_dir) @@ -78,7 +78,7 @@ def test_get_person_by_id(pact: Pact) -> None: ( pact.upon_receiving("a request to get person by ID") - .given("person with ID 1 exists") + .given("person with the given ID exists", parameters={"user_id": 1}) .with_request("GET", "/person/1") .will_respond_with(200) .with_header("Content-Type", "application/x-protobuf") @@ -121,7 +121,7 @@ def test_get_nonexistent_person(pact: Pact) -> None: """ ( pact.upon_receiving("a request to get non-existent person") - .given("person with ID 999 does not exist") + .given("person with the given ID does not exist", parameters={"user_id": 999}) .with_request("GET", "/person/999") .will_respond_with(404) .with_header("Content-Type", "application/json") diff --git a/examples/v3/plugins/protobuf/test_provider.py b/examples/v3/plugins/protobuf/test_provider.py index 3ed4c3dee..d11487ea0 100644 --- a/examples/v3/plugins/protobuf/test_provider.py +++ b/examples/v3/plugins/protobuf/test_provider.py @@ -33,8 +33,8 @@ from fastapi.responses import Response from pact.v3 import Verifier +from ..proto.person_pb2 import AddressBook from . import address_book -from .person import AddressBook if TYPE_CHECKING: from collections.abc import Generator @@ -168,80 +168,71 @@ def test_provider(server: str) -> None: Verifier("protobuf_provider") .add_transport(url=server) .add_source(pact_file) - .state_handler(provider_state_handler, teardown=True) + .state_handler( + { + "person with the given ID exists": state_person_exists, + "person with the given ID does not exist": state_person_doesnt_exist, + }, + teardown=True, + ) ) verifier.verify() -def provider_state_handler( - state: str, +def state_person_exists( action: Literal["setup", "teardown"], - parameters: dict[str, Any] | None = None, # noqa: ARG001 + parameters: dict[str, Any] | None = None, ) -> None: """ - Handle provider state setup and teardown. - - This function is called by Pact to set up the provider's internal state - before each interaction is replayed. It ensures that the provider has - the necessary data to respond correctly to the consumer's requests. + Handle provider state for when a person with the given ID exists. Args: - state: - The provider state name from the consumer test. - action: - Either `"setup"` or `"teardown"`. + Either "setup" or "teardown". parameters: - Additional parameters (not used in this example). - """ - if action == "setup": - { - "person with ID 1 exists": setup_person_exists, - "person with ID 999 does not exist": setup_person_doesnt_exist, - }[state]() - - if action == "teardown": - { - "person with ID 1 exists": teardown_person_exists, - "person with ID 999 does not exist": teardown_person_doesnt_exist, - }[state]() - - -def setup_person_exists() -> None: - """ - Set up the provider state where person with ID 1 exists. - - This creates a mock address book containing Alice (ID 1) so that - the provider can return the expected protobuf data. + Dictionary containing user_id key. """ global MOCK_ADDRESS_BOOK # noqa: PLW0603 - MOCK_ADDRESS_BOOK = address_book() + if action == "setup": + MOCK_ADDRESS_BOOK = address_book() + if user_id := parameters.get("user_id") if parameters else None: + assert any(person.id == user_id for person in MOCK_ADDRESS_BOOK.people), ( + f"Person with ID {user_id} does not exist in address book" + ) + else: + msg = "User ID not provided" + raise AssertionError(msg) + elif action == "teardown": + MOCK_ADDRESS_BOOK = None -def setup_person_doesnt_exist() -> None: - """ - Set up the provider state where person with ID 999 doesn't exist. - This ensures the mock address book is empty so the provider will - return a 404 error as expected. +def state_person_doesnt_exist( + action: Literal["setup", "teardown"], + parameters: dict[str, Any] | None = None, +) -> None: """ - global MOCK_ADDRESS_BOOK # noqa: PLW0603 - MOCK_ADDRESS_BOOK = AddressBook() # Empty address book + Handle provider state for when a person with the given ID doesn't exist. + Args: + action: + Either "setup" or "teardown". -def teardown_person_exists() -> None: - """ - Clean up after testing person exists scenario. + parameters: + Dictionary containing user_id key. """ global MOCK_ADDRESS_BOOK # noqa: PLW0603 - MOCK_ADDRESS_BOOK = None - -def teardown_person_doesnt_exist() -> None: - """ - Clean up after testing person doesn't exist scenario. - """ - global MOCK_ADDRESS_BOOK # noqa: PLW0603 - MOCK_ADDRESS_BOOK = None + if action == "setup": + MOCK_ADDRESS_BOOK = AddressBook() + if user_id := parameters.get("user_id") if parameters else None: + assert not any( + person.id == user_id for person in MOCK_ADDRESS_BOOK.people + ), f"Person with ID {user_id} should not exist in address book" + else: + msg = "User ID not provided" + raise AssertionError(msg) + elif action == "teardown": + MOCK_ADDRESS_BOOK = None diff --git a/pyproject.toml b/pyproject.toml index 8ae7573a0..e28624fad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ dependencies = [ "aiohttp[speedups]~=3.0", "coverage[toml]~=7.0", "flask[async]~=3.0", + "grpcio~=1.0", "httpx~=0.0", "mock~=5.0", "protobuf~=6.0", @@ -97,6 +98,7 @@ dependencies = [ devel-types = [ "mypy==1.16.1", "types-cffi~=1.0", + "types-grpcio~=1.0", "types-protobuf~=6.0", "types-requests~=2.0", ] From 8be66466172a9776c52dd32742781a9064cbec6f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 27 Jun 2025 13:00:53 +1000 Subject: [PATCH 0845/1376] docs(examples): add proto module documentation Signed-off-by: JP-Ellis --- examples/v3/plugins/proto/__init__.py | 100 +++++++++++++++++++++++ examples/v3/plugins/protobuf/__init__.py | 35 ++------ 2 files changed, 108 insertions(+), 27 deletions(-) diff --git a/examples/v3/plugins/proto/__init__.py b/examples/v3/plugins/proto/__init__.py index e69de29bb..8066dab26 100644 --- a/examples/v3/plugins/proto/__init__.py +++ b/examples/v3/plugins/proto/__init__.py @@ -0,0 +1,100 @@ +r""" +Protocol Buffers (protobuf) support for Pact Python examples. + +This module contains the generated Python code from the protobuf definition in +`person.proto`, which is used by both the `protobuf` and `grpc` examples. These +examples are designed to be pedagogical, demonstrating how to use Pact for +contract testing with Protocol Buffers and gRPC services. + +## What is Protocol Buffers (protobuf)? + +Protocol Buffers (protobuf) is Google's language-neutral, platform-neutral, +extensible mechanism for serializing structured data. Think of it like XML or +JSON, but smaller, faster, and simpler. You define your data structures in a +`.proto` file, and then use the protobuf compiler to generate classes in your +programming language of choice. + +For the purposes of our examples, we define `person.proto` which defines the +following messages: + +- `Person`: Represents a person with name, ID, email, and phone numbers +- `AddressBook`: A collection of Person objects +- Request/Response messages for service operations: + + - `GetPersonRequest/Response`: For retrieving a person by ID + - `ListPeopleRequest/Response`: For listing all people + - `AddPersonRequest/Response`: For adding a new person + +The file also defines a single service (useful for the gRPC example): + +- `AddressBookService`: Defines gRPC service methods for managing an address + book + +## Generated Files + +The `.proto` file is used by `protoc` and other tools to generate Python code. +The three files present in this directory can be generated using: + +```bash +python -m grpc_tools.protoc \ + -I. \ # (1) + --python_out=. \ # (2) + --pyi_out=. \ # (3) + --grpc_python_out=. \ # (4) + person.proto +``` + +1. `-I.` is used by the gRPC code generator to allow it to import the + `person_pb2` module. +2. `--python_out=.` specifies the output directory for the generated Python + code files. +3. `--pyi_out=.` specifies the output directory for the generated Python stub + files. +4. `--grpc_python_out=.` specifies the output directory for the generated gRPC + service files. + +### `person_pb2.py` + +**Purpose**: Contains the core protobuf message classes and serialization logic. + +**What it does**: + +- Defines Python classes for each protobuf message (Person, AddressBook, etc.) +- Provides methods for serializing objects to binary format + (`SerializeToString()`) +- Provides methods for deserializing binary data back to objects + (`ParseFromString()`) +- Handles all the low-level protobuf protocol details + +### `person_pb2.pyi` + +**Purpose**: Type stub file providing type hints for better IDE support and +static analysis. + +**What it does**: + +- Provides type annotations for all generated classes and methods +- Enables better autocomplete and type checking in IDEs like VSCode or PyCharm +- Helps static type checkers like mypy understand the generated code +- Makes the code more maintainable and less error-prone + +### `person_pb2_grpc.py` + +**Purpose**: Contains gRPC service client and server classes. + +**What it does**: + +- Defines client stub classes for calling gRPC services +- Defines server base classes for implementing gRPC services +- Handles the gRPC protocol layer (HTTP/2, streaming, etc.) +- Maps protobuf messages to gRPC method calls + +## Learning Resources + +For more information about Protocol Buffers and gRPC: + +- [Protocol Buffers + Tutorial](https://protobuf.dev/getting-started/pythontutorial/) +- [gRPC Python Tutorial](https://grpc.io/docs/languages/python/) +- [Pact gRPC/Protobuf Plugin](https://github.com/pactflow/pact-protobuf-plugin) +""" diff --git a/examples/v3/plugins/protobuf/__init__.py b/examples/v3/plugins/protobuf/__init__.py index a3d328123..13dbfbdbb 100644 --- a/examples/v3/plugins/protobuf/__init__.py +++ b/examples/v3/plugins/protobuf/__init__.py @@ -4,25 +4,11 @@ This module provides an example of how to use Pact plugins to handle different content types in contract testing. Specifically, this example demonstrates the use of the protobuf plugin to test interactions involving Protocol Buffers -(protobuf) message serialization, building on the [protobuf.dev Python -tutorial](https://protobuf.dev/getting-started/pythontutorial/). +(protobuf) message serialization. -## What are Protocol Buffers? - -Protocol Buffers (protobuf) is a language-neutral, platform-neutral extensible -mechanism for serializing structured data developed by Google. It can be thought -of as similar to XML or JSON, but with pre-defined schemas and a binary format -that is more efficient for both storage and transmission. - -The data structure is defined in a `.proto` file, which specifies the messages, -fields, and types. This is then compiled into source code in various programming -languages, allowing you to work with structured data in a type-safe manner. This -examples defines a simple address book and person schema within `person.proto` -and the `person.py` and `person.pyi` files have been generated from it using - -```console -protoc --python_out=. --pyi_out=. person.proto -``` +For detailed information about Protocol Buffers, the generated files, and the +domain model used in this example, see the [`proto`][examples.v3.plugins.proto] +module documentation. ## Pact and the Plugin Ecosystem @@ -41,16 +27,11 @@ - Provide specialized matching rules for different data formats - Enable extensibility without modifying the core Pact library -## The Protobuf Plugin Example - -This example builds an address book application using the same domain model as -the [protobuf.dev -tutorial](https://protobuf.dev/getting-started/pythontutorial/). +## This Example -It defines a simple address book schema using Protocol Buffers and demonstrates -how to use the Pact protobuf plugin to test interactions involving protobuf -messages. It is assumed that you have a basic understanding of Pact and Protocol -Buffers. +This example demonstrates how to use the Pact protobuf plugin to test +interactions involving protobuf messages sent over HTTP. It is assumed that you +have a basic understanding of Pact and Protocol Buffers. """ from ..proto.person_pb2 import AddressBook, Person From 6266a4f00dae2341072560e6e499474eb994d937 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 27 Jun 2025 13:07:38 +1000 Subject: [PATCH 0846/1376] docs: add protobuf and grpc links Signed-off-by: JP-Ellis --- mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index e1ee1ee23..b11e5387c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,6 +28,8 @@ plugins: python: import: - https://docs.python.org/3/objects.inv + - https://googleapis.dev/python/protobuf/latest/objects.inv + - https://grpc.github.io/grpc/python/objects.inv options: # General allow_inspection: true From 5584e32f906854194da5514cf8f4c267cd40b248 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 12:46:51 +0000 Subject: [PATCH 0847/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.0.6 (#1095) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cfe41fa17..9fd8c8303 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.0.2 + rev: v2.0.6 hooks: - id: biome-check additional_dependencies: From b97ecc281d1956a6545246495615f6584f9e7021 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 00:30:39 +0000 Subject: [PATCH 0848/1376] chore(deps): update taiki-e/install-action action to v2.54.3 (#1096) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 18b8dc869..984dd3334 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@9ba3ac3fd006a70c6e186a683577abc1ccf0ff3a # v2.54.0 + uses: taiki-e/install-action@a27ef18d36cfa66b0af3a360104621793b41c036 # v2.54.3 with: tool: git-cliff,typos From feac8ca7daa4d04c2a3d51a80febac2f0a575e2b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:57:39 +0000 Subject: [PATCH 0849/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.34.0 (#1097) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4590c006d..a50cc38b9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -327,7 +327,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@b1ae8d918b6e85bd611117d3d9a3be4f903ee5e4 # v1.33.1 + uses: crate-ci/typos@392b78fe18a52790c53f42456e46124f77346842 # v1.34.0 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9fd8c8303..f4951fe9a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,7 +76,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.33.1 + rev: v1.34.0 hooks: - id: typos From d56547ee0abe07f82f9d4f26f3bc8999f9e2c437 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 4 Jul 2025 09:17:04 +1000 Subject: [PATCH 0850/1376] chore(ci): update runners Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 984dd3334..bfd13991a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: build-sdist: name: Build source distribution - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -85,11 +85,11 @@ jobs: fail-fast: false matrix: include: - - os: ubuntu-22.04 + - os: ubuntu-latest archs: x86_64 - - os: macos-13 + - os: macos-13 # macOS 13 is the latest on x86_64 archs: x86_64 - - os: windows-2019 + - os: windows-latest archs: AMD64 steps: @@ -151,18 +151,18 @@ jobs: fail-fast: false matrix: include: - - os: ubuntu-22.04 + - os: ubuntu-latest archs: aarch64 build: manylinux - - os: ubuntu-22.04 + - os: ubuntu-latest archs: aarch64 build: musllinux - - os: macos-14 + - os: macos-latest archs: arm64 build: '' # TODO: Re-enable once the issues with Windows ARM64 are resolved.exclude: # See: pypa/cibuildwheel#1942 - # - os: windows-2019 + # - os: windows-latest # archs: ARM64 # build: "" @@ -208,7 +208,7 @@ jobs: if: >- github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/pact-python From ba63da28cadf895edef8994a4830aa7ded69e660 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 23:30:12 +0000 Subject: [PATCH 0851/1376] fix(deps): update ruff to v0.12.2 (#1098) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4951fe9a..55e11ece5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.1 + rev: v0.12.2 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index e28624fad..d61317557 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ dependencies = [ "pact-python[devel-docs]", "pact-python[devel-test]", "pact-python[devel-types]", - "ruff==0.12.1", + "ruff==0.12.2", ] devel-docs = [ "mkdocs-literate-nav~=0.6", From 54dfd761fb1097d6b5f83c2516db62cb2f60bd73 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 07:05:42 +0000 Subject: [PATCH 0852/1376] chore(deps): update taiki-e/install-action action to v2.56.7 (#1100) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bfd13991a..9160d5ec8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@a27ef18d36cfa66b0af3a360104621793b41c036 # v2.54.3 + uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7 with: tool: git-cliff,typos From 36ff0aaa23952f7a960d5448a190b5b045df3603 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 07:19:06 +0000 Subject: [PATCH 0853/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.1.1 (#1101) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55e11ece5..f440597a6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.0.6 + rev: v2.1.1 hooks: - id: biome-check additional_dependencies: From df624bebeb5718703f0f2b48a8777935eddf978b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 12:14:06 +0000 Subject: [PATCH 0854/1376] chore(deps): update pactfoundation/pact-broker:latest docker digest to 0106b1f (#1102) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a50cc38b9..a027b9389 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,7 +54,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:1cbd6145671095fba7b4d865f5bce8bc6db1248f32f9945fb2101e7d8d3ff15a + image: pactfoundation/pact-broker:latest@sha256:0106b1f233b8869c865bbcf75bc158148222fad0a44423c4b6ac5f47df12167d ports: - 9292:9292 env: @@ -187,7 +187,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:1cbd6145671095fba7b4d865f5bce8bc6db1248f32f9945fb2101e7d8d3ff15a + image: pactfoundation/pact-broker:latest@sha256:0106b1f233b8869c865bbcf75bc158148222fad0a44423c4b6ac5f47df12167d ports: - 9292:9292 env: From 671216ec27b34dc495464961056d2b11ffe68189 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:45:02 +0000 Subject: [PATCH 0855/1376] fix(deps): update ruff to v0.12.3 (#1104) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f440597a6..82bd53074 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.2 + rev: v0.12.3 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index d61317557..9d29a3e48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ dependencies = [ "pact-python[devel-docs]", "pact-python[devel-test]", "pact-python[devel-types]", - "ruff==0.12.2", + "ruff==0.12.3", ] devel-docs = [ "mkdocs-literate-nav~=0.6", From 45f30274776b82f8030a60eac1c9ba2c2551adc8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 03:16:00 +0000 Subject: [PATCH 0856/1376] chore(deps): update taiki-e/install-action action to v2.56.13 (#1105) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9160d5ec8..6e8e1f32a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7 + uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13 with: tool: git-cliff,typos From 1ce0a25d271a1e16d7ad4d8b5a43d6c5a40bfbe4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 20:38:25 +0000 Subject: [PATCH 0857/1376] fix(deps): update dependency mypy to v1.17.0 (#1106) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9d29a3e48..d4eb02061 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ dependencies = [ "uvicorn[standard]~=0.0", ] devel-types = [ - "mypy==1.16.1", + "mypy==1.17.0", "types-cffi~=1.0", "types-grpcio~=1.0", "types-protobuf~=6.0", From 95bafcd744dbd487f323697fcfcc946165a47a06 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 17 Jul 2025 18:41:00 +1000 Subject: [PATCH 0858/1376] chore: split mypy calls Mypy can sometimes get confused when checking everything at once. I have found more stability by separately checking the main subdirectories. Signed-off-by: JP-Ellis --- pyproject.toml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d4eb02061..a75b550db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -174,14 +174,17 @@ requires = [ installer = "uv" [tool.hatch.envs.default.scripts] - all = ["example", "format", "lint", "test", "typecheck"] - docs = "mkdocs serve {args}" - docs-build = "mkdocs build {args}" - example = "pytest --numprocesses=1 examples/ {args}" - format = "ruff format {args}" - lint = "ruff check --output-format=full --show-fixes {args}" - test = "pytest tests/ {args}" - typecheck = "mypy {args:.}" + all = ["example", "format", "lint", "test", "typecheck"] + docs = "mkdocs serve {args}" + docs-build = "mkdocs build {args}" + example = "pytest --numprocesses=1 examples/ {args}" + format = "ruff format {args}" + lint = "ruff check --output-format=full --show-fixes {args}" + test = "pytest tests/ {args}" + typecheck = ["typecheck-examples", "typecheck-src", "typecheck-tests"] + typecheck-examples = "mypy examples/ {args}" + typecheck-src = "mypy src/ {args}" + typecheck-tests = "mypy tests/ {args}" # Test environment for running unit tests. This automatically tests against all # supported Python versions. From 515cc300051e0be1af6167d3fbb7bda2faccc8d8 Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:41:04 +0000 Subject: [PATCH 0859/1376] docs: update changelog for v2.3.3 --- CHANGELOG.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 868f13efb..91d5bd227 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,66 @@ All notable changes to this project will be documented in this file. +## [2.3.3] _2025-07-17_ + +### 🚀 Features + +- _(v3)_ Add will_respond_with for sync + +### 🐛 Bug Fixes + +- _(v3)_ Avoid error if there's no mismatch type + +### 📚 Documentation + +- _(examples)_ Add proto module documentation +- Add protobuf and grpc links + +### ⚙️ Miscellaneous Tasks + +- _(ci)_ Remove pre-commit cache restore key +- Update biome +- _(examples)_ Add protobuf example +- Add version stub file +- _(examples)_ Parametrize protobuf example +- _(ci)_ Update runners +- Split mypy calls + +### Contributors + +- @JP-Ellis + +## [2.3.2] _2025-05-05_ + +### 🚀 Features + +- _(v3)_ [**breaking**] Allow more flexible functional arguments + > The signature of functional arguments must form a subset of the `MessageProducerArgs` and `StateHandlerArgs` typed dictionaries. + +### 📚 Documentation + +- Replace commitizen with git cliff +- Update blog post +- Rename params -> parameters +- _(example)_ Elaborate on state handler + +### ⚙️ Miscellaneous Tasks + +- Update pre-commit hooks +- Update committed configuration +- Add taplo +- _(ci)_ Update ubuntu runners +- Reduce noise from taiki-e/install-action +- _(ci)_ Upload test results to codecov +- Add apply_arg utility +- _(tests)_ Use consistent return value +- _(test)_ Tweak type signature +- _(examples)_ Fix state handler args + +### Contributors + +- @JP-Ellis + ## [2.3.1] _2025-01-22_ ### 🐛 Bug Fixes @@ -283,7 +343,6 @@ All notable changes to this project will be documented in this file. - _(test)_ Strip authentication from url - _(tests)_ Use long-lived pact broker - _(test)_ Apply a temporary diff to compatibility suite -- _(test)_ Refactor v1 bdd steps - _(test)_ Fix misspelling in step name - _(tests)_ Improve logging - _(tests)_ Allow multiple states with parameters @@ -1374,4 +1433,12 @@ All notable changes to this project will be documented in this file. - @bethesque - @ - +## [0.1.0] _2017-04-07_ + +### Contributors + +- @jslvtr +- @matthewbalvanz-wf +- @mefellows + + From 0a1253fdc3a52167379b11ff9c7ee871a997ec8e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:34:06 +0000 Subject: [PATCH 0860/1376] fix(deps): update ruff to v0.12.4 (#1110) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82bd53074..a5c8231f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.3 + rev: v0.12.4 hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the diff --git a/pyproject.toml b/pyproject.toml index a75b550db..e8850f392 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ dependencies = [ "pact-python[devel-docs]", "pact-python[devel-test]", "pact-python[devel-types]", - "ruff==0.12.3", + "ruff==0.12.4", ] devel-docs = [ "mkdocs-literate-nav~=0.6", From 136227b8c651391b11644da3da325e5c808fee6c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 18 Jul 2025 13:12:43 +1000 Subject: [PATCH 0861/1376] chore: update pre-commit hooks - Use the new `ruff-check` id - Don't pin the biome dependency now that the revisions match the biome version. Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a5c8231f0..ad5e76718 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,8 +38,6 @@ repos: rev: v2.1.1 hooks: - id: biome-check - additional_dependencies: - - '@biomejs/biome@2.0.4' - repo: https://github.com/ComPWA/taplo-pre-commit rev: v0.9.3 @@ -50,7 +48,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.12.4 hooks: - - id: ruff + - id: ruff-check # Exclude python files in pact/** and tests/**, except for the # files in src/pact/v3/** and tests/v3/**. exclude: ^(src/pact|tests)/(?!v3/).*\.py$ From b6c470a50a7efb0e44ad49d8f8c5a2ad930f80d5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 07:50:57 +0000 Subject: [PATCH 0862/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.1.2 (#1114) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad5e76718..653496991 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.1.1 + rev: v2.1.2 hooks: - id: biome-check From 13e260e59e3468752f2af8066549fb586c273099 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 07:51:12 +0000 Subject: [PATCH 0863/1376] chore(deps): update astral-sh/setup-uv action to v6.4.1 (#1111) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6e8e1f32a..c0c86d0db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5ad598a8b..5bedc9c38 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a027b9389..1e6661788 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,7 +90,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true cache-dependency-glob: | @@ -164,7 +164,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true cache-dependency-glob: | @@ -202,7 +202,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true cache-dependency-glob: | @@ -252,7 +252,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true cache-dependency-glob: | @@ -277,7 +277,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true cache-dependency-glob: | @@ -302,7 +302,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true cache-dependency-glob: | @@ -348,7 +348,7 @@ jobs: key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true cache-suffix: pre-commit From 932f7414b6186ffcd5317c8d58506d5ffa4dacb6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 10:54:37 +1000 Subject: [PATCH 0864/1376] chore(deps): update taiki-e/install-action action to v2.56.19 (#1115) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0c86d0db..a9a15f314 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,7 +232,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13 + uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19 with: tool: git-cliff,typos From 6b6477361f54d932dd76d7ce20235bc1dd1851dd Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 17 Jul 2025 10:47:58 +1000 Subject: [PATCH 0865/1376] chore: create cli and ffi packages Signed-off-by: JP-Ellis --- pact-python-cli/pyproject.toml | 10 ++++++++++ pact-python-ffi/pyproject.toml | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 pact-python-cli/pyproject.toml create mode 100644 pact-python-ffi/pyproject.toml diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml new file mode 100644 index 000000000..6bfb8cac0 --- /dev/null +++ b/pact-python-cli/pyproject.toml @@ -0,0 +1,10 @@ +#:schema https://json.schemastore.org/pyproject.json +[project] +description = "Pact CLI bundle for Python" +name = "pact-python-cli" + +license = "MIT" +version = "0.0.0" + +authors = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] +maintainers = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml new file mode 100644 index 000000000..e68713eea --- /dev/null +++ b/pact-python-ffi/pyproject.toml @@ -0,0 +1,10 @@ +#:schema https://json.schemastore.org/pyproject.json +[project] +description = "Python bindings for the Pact FFI library" +name = "pact-python-ffi" + +license = "MIT" +version = "0.0.0" + +authors = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] +maintainers = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] From 164532a8e091797bfcfb853c5767d29fd6faf4d1 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 17 Jul 2025 18:45:50 +1000 Subject: [PATCH 0866/1376] feat: create pact-python-cli package This package replaces the `pact.constants` module from prior versions. It merely stores the paths (as strings) to the bundled CLIs to facilitate calling them. Signed-off-by: JP-Ellis --- .gitignore | 1 - pact-python-cli/.gitignore | 3 + pact-python-cli/LICENSE | 21 +++ pact-python-cli/README.md | 32 ++++ pact-python-cli/hatch_build.py | 208 +++++++++++++++++++++++ pact-python-cli/pyproject.toml | 202 +++++++++++++++++++++- pact-python-cli/src/pact_cli/.gitkeep | 0 pact-python-cli/src/pact_cli/__init__.py | 151 ++++++++++++++++ pact-python-cli/src/pact_cli/py.typed | 0 pact-python-cli/tests/.ruff.toml | 10 ++ pact-python-cli/tests/test_init.py | 175 +++++++++++++++++++ 11 files changed, 801 insertions(+), 2 deletions(-) create mode 100644 pact-python-cli/.gitignore create mode 100644 pact-python-cli/LICENSE create mode 100644 pact-python-cli/README.md create mode 100644 pact-python-cli/hatch_build.py create mode 100644 pact-python-cli/src/pact_cli/.gitkeep create mode 100644 pact-python-cli/src/pact_cli/__init__.py create mode 100644 pact-python-cli/src/pact_cli/py.typed create mode 100644 pact-python-cli/tests/.ruff.toml create mode 100644 pact-python-cli/tests/test_init.py diff --git a/.gitignore b/.gitignore index 5ed2af480..463a6b940 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ ################################################################################ ## Pact Python Specific ################################################################################ -src/pact/bin src/pact/data # Test outputs diff --git a/pact-python-cli/.gitignore b/pact-python-cli/.gitignore new file mode 100644 index 000000000..ae1c5d9da --- /dev/null +++ b/pact-python-cli/.gitignore @@ -0,0 +1,3 @@ +src/pact_cli/bin +src/pact_cli/lib +src/pact_cli/data diff --git a/pact-python-cli/LICENSE b/pact-python-cli/LICENSE new file mode 100644 index 000000000..032bed571 --- /dev/null +++ b/pact-python-cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Pact Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pact-python-cli/README.md b/pact-python-cli/README.md new file mode 100644 index 000000000..c813ff176 --- /dev/null +++ b/pact-python-cli/README.md @@ -0,0 +1,32 @@ + +# Pact Python CLI + +> [!NOTE] +> +> This package is used to package and bundle the Pact CLI _only_. It does not provide any Python functionality or API. + +--- + +This sub-package is part of the [Pact Python](https://github.com/pact-foundation/pact-python) project and exists solely to distribute the [Pact CLI](https://github.com/pact-foundation/pact-ruby-standalone) as a Python package. If you are looking for the main Pact Python library for contract testing, please see the [root package](https://github.com/pact-foundation/pact-python#pact-python). + +It is used by version 2 of Pact Python, and can be used to install the Pact CLI in Python environments. + +## Installation + +You can install this package via pip: + +```console +pip install pact-python-cli +``` + +## Contributing + +Contributions to this package are generally not required as it contains minimal Python functionality and generally only requires updating the version number. To contribute to the Pact CLI itself, please refer to the [Pact Ruby Standalone repository](https://github.com/pact-foundation/pact-ruby-standalone). + +For contributing to Pact Python, see the [main contributing guide](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md). + +--- + +For questions or support, please visit the [Pact Foundation Slack](https://slack.pact.io) or [GitHub Discussions](https://github.com/pact-foundation/pact-python/discussions). + +--- diff --git a/pact-python-cli/hatch_build.py b/pact-python-cli/hatch_build.py new file mode 100644 index 000000000..c2284e534 --- /dev/null +++ b/pact-python-cli/hatch_build.py @@ -0,0 +1,208 @@ +""" +Hatchling build hook for binary downloads. + +Pact Python is built on top of the Ruby Pact binaries and the Rust Pact library. +This build script downloads the binaries and library for the current platform +and installs them in the `pact` directory under `/bin` and `/lib`. + +The version of the binaries and library can be controlled with the +`PACT_BIN_VERSION` and `PACT_LIB_VERSION` environment variables. If these are +not set, a pinned version will be used instead. +""" + +from __future__ import annotations + +import logging +import shutil +import tarfile +import tempfile +import zipfile +from pathlib import Path +from typing import Any + +import requests +from hatchling.builders.hooks.plugin.interface import BuildHookInterface +from packaging.tags import sys_tags + +logger = logging.getLogger(__name__) + +PKG_DIR = Path(__file__).parent.resolve() / "src" / "pact_cli" + +# Latest version available at: +# https://github.com/pact-foundation/pact-ruby-standalone/releases +PACT_BIN_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" + + +class UnsupportedPlatformError(RuntimeError): + """Raised when the current platform is not supported.""" + + def __init__(self, platform: str) -> None: + """ + Initialize the exception. + + Args: + platform: The unsupported platform. + """ + self.platform = platform + super().__init__(f"Unsupported platform {platform}") + + +class PactBuildHook(BuildHookInterface[Any]): + """Custom hook to download Pact binaries.""" + + PLUGIN_NAME = "custom" + """ + This is a hard-coded name required by Hatch + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 + """ + Initialize the build hook. + + For this hook, we additionally define the lib extension based on the + current platform. + """ + super().__init__(*args, **kwargs) + self.tmpdir = Path(tempfile.TemporaryDirectory().name) + self.tmpdir.mkdir(parents=True, exist_ok=True) + + 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(PKG_DIR / 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 + + cli_version = ".".join(self.metadata.version.split(".")[:3]) + if not cli_version: + self.app.display_error("Failed to determine Pact CLI version.") + + try: + self.pact_bin_install(cli_version) + except UnsupportedPlatformError as err: + msg = f"Pact CLI is not available for {err.platform}." + logger.exception(msg, RuntimeWarning, stacklevel=2) + + def pact_bin_install(self, version: str) -> None: + """ + Install the Pact standalone binaries. + + The binaries are installed in `src/pact/bin`, and the relevant version for + the current operating system is determined automatically. + + Args: + version: The Pact version to install. + """ + url = self._pact_bin_url(version) + if url: + artifact = self._download(url) + self._pact_bin_extract(artifact) + + def _pact_bin_url(self, version: str) -> str | None: + """ + Generate the download URL for the Pact binaries. + + Generate the download URL for the Pact binaries based on the current + platform and specified version. This function mainly contains a lot of + matching logic to determine the correct URL to use, due to the + inconsistencies in naming conventions between ecosystems. + + Args: + version: The upstream Pact version. + + Returns: + The URL to download the Pact binaries from, or None if the current + platform is not supported. + """ + platform = next(sys_tags()).platform + + if platform.startswith("macosx"): + os = "osx" + ext = "tar.gz" + elif "linux" in platform: + os = "linux" + ext = "tar.gz" + elif platform.startswith("win"): + os = "windows" + ext = "zip" + else: + raise UnsupportedPlatformError(platform) + + if platform.endswith(("arm64", "aarch64")): + machine = "arm64" + elif platform.endswith(("x86_64", "amd64")): + machine = "x86_64" + elif platform.endswith(("i386", "i686", "x86", "win32")): + machine = "x86" + else: + raise UnsupportedPlatformError(platform) + + return PACT_BIN_URL.format( + version=version, + os=os, + machine=machine, + ext=ext, + ) + + def _pact_bin_extract(self, artifact: Path) -> None: + """ + Extract the Pact binaries. + + The binaries in the `bin` directory require the underlying Ruby runtime + to be present, which is included in the `lib` directory. + + Args: + artifact: The path to the downloaded artifact. + """ + with tempfile.TemporaryDirectory() as tmpdir: + if str(artifact).endswith(".zip"): + with zipfile.ZipFile(artifact) as f: + f.extractall(tmpdir) # noqa: S202 + + if str(artifact).endswith(".tar.gz"): + with tarfile.open(artifact) as f: + f.extractall(tmpdir) # noqa: S202 + + for d in ["bin", "lib"]: + if (PKG_DIR / d).is_dir(): + shutil.rmtree(PKG_DIR / d) + shutil.copytree( + Path(tmpdir) / "pact" / d, + PKG_DIR / d, + ) + + def _download(self, url: str) -> Path: + """ + Download the target URL. + + This will download the target URL to the `pact/data` directory. If the + download artifact is already present, its path will be returned. + + Args: + url: The URL to download + + Return: + The path to the downloaded artifact. + """ + filename = url.split("/")[-1] + artifact = PKG_DIR / "data" / filename + artifact.parent.mkdir(parents=True, exist_ok=True) + + if not artifact.exists(): + response = requests.get(url, timeout=30) + try: + response.raise_for_status() + except requests.HTTPError as e: + msg = f"Failed to download from {url}." + raise RuntimeError(msg) from e + with artifact.open("wb") as f: + f.write(response.content) + + return artifact diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 6bfb8cac0..02faeaaff 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -4,7 +4,207 @@ description = "Pact CLI bundle for Python" name = "pact-python-cli" license = "MIT" -version = "0.0.0" + +# The first three segments of the version correspond to the Pact CLI version. +# The last segment is incremented if there is a need to update the Python +# package without changing the Pact CLI version. +# +# For the latest version, see: +# https://github.com/pact-foundation/pact-ruby-standalone/releases +version = "2.4.26.0" authors = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] maintainers = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] + +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 :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python", + "Topic :: Software Development :: Testing", +] + +requires-python = ">=3.9" + + [project.urls] + "Bug Tracker" = "https://github.com/pact-foundation/pact-python/issues" + "Changelog" = "https://github.com/pact-foundation/pact-python/blob/main/pact-python-cli/CHANGELOG.md" + "Documentation" = "https://docs.pact.io" + "Homepage" = "https://pact.io" + "Repository" = "https://github.com/pact-foundation/pact-python" + + [project.scripts] + pact = "pact_cli:_exec" + pact-broker = "pact_cli:_exec" + pact-message = "pact_cli:_exec" + pact-mock-service = "pact_cli:_exec" + pact-plugin-cli = "pact_cli:_exec" + pact-provider-verifier = "pact_cli:_exec" + pact-stub-service = "pact_cli:_exec" + pactflow = "pact_cli:_exec" + + [project.optional-dependencies] + # Linting and formatting tools use a more narrow specification to ensure + # developper consistency. All other dependencies are as above. + devel = [ + "pact-python-cli[devel-test]", + "pact-python-cli[devel-types]", + "ruff==0.12.4", + ] + devel-test = ["pytest-cov~=6.0", "pytest-mock~=3.0", "pytest~=8.0"] + devel-types = ["mypy==1.17.0"] + +################################################################################ +## Build System +################################################################################ +[build-system] +build-backend = "hatchling.build" +requires = [ + "hatchling", + "packaging", + "requests", + "setuptools ; python_version >= '3.12'", +] + +[tool.hatch] + + [tool.hatch.build] + + [tool.hatch.build.targets.sdist] + include = [ + # Source + "/src/pact_cli/**/*.py", + "/src/pact_cli/**/*.pyi", + "/src/pact_cli/**/py.typed", + + # Metadata + "*.md", + "LICENSE", + ] + + [tool.hatch.build.targets.wheel] + artifacts = ["/src/pact_cli/bin/*", "/src/pact_cli/lib/*"] + include = [ + # Source + "/src/pact_cli/**/*.py", + "/src/pact_cli/**/*.pyi", + "/src/pact_cli/**/py.typed", + ] + packages = ["/src/pact_cli"] + + [tool.hatch.build.targets.wheel.hooks.custom] + + ######################################## + ## Hatch Environment Configuration + ######################################## + [tool.hatch.envs] + + # Install dev dependencies in the default environment to simplify the developer + # workflow. + [tool.hatch.envs.default] + extra-dependencies = [ + "hatchling", + "packaging", + "requests", + "setuptools ; python_version >= '3.12'", + ] + features = ["devel"] + installer = "uv" + + [tool.hatch.envs.default.scripts] + all = ["format", "lint", "test", "typecheck"] + format = "ruff format {args}" + lint = "ruff check --show-fixes {args}" + test = "pytest tests/ {args}" + typecheck = ["typecheck-src", "typecheck-tests"] + typecheck-src = "mypy src/ {args}" + typecheck-tests = "mypy tests/ {args}" + + # Test environment for running unit tests. This automatically tests against all + # supported Python versions. + [tool.hatch.envs.test] + features = ["devel-test"] + installer = "uv" + + [[tool.hatch.envs.test.matrix]] + python = ["3.10", "3.11", "3.12", "3.13", "3.9"] + +################################################################################ +## PyTest Configuration +################################################################################ +[tool.pytest] + + [tool.pytest.ini_options] + addopts = [ + "--import-mode=importlib", + # Coverage options + "--cov-config=pyproject.toml", + "--cov-report=xml", + "--cov=pact_cli", + ] + + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" + +################################################################################ +## Coverage +################################################################################ +[tool.coverage] + + [tool.coverage.paths] + pact-cli = ["/src/pact_cli"] + tests = ["/tests"] + + [tool.coverage.report] + exclude_lines = [ + "@(abc\\.)?abstractmethod", # Ignore abstract methods + "if TYPE_CHECKING:", # Ignore typing + "if __name__ == .__main__.:", # Ignore non-runnable code + "raise NotImplementedError", # Ignore defensive assertions + ] + +################################################################################ +## Ruff Configuration +################################################################################ +[tool.ruff] +extend = "../pyproject.toml" + +exclude = [] + +################################################################################ +## Mypy Configuration +################################################################################ +[tool.mypy] +# Overwrite the exclusions from the root pyproject.toml. +exclude = '' + +################################################################################ +## CI Build Wheel +################################################################################ +[tool.cibuildwheel] +before-build = "rm -rvf src/pact_cli/{bin,data,lib}" +skip = "pp*" +test-command = "pytest" + + [tool.cibuildwheel.macos] + # The repair tool unfortunately did not like the bundled Ruby distributable. + # TODO: Check whether delocate-wheel can be configured. + repair-wheel-command = "" + + [tool.cibuildwheel.windows] + before-build = [ + 'IF EXIST src\pact_cli\bin\ RMDIR /S /Q src\pact_cli\bin', + 'IF EXIST src\pact_cli\data\ RMDIR /S /Q src\pact_cli\data', + 'IF EXIST src\pact_cli\lib\ RMDIR /S /Q src\pact_cli\lib', + ] diff --git a/pact-python-cli/src/pact_cli/.gitkeep b/pact-python-cli/src/pact_cli/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/pact-python-cli/src/pact_cli/__init__.py b/pact-python-cli/src/pact_cli/__init__.py new file mode 100644 index 000000000..91eb27f2a --- /dev/null +++ b/pact-python-cli/src/pact_cli/__init__.py @@ -0,0 +1,151 @@ +""" +Locate and provide paths to bundled or system Pact CLI executables. + +This module exists solely to bundle and distribute the Pact CLI tools as a +Python package. It does not provide any substantive functionality beyond +locating the Pact CLI executables and providing their paths for use in Python +code. + +The module exposes constants for the absolute paths to the Pact CLI executables (such as +`pact-broker`, `pact-message`, `pact-mock-service`, and `pact-provider-verifier`). + +By default, the module will use the binaries bundled with the package. If the +environment variable `PACT_USE_SYSTEM_BINS` is set to `TRUE` or `YES`, or if the bundled +binaries are unavailable, it will fall back to using the system-installed Pact CLI tools +if found in the system PATH. + +This package is intended for use as a dependency to ensure the Pact CLI is available in +Python environments, such as CI pipelines or local development, without requiring a +separate installation step. + +For more information, see the project README or +https://github.com/pact-foundation/pact-python. +""" + +from __future__ import annotations + +import os +import shutil +import warnings +from pathlib import Path + +_USE_SYSTEM_BINS = os.getenv("PACT_USE_SYSTEM_BINS", "").upper() in ("TRUE", "YES") +_BIN_DIR = Path(__file__).parent.resolve() / "bin" + + +def _exec() -> None: + """ + A minimal wrapper to execute the Pact CLI tools. + + This is designed to be used as an entry point the scripts defined in the + `pyproject.toml` file. It simply passes the command line arguments to the + appropriate Pact CLI tool based on the executable name. + + """ + import sys # noqa: PLC0415 + + command = Path(sys.argv[0]).name + args = sys.argv[1:] if len(sys.argv) > 1 else [] + + if command not in ( + "pact", + "pact-broker", + "pact-message", + "pact-mock-service", + "pact-provider-verifier", + "pact-plugin-cli", + "pact-publish", + "pact-stub-service", + "pactflow", + ): + print("Unknown command:", command, file=sys.stderr) # noqa: T201 + sys.exit(1) + + if not _USE_SYSTEM_BINS: + executable = _find_executable(command) + else: + # To avoid finding the same executable, we have to process the PATH + # variable and remove the current executable's directory. + script_dir = Path(sys.argv[0]).parent.resolve() + os.environ["PATH"] = os.pathsep.join( + p + for p in os.getenv("PATH", "").split(os.pathsep) + if Path(p).resolve() != script_dir + ) + executable = _find_executable(command) + + if not executable: + print(f"Command '{command}' not found.", file=sys.stderr) # noqa: T201 + sys.exit(1) + + os.execv(executable, [executable, *args]) # noqa: S606 + + +def _find_executable(executable: str) -> str | None: + """ + Find the path to an executable. + + This inspects the environment variable `PACT_USE_SYSTEM_BINS` to determine + whether to use the bundled Pact binaries or the system ones. Note that if + the local executables are not found, this will fall back to the system + executables (if found). + + Args: + executable: + The name of the executable to find without the extension. Python + will automatically append the correct extension for the current + platform. + + Returns: + The absolute path to the executable. + + Warns: + RuntimeWarning: + If the executable cannot be found in the system path. + """ + if _USE_SYSTEM_BINS: + bin_path = shutil.which(executable) + else: + bin_path = shutil.which(executable, path=_BIN_DIR) + if bin_path is None: + msg = f"Unable to find {executable} binary executable." + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return bin_path + + +PACT_PATH = _find_executable("pact") +""" +Path to the Pact executable +""" +BROKER_PATH = _find_executable("pact-broker") +""" +Path to the Pact Broker executable +""" +BROKER_CLIENT_PATH = _find_executable("pact-broker") +""" +Path to the Pact Broker executable +""" +MESSAGE_PATH = _find_executable("pact-message") +""" +Path to the Pact Message executable +""" +MOCK_SERVICE_PATH = _find_executable("pact-mock-service") +""" +Path to the Pact Mock Service executable +""" +PLUGIN_CLI_PATH = _find_executable("pact-plugin-cli") +""" +Path to the Pact Plugin CLI executable +""" +VERIFIER_PATH = _find_executable("pact-provider-verifier") +""" +Path to the Pact Provider Verifier executable +""" +STUB_SERVICE_PATH = _find_executable("pact-stub-service") +""" +Path to the Pact Stub Service executable +""" +PACTFLOW_PATH = _find_executable("pactflow") +""" +Path to the PactFlow CLI executable +""" diff --git a/pact-python-cli/src/pact_cli/py.typed b/pact-python-cli/src/pact_cli/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/pact-python-cli/tests/.ruff.toml b/pact-python-cli/tests/.ruff.toml new file mode 100644 index 000000000..54c8b9dfc --- /dev/null +++ b/pact-python-cli/tests/.ruff.toml @@ -0,0 +1,10 @@ +#:schema https://json.schemastore.org/ruff.json +extend = "../pyproject.toml" + +[lint] +ignore = [ + "D103", # Require docstrings on public functions + "INP001", # Forbid implicit namespaces + "PLR2004", # Forbid magic numbers + "S101", # Disable assert +] diff --git a/pact-python-cli/tests/test_init.py b/pact-python-cli/tests/test_init.py new file mode 100644 index 000000000..32999c2ce --- /dev/null +++ b/pact-python-cli/tests/test_init.py @@ -0,0 +1,175 @@ +"""Test the values in exported constants.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +import time +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +import pact_cli + +if TYPE_CHECKING: + from unittest.mock import MagicMock + + import pytest_mock + + +def bin_to_sitepackages(exec_path: str | Path) -> Path: + """ + Find the expected site-packages directory for the given executable. + """ + if os.name == "nt": + return Path(exec_path).parents[1] / "Lib" / "site-packages" + return ( + Path(exec_path).parents[1] + / "lib" + / f"python{sys.version_info.major}.{sys.version_info.minor}" + / "site-packages" + ) + + +def assert_in_sys_path(p: str | Path) -> None: + """ + Assert that a given path is in sys.path. + + This performs some normalization on platform where the filesystem is + case-insensitive. + """ + if os.name == "nt": + assert str(p).lower() in (path.lower() for path in sys.path) + else: + assert str(p) in sys.path + + +@pytest.mark.parametrize( + ("constant", "expected"), + [ + pytest.param("PACT_PATH", "pact", id="pact"), + pytest.param("BROKER_PATH", "pact-broker", id="pact-broker"), + pytest.param("BROKER_CLIENT_PATH", "pact-broker", id="pact-broker"), + pytest.param("MESSAGE_PATH", "pact-message", id="pactmessage"), + pytest.param("MOCK_SERVICE_PATH", "pact-mock-service", id="pact-message"), + pytest.param("PLUGIN_CLI_PATH", "pact-plugin-cli", id="pact-plugin-cli"), + pytest.param( + "VERIFIER_PATH", "pact-provider-verifier", id="pact-provider-verifier" + ), + pytest.param("STUB_SERVICE_PATH", "pact-stub-service", id="pact-stub-service"), + pytest.param("PACTFLOW_PATH", "pactflow", id="pactflow"), + ], +) +def test_constants(constant: str, expected: str) -> None: + """Test the values of constants in pact.constants.""" + value: str = getattr(pact_cli, constant) + if os.name == "nt": + # As the Windows filesystem is case insensitive, we must normalize it. + assert value.lower().endswith((f"{expected}.bat", f"{expected}.exe")) + else: + assert value.endswith(expected) + + +@pytest.mark.parametrize( + "executable", + [ + pytest.param("pact", id="pact"), + pytest.param("pact-broker", id="pact-broker"), + pytest.param("pact-message", id="pact-message"), + pytest.param("pact-plugin-cli", id="pact-plugin-cli"), + pytest.param("pact-provider-verifier", id="pact-provider-verifier"), + pytest.param("pact-stub-service", id="pact-stub-service"), + pytest.param("pactflow", id="pactflow"), + ], +) +def test_exec_wrapper(executable: str) -> None: + exec_path = shutil.which(executable) + assert exec_path + + site_packages = bin_to_sitepackages(exec_path) + assert site_packages.is_dir() + assert_in_sys_path(site_packages) + + result = subprocess.run( # noqa: S603 + [exec_path, "--help"], + check=False, # Some CLIs return non-zero for --help + text=True, + capture_output=True, + ) + assert "pact" in (result.stdout + result.stderr).lower() + + result = subprocess.run( # noqa: S603 + [exec_path], + check=False, # Some CLIs return non-zero for --help + text=True, + capture_output=True, + ) + assert "pact" in (result.stdout + result.stderr).lower() + + +def test_exec_wrapper_mock_service() -> None: + """ + Analogous to test_exec_wrapper, but specifically for pact-mock-service. + + This is necessary because pact-mock-service is a long running service, so we + spawn the process, terminate it after a delay, and check the output. + """ + executable = "pact-mock-service" + exec_path = shutil.which(executable) + assert exec_path + site_packages = bin_to_sitepackages(exec_path) + assert site_packages.is_dir() + assert_in_sys_path(site_packages) + + process = subprocess.Popen( # noqa: S603 + [exec_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + time.sleep(1) + process.terminate() + process.wait() + + stdout, stderr = process.communicate() + assert "pact" in (stdout + stderr).lower() + + +@pytest.mark.parametrize( + "executable", + [ + pytest.param("pact", id="pact"), + pytest.param("pact-broker", id="pact-broker"), + pytest.param("pact-message", id="pact-message"), + pytest.param("pact-mock-service", id="pact-mock-service"), + pytest.param("pact-plugin-cli", id="pact-plugin-cli"), + pytest.param("pact-provider-verifier", id="pact-provider-verifier"), + pytest.param("pact-stub-service", id="pact-stub-service"), + pytest.param("pactflow", id="pactflow"), + ], +) +def test_exec_directly(executable: str, mocker: pytest_mock.MockerFixture) -> None: + """ + Test pact_cli._exec with --help, mocking sys.argv and capturing output. + """ + cmd: str + args: list[str] + + mocker.patch.object(sys, "argv", new=[executable, "--help"]) + mock_execv: MagicMock = mocker.patch("os.execv") + pact_cli._exec() # noqa: SLF001 + mock_execv.assert_called_once() + cmd, args = mock_execv.call_args[0] + assert (os.sep + executable) in cmd + assert args == [cmd, "--help"] + + mocker.patch.object(sys, "argv", new=[executable]) + mock_execv.reset_mock() + pact_cli._exec() # noqa: SLF001 + mock_execv.assert_called_once() + cmd, args = mock_execv.call_args[0] + assert (os.sep + executable) in cmd + assert args == [cmd] From 045722d47c0bbaa69c628aec6893ccf12c90d120 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 17 Jul 2025 18:48:42 +1000 Subject: [PATCH 0867/1376] chore: use the new `pact_cli` package With the binaries now bundled in `pact_cli`, the `pact.constants` module can be effectively deprecated. To preserve backwards compatibility, the existing constant reference the appropriate `pact_cli` constant. Signed-off-by: JP-Ellis --- pyproject.toml | 35 +++++++++---- src/pact/constants.py | 52 +++---------------- src/pact/verify_wrapper.py | 2 +- tests/test_broker.py | 2 +- tests/test_message_pact.py | 2 +- tests/test_pact.py | 2 +- tests/test_verify_wrapper.py | 2 +- tests/v3/compatibility_suite/util/provider.py | 32 +++--------- 8 files changed, 41 insertions(+), 88 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e8850f392..61cd58f86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,15 +40,18 @@ requires-python = ">=3.9" # - A specific feature is required in a new minor release # - A minor version address vulnerability which directly impacts Pact Python dependencies = [ - "cffi ~=1.0", - "click ~=8.0", - "fastapi ~=0.0", - "psutil ~=7.0", - "requests ~=2.0", - "six ~=1.0", - "typing-extensions ~=4.0 ; python_version < '3.10'", - "uvicorn ~=0.0", - "yarl ~=1.0", + # Pact dependencies + "pact-python-cli~=2.0", + # All other dependencies + "cffi~=1.0", + "click~=8.0", + "fastapi~=0.0", + "psutil~=7.0", + "requests~=2.0", + "six~=1.0", + "typing-extensions~=4.0 ; python_version < '3.10'", + "uvicorn~=0.0", + "yarl~=1.0", ] [project.urls] @@ -65,6 +68,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. devel = [ + "pact-python-cli[devel]", "pact-python[devel-docs]", "pact-python[devel-test]", "pact-python[devel-types]", @@ -195,6 +199,16 @@ requires = [ [[tool.hatch.envs.test.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.9"] +################################################################################ +## UV Workspace +################################################################################ +[tool.uv] + [tool.uv.sources] + pact-python-cli = { workspace = true } + + [tool.uv.workspace] + members = ["pact-python-cli"] + ################################################################################ ## PyTest Configuration ################################################################################ @@ -272,7 +286,6 @@ extend-exclude = [ "src/pact/__version__.py", "src/pact/broker.py", "src/pact/cli/*.py", - "src/pact/constants.py", "src/pact/consumer.py", "src/pact/http_proxy.py", "src/pact/matchers.py", @@ -321,7 +334,7 @@ extend-exclude = [ convention = "google" [tool.ruff.lint.isort] - known-first-party = ["pact"] + known-first-party = ["pact", "pact_cli"] [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" diff --git a/src/pact/constants.py b/src/pact/constants.py index f06a4d24f..2caae7e84 100644 --- a/src/pact/constants.py +++ b/src/pact/constants.py @@ -1,52 +1,12 @@ """ Constant values for the pact-python package. -This will default to the bundled Pact binaries bundled with the package, but -should these be unavailable or the environment variable `PACT_USE_SYSTEM_BINS` is -set to `TRUE` or `YES`, the system Pact binaries will be used instead. +The constants in this module are simply re-exported from the `pact_cli` module. """ -import os -import shutil -import warnings -from pathlib import Path -_USE_SYSTEM_BINS = os.getenv("PACT_USE_SYSTEM_BINS", "").upper() in ("TRUE", "YES") -_BIN_DIR = Path(__file__).parent.resolve() / "bin" +import pact_cli - -def _find_executable(executable: str) -> str: - """ - Find the path to an executable. - - This inspects the environment variable `PACT_USE_SYSTEM_BINS` to determine - whether to use the bundled Pact binaries or the system ones. Note that if - the local executables are not found, this will fall back to the system - executables (if found). - - Args: - executable: - The name of the executable to find without the extension. Python - will automatically append the correct extension for the current - platform. - - Returns: - The absolute path to the executable. - - Warns: - RuntimeWarning: - If the executable cannot be found in the system path. - """ - if _USE_SYSTEM_BINS: - bin_path = shutil.which(executable) - else: - bin_path = shutil.which(executable, path=_BIN_DIR) or shutil.which(executable) - if bin_path is None: - msg = f"Unable to find {executable} binary executable." - warnings.warn(msg, RuntimeWarning, stacklevel=2) - return bin_path or "" - - -BROKER_CLIENT_PATH = _find_executable("pact-broker") -MESSAGE_PATH = _find_executable("pact-message") -MOCK_SERVICE_PATH = _find_executable("pact-mock-service") -VERIFIER_PATH = _find_executable("pact-provider-verifier") +BROKER_CLIENT_PATH = pact_cli.BROKER_CLIENT_PATH or "" +MESSAGE_PATH = pact_cli.MESSAGE_PATH or "" +MOCK_SERVICE_PATH = pact_cli.MOCK_SERVICE_PATH or "" +VERIFIER_PATH = pact_cli.VERIFIER_PATH or "" diff --git a/src/pact/verify_wrapper.py b/src/pact/verify_wrapper.py index 71da1d02e..64f0c2adf 100644 --- a/src/pact/verify_wrapper.py +++ b/src/pact/verify_wrapper.py @@ -1,7 +1,7 @@ """Wrapper to verify previously created pacts.""" import warnings -from pact.constants import VERIFIER_PATH +from pact_cli import VERIFIER_PATH import sys import os import platform diff --git a/tests/test_broker.py b/tests/test_broker.py index 0cbdc3566..fe8cbb925 100644 --- a/tests/test_broker.py +++ b/tests/test_broker.py @@ -5,7 +5,7 @@ from pact.broker import Broker from pact.consumer import Consumer, Provider -from pact.constants import BROKER_CLIENT_PATH +from pact_cli import BROKER_CLIENT_PATH from pact import broker as broker diff --git a/tests/test_message_pact.py b/tests/test_message_pact.py index 6244735d8..aa0516b76 100644 --- a/tests/test_message_pact.py +++ b/tests/test_message_pact.py @@ -6,7 +6,7 @@ from pact.message_consumer import MessageConsumer, Provider from pact.message_pact import MessagePact -from pact.constants import MESSAGE_PATH +from pact_cli import MESSAGE_PATH from pact import message_pact as message_pact from pact import Term diff --git a/tests/test_pact.py b/tests/test_pact.py index 7316f1cdb..114faebf9 100644 --- a/tests/test_pact.py +++ b/tests/test_pact.py @@ -8,7 +8,7 @@ from pact.broker import Broker from pact.consumer import Consumer, Provider from pact.matchers import Term -from pact.constants import MOCK_SERVICE_PATH +from pact_cli import MOCK_SERVICE_PATH from pact.pact import Pact, FromTerms, Request, Response from pact import pact as pact from pact.verify_wrapper import PactException diff --git a/tests/test_verify_wrapper.py b/tests/test_verify_wrapper.py index 8ec63eaf7..909edc79c 100644 --- a/tests/test_verify_wrapper.py +++ b/tests/test_verify_wrapper.py @@ -3,7 +3,7 @@ from mock import patch, Mock, call -from pact.constants import VERIFIER_PATH +from pact_cli import VERIFIER_PATH from pact.verify_wrapper import VerifyWrapper, PactException, path_exists, sanitize_logs, expand_directories, rerun_command from pact import verify_wrapper diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 7473cc9ef..97a17b42c 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -18,9 +18,7 @@ import inspect import json import logging -import os import re -import shutil import subprocess import warnings from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer @@ -36,7 +34,7 @@ from typing_extensions import Self from yarl import URL -import pact.constants # type: ignore[import-untyped] +import pact_cli from pact import __version__ from pact.v3._server import MessageProducer from pact.v3._util import find_free_port @@ -343,29 +341,11 @@ def __init__( self.provider = provider self.consumer = consumer - self.broker_bin: str = ( - shutil.which("pact-broker") or pact.constants.BROKER_CLIENT_PATH - ) - if not self.broker_bin: - if "CI" in os.environ: - self._install() - bin_path = shutil.which("pact-broker") - assert bin_path, "pact-broker not found" - self.broker_bin = bin_path - else: - msg = "pact-broker not found" - raise RuntimeError(msg) - - def _install(self) -> None: - """ - Install the Pact Broker CLI tool. - - This function is intended to be run in CI environments, where the pact-broker - CLI tool may not be installed already. This will download and extract - the tool - """ - msg = "pact-broker not found" - raise NotImplementedError(msg) + if bin_path := pact_cli.BROKER_CLIENT_PATH: + self.broker_bin = bin_path + else: + msg = "pact-broker not found" + raise RuntimeError(msg) def reset(self) -> None: """ From ec6b5161c5c146fb925454df12d7a8ba2923e27e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 17 Jul 2025 20:44:36 +1000 Subject: [PATCH 0868/1376] chore: remove packaging of pact cli Signed-off-by: JP-Ellis --- hatch_build.py | 122 ------------------------------------------------- 1 file changed, 122 deletions(-) diff --git a/hatch_build.py b/hatch_build.py index 7960f5ffa..e472ec9d3 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -15,10 +15,8 @@ import gzip import os import shutil -import tarfile import tempfile import warnings -import zipfile from pathlib import Path from typing import Any @@ -29,11 +27,6 @@ PACT_ROOT_DIR = Path(__file__).parent.resolve() / "src" / "pact" -# Latest version available at: -# https://github.com/pact-foundation/pact-ruby-standalone/releases -PACT_BIN_VERSION = os.getenv("PACT_BIN_VERSION", "2.4.1") -PACT_BIN_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" - # Latest version available at: # https://github.com/pact-foundation/pact-reference/releases PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.22") @@ -90,13 +83,6 @@ def initialize( build_data["pure_python"] = False binaries_included = False - try: - self.pact_bin_install(PACT_BIN_VERSION) - binaries_included = True - except UnsupportedPlatformError as err: - msg = f"Pact binaries on not available for {err.platform}." - warnings.warn(msg, RuntimeWarning, stacklevel=2) - try: self.pact_lib_install(PACT_LIB_VERSION) binaries_included = True @@ -108,114 +94,6 @@ def initialize( msg = "Wheel does not include any binaries. Aborting." raise UnsupportedPlatformError(msg) - def pact_bin_install(self, version: str) -> None: - """ - Install the Pact standalone binaries. - - The binaries are installed in `src/pact/bin`, and the relevant version for - the current operating system is determined automatically. - - Args: - version: The Pact version to install. - """ - url = self._pact_bin_url(version) - if url: - artifact = self._download(url) - self._pact_bin_extract(artifact) - - def _pact_bin_url(self, version: str) -> str | None: - """ - Generate the download URL for the Pact binaries. - - Generate the download URL for the Pact binaries based on the current - platform and specified version. This function mainly contains a lot of - matching logic to determine the correct URL to use, due to the - inconsistencies in naming conventions between ecosystems. - - Args: - version: The upstream Pact version. - - Returns: - The URL to download the Pact binaries from, or None if the current - platform is not supported. - """ - platform = 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: - raise UnsupportedPlatformError(platform) - return PACT_BIN_URL.format( - version=version, - os=os, - machine=machine, - ext="tar.gz", - ) - - if platform.startswith("win"): - os = "windows" - - if platform.endswith("amd64"): - machine = "x86_64" - elif platform.endswith(("x86", "win32")): - machine = "x86" - else: - raise UnsupportedPlatformError(platform) - return PACT_BIN_URL.format( - version=version, - os=os, - machine=machine, - ext="zip", - ) - - if "manylinux" in platform: - os = "linux" - if platform.endswith("x86_64"): - machine = "x86_64" - elif platform.endswith("aarch64"): - machine = "arm64" - else: - raise UnsupportedPlatformError(platform) - return PACT_BIN_URL.format( - version=version, - os=os, - machine=machine, - ext="tar.gz", - ) - - raise UnsupportedPlatformError(platform) - - def _pact_bin_extract(self, artifact: Path) -> None: - """ - Extract the Pact binaries. - - The binaries in the `bin` directory require the underlying Ruby runtime - to be present, which is included in the `lib` directory. - - Args: - artifact: The path to the downloaded artifact. - """ - with tempfile.TemporaryDirectory() as tmpdir: - if str(artifact).endswith(".zip"): - with zipfile.ZipFile(artifact) as f: - f.extractall(tmpdir) # noqa: S202 - - if str(artifact).endswith(".tar.gz"): - with tarfile.open(artifact) as f: - f.extractall(tmpdir) # noqa: S202 - - for d in ["bin", "lib"]: - if (PACT_ROOT_DIR / d).is_dir(): - shutil.rmtree(PACT_ROOT_DIR / d) - shutil.copytree( - Path(tmpdir) / "pact" / d, - PACT_ROOT_DIR / d, - ) - def pact_lib_install(self, version: str) -> None: """ Install the Pact library binary. From c15a38ea6d4ff40838e692dbe8b3bb80ba5a103e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 18 Jul 2025 11:45:25 +1000 Subject: [PATCH 0869/1376] chore(ci): incorporate tests of pact cli Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 51 ++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1e6661788..e45a67383 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,8 +29,8 @@ jobs: runs-on: ubuntu-latest needs: - - test-linux - - test-other + - test-container + - test - example - format - lint @@ -44,7 +44,7 @@ jobs: if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') - test-linux: + test-container: name: >- Test Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} @@ -93,9 +93,6 @@ jobs: uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true - cache-dependency-glob: | - **/pyproject.toml - **/uv.lock - name: Install Python run: uv python install ${{ matrix.python-version }} @@ -118,6 +115,10 @@ jobs: - name: Run tests run: hatch run test --broker-url=http://pactbroker:pactbroker@localhost:9292 --container + - name: Run tests (CLI) + working-directory: pact-python-cli + run: hatch run test + - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 @@ -131,7 +132,7 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} - test-other: + test: name: >- Test Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} @@ -142,8 +143,9 @@ jobs: fail-fast: false matrix: os: - - windows-latest - macos-latest + - ubuntu-latest + - windows-latest python-version: - '3.9' - '3.10' @@ -167,9 +169,6 @@ jobs: uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true - cache-dependency-glob: | - **/pyproject.toml - **/uv.lock - name: Install Python run: uv python install ${{ matrix.python-version }} @@ -180,6 +179,10 @@ jobs: - name: Run tests run: hatch run test + - name: Run tests (CLI) + working-directory: pact-python-cli + run: hatch run test + example: name: Example @@ -205,9 +208,6 @@ jobs: uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true - cache-dependency-glob: | - **/pyproject.toml - **/uv.lock - name: Install Python run: uv python install ${{ env.STABLE_PYTHON_VERSION }} @@ -255,9 +255,6 @@ jobs: uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true - cache-dependency-glob: | - **/pyproject.toml - **/uv.lock - name: Install Python run: uv python install ${{ env.STABLE_PYTHON_VERSION }} @@ -268,6 +265,10 @@ jobs: - name: Format run: hatch run format + - name: Format (CLI) + working-directory: pact-python-cli + run: hatch run format + lint: name: Lint @@ -280,9 +281,6 @@ jobs: uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true - cache-dependency-glob: | - **/pyproject.toml - **/uv.lock - name: Install Python run: uv python install ${{ env.STABLE_PYTHON_VERSION }} @@ -293,6 +291,10 @@ jobs: - name: Format run: hatch run lint + - name: Format (CLI) + working-directory: pact-python-cli + run: hatch run lint + typecheck: name: Typecheck @@ -305,9 +307,6 @@ jobs: uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true - cache-dependency-glob: | - **/pyproject.toml - **/uv.lock - name: Install Python run: uv python install ${{ env.STABLE_PYTHON_VERSION }} @@ -315,7 +314,11 @@ jobs: - name: Install Hatch run: uv tool install hatch - - name: Format + - name: Typecheck + run: hatch run typecheck + + - name: Typecheck (CLI) + working-directory: pact-python-cli run: hatch run typecheck spelling: From dcc286ba29618ca5ee87c69bede634529cf8af8a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 18 Jul 2025 12:57:38 +1000 Subject: [PATCH 0870/1376] chore(ci): use new `pact-python/*` tags Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a9a15f314..8c6d897ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ name: build on: push: tags: - - v* + - pact-python/* branches: - main pull_request: @@ -51,13 +51,10 @@ jobs: # Fetch all tags fetch-depth: 0 - - name: Set up Python + - name: Set up uv uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true - cache-dependency-glob: | - **/pyproject.toml - **/uv.lock - name: Install Python run: uv python install ${{ env.STABLE_PYTHON_VERSION }} @@ -145,7 +142,7 @@ jobs: # As this requires emulation, it's not worth running on PRs or main if: >- github.event_name == 'push' && - startsWith(github.event.ref, 'refs/tags/v') + startsWith(github.event.ref, 'refs/tags/pact-python/') runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -207,7 +204,7 @@ jobs: if: >- github.event_name == 'push' && - startsWith(github.event.ref, 'refs/tags/v') + startsWith(github.event.ref, 'refs/tags/pact-python/') runs-on: ubuntu-latest environment: name: pypi From 35e44bf1225d0ae04bc2d008ad49e38c22e4449e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 18 Jul 2025 12:58:22 +1000 Subject: [PATCH 0871/1376] chore(ci): add build cli pipeline Signed-off-by: JP-Ellis --- .github/workflows/build-cli.yml | 212 ++++++++++++++++++++++++++++++++ pact-python-cli/pyproject.toml | 9 +- 2 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/build-cli.yml diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml new file mode 100644 index 000000000..f35531d52 --- /dev/null +++ b/.github/workflows/build-cli.yml @@ -0,0 +1,212 @@ +--- +name: build cli + +on: + push: + tags: + - pact-python-cli/* + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true + +env: + STABLE_PYTHON_VERSION: '3.13' + HATCH_VERBOSE: '1' + FORCE_COLOR: '1' + CIBW_BUILD_FRONTEND: build + +jobs: + complete: + name: Build CLI completion check + if: always() + + permissions: + contents: none + + runs-on: ubuntu-latest + needs: + - build-sdist + - build-wheels + + steps: + - name: Failed + run: exit 1 + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + + build-sdist: + name: Build CLI source distribution + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # Fetch all tags + fetch-depth: 0 + + - name: Set up uv + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + with: + enable-cache: true + + - name: Install Python + run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + + - name: Install hatch + run: uv tool install hatch + + - name: Create source distribution + working-directory: pact-python-cli + run: hatch build --target sdist + + - name: Upload sdist + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: wheels-sdist + path: pact-python-cli/dist/*.tar* + if-no-files-found: error + compression-level: 0 + + build-wheels: + name: Build CLI wheels on ${{ matrix.os }} + + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-13 + - os: macos-latest + - os: ubuntu-24.04-arm + - os: ubuntu-latest + # - os: windows-11-arm # Not supported upstream + - os: windows-latest + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # Fetch all tags + fetch-depth: 0 + + - name: Filter targets + id: cibw-filter + shell: bash + # On PRs, only build the latest stable version of Python to speed up the + # workflow. + run: | + if [[ "${{ github.event_name}}" == "pull_request" ]] ; then + echo "build=cp${STABLE_PYTHON_VERSION/./}-*" >> "$GITHUB_OUTPUT" + else + echo "build=*" >> "$GITHUB_OUTPUT" + fi + + - name: Set macOS deployment target + if: startsWith(matrix.os, 'macos-') + run: | + echo "MACOSX_DEPLOYMENT_TARGET=10.13" >> "$GITHUB_ENV" + + - name: Create wheels + uses: pypa/cibuildwheel@95d2f3a92fbf80abe066b09418bbf128a8923df2 # v3.0.1 + with: + package-dir: pact-python-cli + env: + CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} + + - name: Upload wheels + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: wheels-${{ matrix.os }} + path: wheelhouse/*.whl + if-no-files-found: error + compression-level: 0 + + publish: + name: Publish CLI wheels and sdist + + if: >- + github.event_name == 'push' && + startsWith(github.event.ref, 'refs/tags/pact-python-cli/') + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/pact-python-cli + + needs: + - build-sdist + - build-wheels + + permissions: + # Required for trusted publishing + id-token: write + # Required for release creation + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # Fetch all tags + fetch-depth: 0 + + - name: Install git cliff and typos + uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13 + with: + tool: git-cliff,typos + + - name: Update changelog + run: git cliff --verbose + working-directory: pact-python-cli + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + + - name: Generate release changelog + id: release-changelog + working-directory: pact-python-cli + run: | + git cliff \ + --current \ + --strip header \ + --output ${{ runner.temp }}/release-changelog.md + + echo -e "\n\n## Pull Requests\n\n" >> ${{ runner.temp }}/release-changelog.md + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + + - name: Download wheels and sdist + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + path: wheelhouse + merge-multiple: true + + - name: Generate release + id: release + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 + with: + files: wheelhouse/* + body_path: ${{ runner.temp }}/release-changelog.md + draft: false + prerelease: false + generate_release_notes: true + + - name: Push build artifacts to PyPI + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 + with: + skip-existing: true + packages-dir: wheelhouse + + - name: Create PR for changelog update + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + with: + token: ${{ secrets.GH_TOKEN }} + commit-message: 'docs: update changelog for ${{ github.ref_name }}' + title: 'docs: update cli changelog' + body: | + This PR updates the changelog for ${{ github.ref_name }}. + branch: docs/update-changelog + base: main diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 02faeaaff..2682e4d40 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -194,13 +194,10 @@ exclude = '' ################################################################################ [tool.cibuildwheel] before-build = "rm -rvf src/pact_cli/{bin,data,lib}" -skip = "pp*" -test-command = "pytest" - [tool.cibuildwheel.macos] - # The repair tool unfortunately did not like the bundled Ruby distributable. - # TODO: Check whether delocate-wheel can be configured. - repair-wheel-command = "" +# The repair tool unfortunately did not like the bundled Ruby distributable, +# with false-positives missing libraries despite being bundled. +repair-wheel-command = "" [tool.cibuildwheel.windows] before-build = [ From a880be6fc60270f5c9cc98d7432578f958e2c24b Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 18 Jul 2025 13:23:37 +1000 Subject: [PATCH 0872/1376] chore: add git cliff configuration Signed-off-by: JP-Ellis --- pact-python-cli/cliff.toml | 113 +++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 pact-python-cli/cliff.toml diff --git a/pact-python-cli/cliff.toml b/pact-python-cli/cliff.toml new file mode 100644 index 000000000..cacd548cb --- /dev/null +++ b/pact-python-cli/cliff.toml @@ -0,0 +1,113 @@ +#:schema https://json.schemastore.org/any.json +# git-cliff ~ default configuration file +# https://git-cliff.org/docs/configuration + +[changelog] +# template for the changelog header +header = """ +# Pact Python CLI Changelog + +All notable changes to this project will be documented in this file. + +Note that this _only_ includes changes to the Python re-packaging of the Pact CLI. For changes to the Pact CLI itself, see the [Pact CLI changelog](https://github.com/pact-foundation/pact-ruby-standalone/blob/master/CHANGELOG.md). + + + + + +""" + +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] _{{ timestamp | date(format="%Y-%m-%d") }}_ +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}_({{ commit.scope }})_ {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }}\ + {% if commit.breaking and commit.breaking_description %} + {{ " " }}\ + > {{ + commit.breaking_description + | split(pat="\n") + | join(sep=" ") + | replace(from=" ", to=" ") + | replace(from=" ", to=" ") + | replace(from=" ", to=" ") + | upper_first + }}\ + {% endif %}\ + {% endfor %} +{% endfor %} +{% if github.contributors %}\ + ### Contributors + {% for contributor in github.contributors %}\ + {% if contributor.username and contributor.username is ending_with("[bot]") %}{% continue %}{% endif %} + - @{{ contributor.username }}\ + {% endfor %} +{% endif %} + +""" + +# template for the changelog footer +footer = """\ + +""" + +# remove the leading and trailing s +trim = true +# postprocessors +postprocessors = [] +# render body even when there are no releases to process +# render_always = true +# output file path +output = "CHANGELOG.md" + +[git] +tag_pattern = "pact-python-cli/.*" +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # Remove the PR number added by GitHub when merging PR in UI + { pattern = '\s*\(#([0-9]+)\)$', replace = "" }, + # Check spelling of the commit with https://github.com/crate-ci/typos + { pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# regex for parsing and grouping commits +commit_parsers = [ + # Ignore deps commits from the changelog + { message = "^(chore|fix)\\(deps.*\\)", skip = true }, + { message = "^chore: update changelog.*", skip = true }, + # Group commits by type + { group = "🚀 Features", message = "^feat" }, + { group = "🐛 Bug Fixes", message = "^fix" }, + { group = "🚜 Refactor", message = "^refactor" }, + { group = "⚡ Performance", message = "^perf" }, + { group = "🎨 Styling", message = "^style" }, + { group = "📚 Documentation", message = "^docs" }, + { group = "🧪 Testing", message = "^test" }, + { group = "◀️ Revert", message = "^revert" }, + { group = "⚙️ Miscellaneous Tasks", message = "^chore" }, + { group = "� Other", message = ".*" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = false +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" + +[remote.github] +owner = "pact-foundation" +repo = "pact-python" From ac1f4f5fffdaa2731b46ca1528457523e30b79f7 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 18 Jul 2025 13:26:58 +1000 Subject: [PATCH 0873/1376] chore: exclude hatch_build from mypy checks Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 653496991..f4f7cf126 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -88,6 +88,10 @@ repos: language: system types: - python - exclude: ^(src/pact|tests|examples|examples/tests)/(?!v3/).*\.py$ + exclude: | + (?x)^( + .*/hatch_build.py | + (src|tests|examples)/(?!v3/).*\.py + )$ stages: - pre-push From 837a14b692dec2dfb39186f8cba16a654c8f6792 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 18 Jul 2025 13:36:52 +1000 Subject: [PATCH 0874/1376] chore(ci): narrow token permissions Signed-off-by: JP-Ellis --- .github/workflows/build-cli.yml | 8 +++++--- .github/workflows/build.yml | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index f35531d52..d05744f04 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -11,9 +11,12 @@ on: branches: - main +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref || github.run_id }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: STABLE_PYTHON_VERSION: '3.13' @@ -142,10 +145,9 @@ jobs: - build-wheels permissions: + contents: read # Required for trusted publishing id-token: write - # Required for release creation - contents: write steps: - name: Checkout code diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c6d897ec..cadfee18b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,9 +11,12 @@ on: branches: - main +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref || github.run_id }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: STABLE_PYTHON_VERSION: '3.13' @@ -216,10 +219,9 @@ jobs: - build-arm64 permissions: + contents: read # Required for trusted publishing id-token: write - # Required for release creation - contents: write steps: - name: Checkout code From ab922e67c3e85c47714697f2d478d475499e0fd8 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 22 Jul 2025 11:35:14 +1000 Subject: [PATCH 0875/1376] chore: remove macosx deployment target Signed-off-by: JP-Ellis --- .github/workflows/build-cli.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index d05744f04..f3f563f37 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -109,11 +109,6 @@ jobs: echo "build=*" >> "$GITHUB_OUTPUT" fi - - name: Set macOS deployment target - if: startsWith(matrix.os, 'macos-') - run: | - echo "MACOSX_DEPLOYMENT_TARGET=10.13" >> "$GITHUB_ENV" - - name: Create wheels uses: pypa/cibuildwheel@95d2f3a92fbf80abe066b09418bbf128a8923df2 # v3.0.1 with: From ae218a5fedbbb968109dbdc07b15c668e379acaf Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 22 Jul 2025 11:47:07 +1000 Subject: [PATCH 0876/1376] chore(ci): fix cli publish permissions Signed-off-by: JP-Ellis --- .github/workflows/build-cli.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index f3f563f37..46d3cefff 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,8 @@ jobs: - build-wheels permissions: - contents: read + # Required for creating the release + contents: write # Required for trusted publishing id-token: write From a3efe353d6a8b5a6aab78eda5cf11626bf70a848 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 22 Jul 2025 12:19:54 +1000 Subject: [PATCH 0877/1376] chore: properly extract tag version Signed-off-by: JP-Ellis --- .github/workflows/build-cli.yml | 9 +++--- .github/workflows/build.yml | 13 ++++---- .github/workflows/test.yml | 38 +++++++++++++++++++----- .taplo.toml | 2 +- pact-python-cli/.gitignore | 3 +- pact-python-cli/pyproject.toml | 31 ++++++++++++++----- pact-python-cli/src/pact_cli/__init__.py | 11 +++++++ pyproject.toml | 20 ++++++++++++- 8 files changed, 96 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 46d3cefff..2f0c69378 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -48,9 +48,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - # Fetch all tags fetch-depth: 0 - name: Set up uv @@ -92,9 +92,9 @@ jobs: - os: windows-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - # Fetch all tags fetch-depth: 0 - name: Filter targets @@ -149,7 +149,6 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - # Fetch all tags fetch-depth: 0 - name: Install git cliff and typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cadfee18b..fe2153530 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,9 +49,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - # Fetch all tags fetch-depth: 0 - name: Set up uv @@ -93,9 +93,9 @@ jobs: archs: AMD64 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - # Fetch all tags fetch-depth: 0 - name: Cache pip packages @@ -167,9 +167,9 @@ jobs: # build: "" steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - # Fetch all tags fetch-depth: 0 - name: Cache pip packages @@ -227,7 +227,6 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - # Fetch all tags fetch-depth: 0 - name: Install git cliff and typos diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e45a67383..918102ca2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -85,8 +85,10 @@ jobs: experimental: true steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: + fetch-depth: 0 submodules: true - name: Set up uv @@ -161,8 +163,10 @@ jobs: python-version: '3.9' steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: + fetch-depth: 0 submodules: true - name: Set up uv @@ -202,7 +206,10 @@ jobs: PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 - name: Set up uv uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 @@ -249,7 +256,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 - name: Set up uv uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 @@ -275,7 +285,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 - name: Set up uv uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 @@ -301,7 +314,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 - name: Set up uv uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 @@ -327,7 +343,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ uses: crate-ci/typos@392b78fe18a52790c53f42456e46124f77346842 # v1.34.0 @@ -341,7 +360,10 @@ jobs: PRE_COMMIT_HOME: ${{ github.workspace }}/.pre-commit steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 - name: Cache pre-commit uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 diff --git a/.taplo.toml b/.taplo.toml index 14ddb3d98..e5eeae78c 100644 --- a/.taplo.toml +++ b/.taplo.toml @@ -5,6 +5,6 @@ path = "taplo://taplo.toml" align_entries = true indent_entries = false indent_tables = true -reorder_arrays = true +reorder_arrays = false reorder_inline_tables = true reorder_keys = true diff --git a/pact-python-cli/.gitignore b/pact-python-cli/.gitignore index ae1c5d9da..e9472099d 100644 --- a/pact-python-cli/.gitignore +++ b/pact-python-cli/.gitignore @@ -1,3 +1,4 @@ +src/pact_cli/__version__.py src/pact_cli/bin -src/pact_cli/lib src/pact_cli/data +src/pact_cli/lib diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 2682e4d40..e7d9bf754 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -3,16 +3,9 @@ description = "Pact CLI bundle for Python" name = "pact-python-cli" +dynamic = ["version"] license = "MIT" -# The first three segments of the version correspond to the Pact CLI version. -# The last segment is incremented if there is a need to update the Python -# package without changing the Pact CLI version. -# -# For the latest version, see: -# https://github.com/pact-foundation/pact-ruby-standalone/releases -version = "2.4.26.0" - authors = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] maintainers = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] @@ -70,6 +63,7 @@ requires-python = ">=3.9" [build-system] build-backend = "hatchling.build" requires = [ + "hatch-vcs", "hatchling", "packaging", "requests", @@ -78,8 +72,29 @@ requires = [ [tool.hatch] + [tool.hatch.version] + source = "vcs" + tag-pattern = "^pact-python-cli/(?P[vV]?\\d+(?:\\.\\d+)*)$" + + [tool.hatch.version.raw-options] + git_describe_command = [ + "git", + "describe", + "--tags", + "--dirty", + "--always", + "--long", + "--match", + "pact-python-cli/*", + ] + root = ".." + version_scheme = "no-guess-dev" + [tool.hatch.build] + [tool.hatch.build.hooks.vcs] + version-file = "src/pact_cli/__version__.py" + [tool.hatch.build.targets.sdist] include = [ # Source diff --git a/pact-python-cli/src/pact_cli/__init__.py b/pact-python-cli/src/pact_cli/__init__.py index 91eb27f2a..9b38d9213 100644 --- a/pact-python-cli/src/pact_cli/__init__.py +++ b/pact-python-cli/src/pact_cli/__init__.py @@ -24,11 +24,22 @@ from __future__ import annotations +__author__ = "Pact Foundation" +__license__ = "MIT" +__url__ = "https://github.com/pact-foundation/pact-python" + import os import shutil import warnings from pathlib import Path +from pact_cli.__version__ import ( + __version__ as __version__, +) +from pact_cli.__version__ import ( + __version_tuple__ as __version_tuple__, +) + _USE_SYSTEM_BINS = os.getenv("PACT_USE_SYSTEM_BINS", "").upper() in ("TRUE", "YES") _BIN_DIR = Path(__file__).parent.resolve() / "bin" diff --git a/pyproject.toml b/pyproject.toml index 61cd58f86..b2492237b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,7 +124,22 @@ requires = [ [tool.hatch] [tool.hatch.version] - source = "vcs" + source = "vcs" + tag-pattern = "^pact-python/(?P[vV]?\\d+(?:\\.\\d+)*)$" + + [tool.hatch.version.raw-options] + git_describe_command = [ + "git", + "describe", + "--tags", + " --dirty", + "--always", + "--long", + "--match", + "pact-python-cli/*", + ] + root = "." + version_scheme = "no-guess-dev" [tool.hatch.build] @@ -176,6 +191,9 @@ requires = [ ] features = ["devel"] installer = "uv" + # This is require to get around an incompatibility between hatch and uv + # See: https://github.com/pypa/hatch/issues/1639 + pre-install-commands = ["uv pip install -e .[devel]"] [tool.hatch.envs.default.scripts] all = ["example", "format", "lint", "test", "typecheck"] From cd23152dde0404503a024497b1321b4a6244b0e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 01:23:50 +0000 Subject: [PATCH 0878/1376] chore(deps): update astral-sh/setup-uv action to v6.4.1 --- .github/workflows/build-cli.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 2f0c69378..6499af8bd 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 with: enable-cache: true From b0da83e969b381313ea3a2b001d7199c32bdd609 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 23 Jul 2025 19:48:43 +1000 Subject: [PATCH 0879/1376] feat(cli): build abi-agnostic wheels Instead of building separate wheels for each Python version, we now build `py3-none-{platform}` wheels. These can be used by any Python 3 version, now and into the future. The reason this works is that the Python code is pure Python and only bundles CLIs to be called through Python. Ref: #1120 Ref: #1082 Signed-off-by: JP-Ellis --- .github/workflows/build-cli.yml | 16 +--- pact-python-cli/hatch_build.py | 129 ++++++++++++++++++-------------- pact-python-cli/pyproject.toml | 41 +++------- 3 files changed, 86 insertions(+), 100 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 6499af8bd..81dd4d4f0 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -19,7 +19,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: - STABLE_PYTHON_VERSION: '3.13' + STABLE_PYTHON_VERSION: '313' HATCH_VERBOSE: '1' FORCE_COLOR: '1' CIBW_BUILD_FRONTEND: build @@ -97,24 +97,12 @@ jobs: with: fetch-depth: 0 - - name: Filter targets - id: cibw-filter - shell: bash - # On PRs, only build the latest stable version of Python to speed up the - # workflow. - run: | - if [[ "${{ github.event_name}}" == "pull_request" ]] ; then - echo "build=cp${STABLE_PYTHON_VERSION/./}-*" >> "$GITHUB_OUTPUT" - else - echo "build=*" >> "$GITHUB_OUTPUT" - fi - - name: Create wheels uses: pypa/cibuildwheel@95d2f3a92fbf80abe066b09418bbf128a8923df2 # v3.0.1 with: package-dir: pact-python-cli env: - CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} + CIBW_BUILD: cp${{ env.STABLE_PYTHON_VERSION }}-* - name: Upload wheels uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/pact-python-cli/hatch_build.py b/pact-python-cli/hatch_build.py index c2284e534..e6f65bbc6 100644 --- a/pact-python-cli/hatch_build.py +++ b/pact-python-cli/hatch_build.py @@ -13,23 +13,24 @@ from __future__ import annotations import logging +import os import shutil +import sys import tarfile import tempfile +import urllib.request import zipfile from pathlib import Path from typing import Any -import requests +from hatchling.builders.config import BuilderConfig from hatchling.builders.hooks.plugin.interface import BuildHookInterface from packaging.tags import sys_tags logger = logging.getLogger(__name__) +EXE = ".exe" if os.name == "nt" else "" PKG_DIR = Path(__file__).parent.resolve() / "src" / "pact_cli" - -# Latest version available at: -# https://github.com/pact-foundation/pact-ruby-standalone/releases PACT_BIN_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" @@ -47,15 +48,12 @@ def __init__(self, platform: str) -> None: super().__init__(f"Unsupported platform {platform}") -class PactBuildHook(BuildHookInterface[Any]): +class PactCliBuildHook(BuildHookInterface[BuilderConfig]): """Custom hook to download Pact binaries.""" - PLUGIN_NAME = "custom" - """ - This is a hard-coded name required by Hatch - """ + PLUGIN_NAME = "pact-cli" - def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 + def __init__(self, *args: object, **kwargs: object) -> None: """ Initialize the build hook. @@ -64,7 +62,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 """ super().__init__(*args, **kwargs) self.tmpdir = Path(tempfile.TemporaryDirectory().name) - self.tmpdir.mkdir(parents=True, exist_ok=True) def clean(self, versions: list[str]) -> None: # noqa: ARG002 """Clean up any files created by the build hook.""" @@ -77,51 +74,54 @@ def initialize( build_data: dict[str, Any], ) -> None: """Hook into Hatchling's build process.""" - build_data["infer_tag"] = True - build_data["pure_python"] = False - cli_version = ".".join(self.metadata.version.split(".")[:3]) if not cli_version: self.app.display_error("Failed to determine Pact CLI version.") try: - self.pact_bin_install(cli_version) + self._pact_bin_install(cli_version) except UnsupportedPlatformError as err: msg = f"Pact CLI is not available for {err.platform}." logger.exception(msg, RuntimeWarning, stacklevel=2) - def pact_bin_install(self, version: str) -> None: + build_data["tag"] = self._infer_tag() + + def _sys_tag_platform(self) -> str: + """ + Get the platform tag from the current system tags. + + This is used to determine the target platform for the Pact binaries. + """ + return next(t.platform for t in sys_tags()) + + def _pact_bin_install(self, version: str) -> None: """ Install the Pact standalone binaries. - The binaries are installed in `src/pact/bin`, and the relevant version for - the current operating system is determined automatically. + The binaries are installed in `src/pact_cli/bin`, and the relevant + version for the current operating system is determined automatically. Args: - version: The Pact version to install. + version: + The Pact CLI version to install. """ url = self._pact_bin_url(version) - if url: - artifact = self._download(url) - self._pact_bin_extract(artifact) + artifact = self._download(url) + self._pact_bin_extract(artifact) - def _pact_bin_url(self, version: str) -> str | None: + def _pact_bin_url(self, version: str) -> str: """ Generate the download URL for the Pact binaries. - Generate the download URL for the Pact binaries based on the current - platform and specified version. This function mainly contains a lot of - matching logic to determine the correct URL to use, due to the - inconsistencies in naming conventions between ecosystems. - Args: - version: The upstream Pact version. + version: + The Pact CLI version to download. Returns: - The URL to download the Pact binaries from, or None if the current - platform is not supported. + The URL to download the Pact binaries from. If the platform is not + supported, the resulting URL may be invalid. """ - platform = next(sys_tags()).platform + platform = self._sys_tag_platform() if platform.startswith("macosx"): os = "osx" @@ -161,22 +161,21 @@ def _pact_bin_extract(self, artifact: Path) -> None: Args: artifact: The path to the downloaded artifact. """ - with tempfile.TemporaryDirectory() as tmpdir: - if str(artifact).endswith(".zip"): - with zipfile.ZipFile(artifact) as f: - f.extractall(tmpdir) # noqa: S202 - - if str(artifact).endswith(".tar.gz"): - with tarfile.open(artifact) as f: - f.extractall(tmpdir) # noqa: S202 - - for d in ["bin", "lib"]: - if (PKG_DIR / d).is_dir(): - shutil.rmtree(PKG_DIR / d) - shutil.copytree( - Path(tmpdir) / "pact" / d, - PKG_DIR / d, - ) + if str(artifact).endswith(".zip"): + with zipfile.ZipFile(artifact) as f: + f.extractall(self.tmpdir) # noqa: S202 + + if str(artifact).endswith(".tar.gz"): + with tarfile.open(artifact) as f: + f.extractall(self.tmpdir) # noqa: S202 + + for d in ["bin", "lib"]: + if (PKG_DIR / d).is_dir(): + shutil.rmtree(PKG_DIR / d) + shutil.copytree( + Path(self.tmpdir) / "pact" / d, + PKG_DIR / d, + ) def _download(self, url: str) -> Path: """ @@ -196,13 +195,31 @@ def _download(self, url: str) -> Path: artifact.parent.mkdir(parents=True, exist_ok=True) if not artifact.exists(): - response = requests.get(url, timeout=30) - try: - response.raise_for_status() - except requests.HTTPError as e: - msg = f"Failed to download from {url}." - raise RuntimeError(msg) from e - with artifact.open("wb") as f: - f.write(response.content) + urllib.request.urlretrieve(url, artifact) # noqa: S310 return artifact + + def _infer_tag(self) -> str: + """ + Infer the tag for the current build. + + Since we have a pure Python wrapper around a binary CLI, we are not + tied to any specific Python version or ABI. As a result, we generate + `py3-none-{platform}` tags for the wheels. + """ + platform = self._sys_tag_platform() + + # On macOS, the version needs to be set based on the deployment target + # (i.e., the version of the system libraries). + if sys.platform == "darwin" and ( + deployment_target := os.environ.get("MACOSX_DEPLOYMENT_TARGET") + ): + target = deployment_target.replace(".", "_") + if platform.endswith("_arm64"): + platform = f"macosx_{target}_arm64" + elif platform.endswith("_x86_64"): + platform = f"macosx_{target}_x86_64" + else: + raise UnsupportedPlatformError(platform) + + return f"py3-none-{platform}" diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index e7d9bf754..b4ef7b931 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -66,8 +66,7 @@ requires = [ "hatch-vcs", "hatchling", "packaging", - "requests", - "setuptools ; python_version >= '3.12'", + # "setuptools ; python_version >= '3.12'", ] [tool.hatch] @@ -95,29 +94,12 @@ requires = [ [tool.hatch.build.hooks.vcs] version-file = "src/pact_cli/__version__.py" - [tool.hatch.build.targets.sdist] - include = [ - # Source - "/src/pact_cli/**/*.py", - "/src/pact_cli/**/*.pyi", - "/src/pact_cli/**/py.typed", - - # Metadata - "*.md", - "LICENSE", - ] - [tool.hatch.build.targets.wheel] - artifacts = ["/src/pact_cli/bin/*", "/src/pact_cli/lib/*"] - include = [ - # Source - "/src/pact_cli/**/*.py", - "/src/pact_cli/**/*.pyi", - "/src/pact_cli/**/py.typed", - ] - packages = ["/src/pact_cli"] + artifacts = ["src/pact_cli/bin", "src/pact_cli/lib"] + packages = ["src/pact_cli"] [tool.hatch.build.targets.wheel.hooks.custom] + patch = "hatch_build.py" ######################################## ## Hatch Environment Configuration @@ -208,15 +190,14 @@ exclude = '' ## CI Build Wheel ################################################################################ [tool.cibuildwheel] -before-build = "rm -rvf src/pact_cli/{bin,data,lib}" - # The repair tool unfortunately did not like the bundled Ruby distributable, # with false-positives missing libraries despite being bundled. repair-wheel-command = "" - [tool.cibuildwheel.windows] - before-build = [ - 'IF EXIST src\pact_cli\bin\ RMDIR /S /Q src\pact_cli\bin', - 'IF EXIST src\pact_cli\data\ RMDIR /S /Q src\pact_cli\data', - 'IF EXIST src\pact_cli\lib\ RMDIR /S /Q src\pact_cli\lib', - ] + [[tool.cibuildwheel.overrides]] + environment.MACOSX_DEPLOYMENT_TARGET = "10.13" + select = "*-macosx_x86_64" + + [[tool.cibuildwheel.overrides]] + environment.MACOSX_DEPLOYMENT_TARGET = "11.0" + select = "*-macosx_arm64" From 793abf34008fbd242ca1dff3b10bfc28fa46c0c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 07:49:00 +1000 Subject: [PATCH 0880/1376] chore(deps): update astral-sh/setup-uv action to v6.4.2 (#1123) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 81dd4d4f0..29a19eece 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 + uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe2153530..72fc13df6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 + uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5bedc9c38..48bae86ef 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 + uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 918102ca2..509f1592b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -92,7 +92,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 + uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 with: enable-cache: true @@ -170,7 +170,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 + uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 with: enable-cache: true @@ -212,7 +212,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 + uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 with: enable-cache: true @@ -262,7 +262,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 + uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 with: enable-cache: true @@ -291,7 +291,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 + uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 with: enable-cache: true @@ -320,7 +320,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 + uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 with: enable-cache: true @@ -373,7 +373,7 @@ jobs: key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Set up uv - uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1 + uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 with: enable-cache: true cache-suffix: pre-commit From 08641fc380e4defd907ee7306a16c3e8c83c6d2e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 24 Jul 2025 08:47:39 +1000 Subject: [PATCH 0881/1376] chore: update gitignore Signed-off-by: JP-Ellis --- .gitignore | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 463a6b940..f92d2bf56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ ################################################################################ -## Pact Python Specific +## Project Specific ################################################################################ -src/pact/data # Test outputs examples/tests/pacts @@ -9,6 +8,9 @@ examples/tests/pacts # Version is determined from the VCS src/pact/__version__.py +# Wheels from CIBuildWheel +wheelhouse/ + ################################################################################ ## Standard Templates ################################################################################ From 0f9adc186b4d58534168e1f470fb17a4e2ca4d0e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 23 Jul 2025 11:32:29 +0000 Subject: [PATCH 0882/1376] docs: update changelog for pact-python-cli/2.4.26.2 --- pact-python-cli/CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 pact-python-cli/CHANGELOG.md diff --git a/pact-python-cli/CHANGELOG.md b/pact-python-cli/CHANGELOG.md new file mode 100644 index 000000000..b63bdebf2 --- /dev/null +++ b/pact-python-cli/CHANGELOG.md @@ -0,0 +1,29 @@ +# Pact Python CLI Changelog + +All notable changes to this project will be documented in this file. + +Note that this _only_ includes changes to the Python re-packaging of the Pact CLI. For changes to the Pact CLI itself, see the [Pact CLI changelog](https://github.com/pact-foundation/pact-ruby-standalone/blob/master/CHANGELOG.md). + + + + + +## [pact-python-cli/2.4.26.2] _2025-07-23_ + +### 🚀 Features + +- Create pact-python-cli package +- _(cli)_ Build abi-agnostic wheels + +### ⚙️ Miscellaneous Tasks + +- Create cli and ffi packages +- _(ci)_ Add build cli pipeline +- Add git cliff configuration +- Properly extract tag version + +### Contributors + +- @JP-Ellis + + From 0da628eaff93f4aea1c2252f55a1796fc95a80ec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 23:28:34 +0000 Subject: [PATCH 0883/1376] chore(deps): update astral-sh/setup-uv action to v6.4.3 (#1125) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 29a19eece..1b0fee405 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72fc13df6..f1db75864 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 48bae86ef..8e72a1572 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 509f1592b..15d67cb57 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -92,7 +92,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 with: enable-cache: true @@ -170,7 +170,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 with: enable-cache: true @@ -212,7 +212,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 with: enable-cache: true @@ -262,7 +262,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 with: enable-cache: true @@ -291,7 +291,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 with: enable-cache: true @@ -320,7 +320,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 with: enable-cache: true @@ -373,7 +373,7 @@ jobs: key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Set up uv - uses: astral-sh/setup-uv@2c7142f755d7b37bdaea8d226073714c732889fe # v6.4.2 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 with: enable-cache: true cache-suffix: pre-commit From a3dfbda6cdd2d5c57b2d87d18af687179c4ddc16 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 24 Jul 2025 10:18:05 +1000 Subject: [PATCH 0884/1376] docs(cli): update readme and ensure it is on pypi Signed-off-by: JP-Ellis --- pact-python-cli/README.md | 6 +++++- pact-python-cli/pyproject.toml | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pact-python-cli/README.md b/pact-python-cli/README.md index c813ff176..493c44abe 100644 --- a/pact-python-cli/README.md +++ b/pact-python-cli/README.md @@ -11,6 +11,8 @@ This sub-package is part of the [Pact Python](https://github.com/pact-foundation It is used by version 2 of Pact Python, and can be used to install the Pact CLI in Python environments. +The versionining of `pact-python-cli` is aligned with the Pact CLI versioning. For example, version `2.4.26.2` corresponds to Pact CLI version `2.4.26`, with the `.2` indicating that this is the third release of that Pact CLI version in the Python package (with the first release being `.0`). + ## Installation You can install this package via pip: @@ -21,7 +23,9 @@ pip install pact-python-cli ## Contributing -Contributions to this package are generally not required as it contains minimal Python functionality and generally only requires updating the version number. To contribute to the Pact CLI itself, please refer to the [Pact Ruby Standalone repository](https://github.com/pact-foundation/pact-ruby-standalone). +Contributions to this package are generally not required as it contains minimal Python functionality and generally only requires updating the version number. This is done by pushing a tag of the form `pact-python-cli/` which will automatically trigger a release build in the CI pipeline. + +To contribute to the Pact CLI itself, please refer to the [Pact Ruby Standalone repository](https://github.com/pact-foundation/pact-ruby-standalone). For contributing to Pact Python, see the [main contributing guide](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md). diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index b4ef7b931..0b063f02b 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -3,8 +3,10 @@ description = "Pact CLI bundle for Python" name = "pact-python-cli" -dynamic = ["version"] -license = "MIT" +dynamic = ["version"] +keywords = ["pact", "cli", "pact-python", "contract-testing"] +license = "MIT" +readme = "README.md" authors = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] maintainers = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] From 219de203b68d62d54549a6b062bfdb2a4bab4938 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 23:37:34 +0000 Subject: [PATCH 0885/1376] chore(deps): update pactfoundation/pact-broker:latest docker digest to 05b05a1 (#1127) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15d67cb57..581b96590 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,7 +54,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:0106b1f233b8869c865bbcf75bc158148222fad0a44423c4b6ac5f47df12167d + image: pactfoundation/pact-broker:latest@sha256:05b05a192c771b33ba67ffdf2d6829bb32600145ca8f154165187d318c8ee70f ports: - 9292:9292 env: @@ -194,7 +194,7 @@ jobs: services: broker: - image: pactfoundation/pact-broker:latest@sha256:0106b1f233b8869c865bbcf75bc158148222fad0a44423c4b6ac5f47df12167d + image: pactfoundation/pact-broker:latest@sha256:05b05a192c771b33ba67ffdf2d6829bb32600145ca8f154165187d318c8ee70f ports: - 9292:9292 env: From 063f31bce3d816b8d15967ea9921aa86b07b8ceb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:22:43 +1000 Subject: [PATCH 0886/1376] chore(deps): update taiki-e/install-action action to v2.57.1 (#1130) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 1b0fee405..ff1d3ee58 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13 + uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1db75864..3e2304e04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -230,7 +230,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19 + uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1 with: tool: git-cliff,typos From c609333bb86bb220cd4c556403bd5c24d2f3b7ec Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 25 Jul 2025 13:57:20 +1000 Subject: [PATCH 0887/1376] feat(ffi): add standalone ffi package Signed-off-by: JP-Ellis --- .github/workflows/build-ffi.yml | 198 + .github/workflows/test.yml | 28 +- pact-python-cli/pyproject.toml | 7 +- pact-python-ffi/.gitignore | 2 + pact-python-ffi/LICENSE | 21 + pact-python-ffi/README.md | 43 + pact-python-ffi/cliff.toml | 113 + pact-python-ffi/hatch_build.py | 354 + pact-python-ffi/pyproject.toml | 220 +- pact-python-ffi/src/pact_ffi/__init__.py | 7820 ++++++++++++++++++++++ pact-python-ffi/src/pact_ffi/ffi.pyi | 6 + pact-python-ffi/src/pact_ffi/py.typed | 0 pact-python-ffi/tests/.ruff.toml | 10 + pact-python-ffi/tests/test_init.py | 76 + pyproject.toml | 12 + 15 files changed, 8900 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/build-ffi.yml create mode 100644 pact-python-ffi/.gitignore create mode 100644 pact-python-ffi/LICENSE create mode 100644 pact-python-ffi/README.md create mode 100644 pact-python-ffi/cliff.toml create mode 100644 pact-python-ffi/hatch_build.py create mode 100644 pact-python-ffi/src/pact_ffi/__init__.py create mode 100644 pact-python-ffi/src/pact_ffi/ffi.pyi create mode 100644 pact-python-ffi/src/pact_ffi/py.typed create mode 100644 pact-python-ffi/tests/.ruff.toml create mode 100644 pact-python-ffi/tests/test_init.py diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml new file mode 100644 index 000000000..691ec3eaa --- /dev/null +++ b/.github/workflows/build-ffi.yml @@ -0,0 +1,198 @@ +--- +name: build ffi + +on: + push: + tags: + - pact-python-ffi/* + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + STABLE_PYTHON_VERSION: '39' + HATCH_VERBOSE: '1' + FORCE_COLOR: '1' + CIBW_BUILD_FRONTEND: build + +jobs: + complete: + name: Build FFI completion check + if: always() + + permissions: + contents: none + + runs-on: ubuntu-latest + needs: + - build-sdist + - build-wheels + + steps: + - name: Failed + run: exit 1 + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + + build-sdist: + name: Build FFI source distribution + + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Set up uv + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + with: + enable-cache: true + + - name: Install Python + run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + + - name: Install hatch + run: uv tool install hatch + + - name: Create source distribution + working-directory: pact-python-ffi + run: hatch build --target sdist + + - name: Upload sdist + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: wheels-sdist + path: pact-python-ffi/dist/*.tar* + if-no-files-found: error + compression-level: 0 + + build-wheels: + name: Build FFI wheels on ${{ matrix.os }} + + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-13 + - os: macos-latest + - os: ubuntu-24.04-arm + - os: ubuntu-latest + - os: windows-11-arm + - os: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Create wheels + uses: pypa/cibuildwheel@95d2f3a92fbf80abe066b09418bbf128a8923df2 # v3.0.1 + with: + package-dir: pact-python-ffi + env: + CIBW_BUILD: cp${{ env.STABLE_PYTHON_VERSION }}-* + HATCH_VERBOSE: '1' + + - name: Upload wheels + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: wheels-${{ matrix.os }} + path: wheelhouse/*.whl + if-no-files-found: error + compression-level: 0 + + publish: + name: Publish FFI wheels and sdist + + if: >- + github.event_name == 'push' && + startsWith(github.event.ref, 'refs/tags/pact-python-ffi/') + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/pact-python-ffi + + needs: + - build-sdist + - build-wheels + + permissions: + # Required for creating the release + contents: write + # Required for trusted publishing + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Install git cliff and typos + uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13 + with: + tool: git-cliff,typos + + - name: Update changelog + run: git cliff --verbose + working-directory: pact-python-ffi + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + + - name: Generate release changelog + id: release-changelog + working-directory: pact-python-ffi + run: | + git cliff \ + --current \ + --strip header \ + --output ${{ runner.temp }}/release-changelog.md + + echo -e "\n\n## Pull Requests\n\n" >> ${{ runner.temp }}/release-changelog.md + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + + - name: Download wheels and sdist + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + path: wheelhouse + merge-multiple: true + + - name: Generate release + id: release + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 + with: + files: wheelhouse/* + body_path: ${{ runner.temp }}/release-changelog.md + draft: false + prerelease: false + generate_release_notes: true + + - name: Push build artifacts to PyPI + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 + with: + skip-existing: true + packages-dir: wheelhouse + + - name: Create PR for changelog update + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + with: + token: ${{ secrets.GH_TOKEN }} + commit-message: 'docs: update changelog for ${{ github.ref_name }}' + title: 'docs: update ffi changelog' + body: | + This PR updates the changelog for ${{ github.ref_name }}. + branch: docs/update-changelog + base: main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 581b96590..819fccfa1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -121,6 +121,10 @@ jobs: working-directory: pact-python-cli run: hatch run test + - name: Run tests (FFI) + working-directory: pact-python-ffi + run: hatch run test + - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 @@ -177,6 +181,15 @@ jobs: - name: Install Python run: uv python install ${{ matrix.python-version }} + - name: Set PATH on Windows + if: startsWith(matrix.os, 'windows-') + shell: pwsh + run: echo "$pwd/pact-python-ffi/src/pact_ffi" >> $env:GITHUB_PATH + + - name: Set DYLD_LIBRARY_PATH on macOS + if: startsWith(matrix.os, 'macos-') + run: echo "DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:$PWD/pact-python-ffi/src/pact_ffi" >> $GITHUB_ENV + - name: Install Hatch run: uv tool install hatch @@ -187,6 +200,10 @@ jobs: working-directory: pact-python-cli run: hatch run test + - name: Run tests (FFI) + working-directory: pact-python-ffi + run: hatch run test + example: name: Example @@ -279,6 +296,9 @@ jobs: working-directory: pact-python-cli run: hatch run format + - name: Format (FFI) + working-directory: pact-python-ffi + run: hatch run format lint: name: Lint @@ -301,13 +321,17 @@ jobs: - name: Install Hatch run: uv tool install hatch - - name: Format + - name: Lint run: hatch run lint - - name: Format (CLI) + - name: Lint (CLI) working-directory: pact-python-cli run: hatch run lint + - name: Lint (FFI) + working-directory: pact-python-ffi + run: hatch run lint + typecheck: name: Typecheck diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 0b063f02b..3e517ed39 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -64,12 +64,7 @@ requires-python = ">=3.9" ################################################################################ [build-system] build-backend = "hatchling.build" -requires = [ - "hatch-vcs", - "hatchling", - "packaging", - # "setuptools ; python_version >= '3.12'", -] +requires = ["hatch-vcs", "hatchling", "packaging"] [tool.hatch] diff --git a/pact-python-ffi/.gitignore b/pact-python-ffi/.gitignore new file mode 100644 index 000000000..3f3a7ec73 --- /dev/null +++ b/pact-python-ffi/.gitignore @@ -0,0 +1,2 @@ +src/pact_ffi/data +src/pact_ffi/__version__.py diff --git a/pact-python-ffi/LICENSE b/pact-python-ffi/LICENSE new file mode 100644 index 000000000..032bed571 --- /dev/null +++ b/pact-python-ffi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Pact Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pact-python-ffi/README.md b/pact-python-ffi/README.md new file mode 100644 index 000000000..23d9231a1 --- /dev/null +++ b/pact-python-ffi/README.md @@ -0,0 +1,43 @@ +# Pact Python FFI + +> [!NOTE] +> +> This package provides direct access to the Pact Foreign Function Interface (FFI) with minimal abstraction. It is intended for advanced users who need low-level control over Pact operations in Python. + +--- + +This sub-package is part of the [Pact Python](https://github.com/pact-foundation/pact-python) project and exists to expose the [Pact FFI](https://github.com/pact-foundation/pact-reference) directly to Python. If you are looking for the main Pact Python library for contract testing, please see the [root package](https://github.com/pact-foundation/pact-python#pact-python). + +## Overview + +- The module provides a thin Python wrapper around the Pact FFI (C API). +- Most classes correspond directly to structs from the FFI, and are designed to wrap the underlying C pointers. +- Many classes implement the `__del__` method to ensure memory allocated by the Rust library is freed when the Python object is destroyed, preventing memory leaks. +- Functions from the FFI are exposed directly: if a function `foo` exists in the FFI, it is accessible as `pact_ffi.foo(...)`. +- The API is not guaranteed to be stable and is intended for use by advanced users or for building higher-level libraries. For typical contract testing, use the main Pact Python client library. + +## Installation + +You can install this package via pip: + +```console +pip install pact-python-ffi +``` + +## Usage + +This package exposes the raw FFI bindings for Pact. It is suitable for advanced use cases, custom integrations, or for building higher-level libraries. For typical contract testing, prefer using the main Pact Python library. + +## Contributing + +As this is a relatively thin wrapper around the Pact FFI, the code is unlikely to change frequently; however, contributions to improve the coverage of the FFI bindings or to improve existing functionality are welcome. See the [main contributing guide](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md) for details. + +To release a new version of `pact-python-ffi`, simply push a tag in the format `pact-python-ffi/x.y.z.w`. This will automatically trigger a release process, pulling in version `x.y.z` of the underlying Pact FFI. Before creating and pushing such a tag, please ensure that the Python wrapper has been updated to reflect any changes or updates in the corresponding FFI version. + +Higher-level abstractions or utilities should be implemented in separate libraries (such as [`pact-python`](https://github.com/pact-foundation/pact-python)). + +--- + +For questions or support, please visit the [Pact Foundation Slack](https://slack.pact.io) or [GitHub Discussions](https://github.com/pact-foundation/pact-python/discussions) + +--- diff --git a/pact-python-ffi/cliff.toml b/pact-python-ffi/cliff.toml new file mode 100644 index 000000000..245b7915a --- /dev/null +++ b/pact-python-ffi/cliff.toml @@ -0,0 +1,113 @@ +#:schema https://json.schemastore.org/any.json +# git-cliff configuration file +# https://git-cliff.org/docs/configuration + +[changelog] +# template for the changelog header +header = """ +# Pact Python FFI Changelog + +All notable changes to this project will be documented in this file. + +Note that this _only_ includes changes to the Python FFI interface. For changes to the Pact FFI itself, see the [Pact FFI changelog](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/CHANGELOG.md). + + + + + +""" + +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] _{{ timestamp | date(format="%Y-%m-%d") }}_ +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}_({{ commit.scope }})_ {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }}\ + {% if commit.breaking and commit.breaking_description %} + {{ " " }}\ + > {{ + commit.breaking_description + | split(pat="\n") + | join(sep=" ") + | replace(from=" ", to=" ") + | replace(from=" ", to=" ") + | replace(from=" ", to=" ") + | upper_first + }}\ + {% endif %}\ + {% endfor %} +{% endfor %} +{% if github.contributors %}\ + ### Contributors + {% for contributor in github.contributors %}\ + {% if contributor.username and contributor.username is ending_with("[bot]") %}{% continue %}{% endif %} + - @{{ contributor.username }}\ + {% endfor %} +{% endif %} + +""" + +# template for the changelog footer +footer = """\ + +""" + +# remove the leading and trailing s +trim = true +# postprocessors +postprocessors = [] +# render body even when there are no releases to process +# render_always = true +# output file path +output = "CHANGELOG.md" + +[git] +tag_pattern = "pact-python-ffi/.*" +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # Remove the PR number added by GitHub when merging PR in UI + { pattern = '\s*\(#([0-9]+)\)$', replace = "" }, + # Check spelling of the commit with https://github.com/crate-ci/typos + { pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# regex for parsing and grouping commits +commit_parsers = [ + # Ignore deps commits from the changelog + { message = "^(chore|fix)\\(deps.*\\)", skip = true }, + { message = "^chore: update changelog.*", skip = true }, + # Group commits by type + { group = "🚀 Features", message = "^feat" }, + { group = "🐛 Bug Fixes", message = "^fix" }, + { group = "🚜 Refactor", message = "^refactor" }, + { group = "⚡ Performance", message = "^perf" }, + { group = "🎨 Styling", message = "^style" }, + { group = "📚 Documentation", message = "^docs" }, + { group = "🧪 Testing", message = "^test" }, + { group = "◀️ Revert", message = "^revert" }, + { group = "⚙️ Miscellaneous Tasks", message = "^chore" }, + { group = "� Other", message = ".*" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = false +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" + +[remote.github] +owner = "pact-foundation" +repo = "pact-python" diff --git a/pact-python-ffi/hatch_build.py b/pact-python-ffi/hatch_build.py new file mode 100644 index 000000000..294584c94 --- /dev/null +++ b/pact-python-ffi/hatch_build.py @@ -0,0 +1,354 @@ +""" +Hatchling build hook for binary downloads. + +Pact Python is built on top of the Ruby Pact binaries and the Rust Pact library. +This build script downloads the binaries and library for the current platform +and installs them in the `pact` directory under `/bin` and `/lib`. + +The version of the binaries and library can be controlled with the +`PACT_BIN_VERSION` and `PACT_LIB_VERSION` environment variables. If these are +not set, a pinned version will be used instead. +""" + +from __future__ import annotations + +import gzip +import os +import shutil +import sys +import tempfile +import urllib.request +from pathlib import Path +from typing import Any + +import cffi +from hatchling.builders.hooks.plugin.interface import BuildHookInterface +from packaging.tags import sys_tags + +PKG_DIR = Path(__file__).parent.resolve() / "src" / "pact_ffi" +PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{platform}{suffix}.{ext}" + + +class UnsupportedPlatformError(RuntimeError): + """Raised when the current platform is not supported.""" + + def __init__(self, platform: str) -> None: + """ + Initialize the exception. + + Args: + platform: The unsupported platform. + """ + self.platform = platform + super().__init__(f"Unsupported platform {platform}") + + +class PactBuildHook(BuildHookInterface[Any]): + """Custom hook to download Pact binaries.""" + + PLUGIN_NAME = "pact-ffi" + + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 + """ + Initialize the build hook. + + For this hook, we additionally define the lib extension based on the + current platform. + """ + super().__init__(*args, **kwargs) + self.tmpdir = Path(tempfile.TemporaryDirectory().name) + self.tmpdir.mkdir(parents=True, exist_ok=True) + + def clean(self, versions: list[str]) -> None: # noqa: ARG002 + """Clean up any files created by the build hook.""" + for ffi in (PKG_DIR / "v3").glob("__init__.*"): + if ffi.suffix in (".so", ".dylib", ".dll", ".a", ".pyd"): + ffi.unlink() + + def initialize( + self, + version: str, # noqa: ARG002 + build_data: dict[str, Any], + ) -> None: + """Hook into Hatchling's build process.""" + ffi_version = ".".join(self.metadata.version.split(".")[:3]) + if not ffi_version: + self.app.display_error("Failed to determine Pact FFI version.") + + try: + build_data["force_include"] = self._install(ffi_version) + except UnsupportedPlatformError as err: + msg = f"Pact FFI library is not available for {err.platform}" + self.app.display_error(msg) + + self.app.display_debug(f"Wheel artifacts: {build_data['force_include']}") + build_data["tag"] = self._infer_tag() + + def _sys_tag_platform(self) -> str: + """ + Get the platform tag from the current system tags. + + This is used to determine the target platform for the Pact library. + """ + return next(t.platform for t in sys_tags()) + + def _install(self, version: str) -> dict[str, str]: + """ + Install the Pact library binary. + + This will download the Pact library binary for the current platform and + build the CFFI bindings for it. + + Args: + version: The Pact version to install. + """ + # Download the Pact library binary and header file + lib_url = self._lib_url(version) + header = self._download(lib_url.rsplit("/", 1)[0] + "/pact.h") + lib = self._extract_lib(self._download(lib_url)) + if lib.suffix == ".dll": + dll_lib = self._extract_lib( + self._download(lib_url.replace(".dll.gz", ".dll.lib.gz")) + ) + else: + dll_lib = None + + # Compile the FFI extension + extension = self._compile(lib, header) + + # Copy into the package directory, using the ABI3 marking for broad + # compatibility. + # NOTE: Windows does _not_ use the version infixation + extension_name, _, suffix = extension.name.split(".") + infix = ".abi3" if os.name != "nt" else "" + extension_dest = f"{extension_name}{infix}.{suffix}" + shutil.copy(extension, PKG_DIR / extension_dest) + + if pact_lib_dir := os.getenv("PACT_LIB_DIR"): + # Copy the library to make it available by other processes (such as + # the wheel repair). + dir_path = Path(pact_lib_dir) + dir_path.mkdir(parents=True, exist_ok=True) + self.app.display_debug(f"Copying {lib.name} into {dir_path}") + shutil.copy(lib, dir_path / lib.name) + if dll_lib: + self.app.display_debug(f"Copying {dll_lib.name} into {dir_path}") + shutil.copy(dll_lib, dir_path / dll_lib.name) + + return {str(extension): f"pact_ffi/{extension_dest}"} + + def _lib_url(self, version: str) -> str: # noqa: C901, PLR0912 + """ + Generate the download URL for the Pact library. + + Args: + version: The upstream Pact version. + + Returns: + The URL to download the Pact library from. + + Raises: + UnsupportedPlatformError: + If the current platform is not supported. + """ + wheel_platform = self._sys_tag_platform() + + aarch64 = ("_arm64", "_aarch64") + x86_64 = ("_x86_64", "_amd64") + + # Simplified platform and architecture detection + if wheel_platform.startswith("macosx"): + os, ext = "macos", "dylib.gz" + prefix = "lib" + suffix = "" + if wheel_platform.endswith(aarch64): + platform = "aarch64" + elif wheel_platform.endswith(x86_64): + platform = "x86_64" + else: + raise UnsupportedPlatformError(wheel_platform) + + elif wheel_platform.startswith("musllinux"): + os, ext = "linux", "a.gz" # MUSL uses static library + prefix = "lib" + suffix = "-musl" + if wheel_platform.endswith(aarch64): + platform = "aarch64" + elif wheel_platform.endswith(x86_64): + platform = "x86_64" + else: + raise UnsupportedPlatformError(wheel_platform) + + elif wheel_platform.startswith("manylinux"): + os, ext = "linux", "so.gz" + prefix = "lib" + suffix = "" + if wheel_platform.endswith(aarch64): + platform = "aarch64" + elif wheel_platform.endswith(x86_64): + platform = "x86_64" + else: + raise UnsupportedPlatformError(wheel_platform) + + elif wheel_platform.startswith("win"): + # TODO: Switch to using `dll.gz` + # https://github.com/python-cffi/cffi/issues/182 + os, ext = "windows", "dll.gz" + prefix = "" + suffix = "" + if wheel_platform.endswith(aarch64): + platform = "aarch64" + elif wheel_platform.endswith(x86_64): + platform = "x86_64" + else: + raise UnsupportedPlatformError(wheel_platform) + + else: + raise UnsupportedPlatformError(wheel_platform) + + return PACT_LIB_URL.format( + version=version, + prefix=prefix, + os=os, + platform=platform, + suffix=suffix, + ext=ext, + ) + + def _extract_lib(self, artifact: Path) -> Path: + """ + Extract the Pact library. + + Args: + artifact: The URL to download the Pact binaries from. + """ + target = PKG_DIR / (artifact.name.split("-")[0] + artifact.suffixes[-2]) + with ( + gzip.open(artifact, "rb") as f_in, + target.open("wb") as f_out, + ): + shutil.copyfileobj(f_in, f_out) + self.app.display_debug(f"Extracted Pact library to {target}") + return target + + def _compile(self, lib: Path, header: Path) -> Path: + """ + Build the CFFI bindings for the Pact library. + + Args: + lib: + The path to the Pact library binary. + + header: + The path to the Pact library header file. + """ + if os.name == "nt": + extra_libs = [ + "advapi32", + "bcrypt", + "crypt32", + "iphlpapi", + "ncrypt", + "netapi32", + "ntdll", + "ole32", + "oleaut32", + "pdh", + "powrprof", + "psapi", + "secur32", + "shell32", + "user32", + "userenv", + "ws2_32", + ] + else: + extra_libs = [] + + ffibuilder = cffi.FFI() + ffibuilder.cdef( + "\n".join( + line + for line in header.read_text().splitlines() + if not line.strip().startswith("#") + ) + ) + + linker_args: list[str] = [] + if os.name == "posix": + linker_args.append(f"-Wl,-rpath,{lib.parent}") + elif os.name == "nt": + # Windows has no equivalent to rpath, instead, the end-user must + # ensure that the PATH environment variable is updated to include + # the directory containing the Pact library. + self.app.display_warning( + "On Windows, ensure that the PATH environment variable includes " + f"{lib.parent} to load the Pact library at runtime." + ) + + ffibuilder.set_source( + "ffi", + header.read_text(), + libraries=["pact_ffi", *extra_libs], + library_dirs=[str(lib.parent)], + extra_link_args=linker_args, + ) + extension = Path(ffibuilder.compile(verbose=True, tmpdir=str(self.tmpdir))) + self.app.display_debug(f"Compiled CFFI bindings to {extension}") + return extension + + def _download(self, url: str) -> Path: + """ + Download the target URL. + + This will download the target URL to the `pact/data` directory. If the + download artifact is already present, its path will be returned. + + If `extract` is True, the downloaded artifact will be extracted and the + path to the extract file will be returned instead. + + Args: + url: The URL to download + extract: Whether to extract the downloaded artifact. + + Return: + The path to the downloaded artifact. + """ + filename = url.split("/")[-1] + artifact = PKG_DIR / "data" / filename + artifact.parent.mkdir(parents=True, exist_ok=True) + + if not artifact.exists(): + self.app.display_debug(f"Downloading {url} to {artifact}") + urllib.request.urlretrieve(url, artifact) # noqa: S310 + else: + self.app.display_debug(f"Using cached artifact {artifact}") + + return artifact + + def _infer_tag(self) -> str: + """ + Infer the tag for the current build. + + The bindings are built to target ABI3, which is compatible with multiple + Python versions. As a result, we generate `py3-abi3-{platform}` tags for + the wheels. + """ + python_version = f"{sys.version_info.major}{sys.version_info.minor}" + + platform = self._sys_tag_platform() + + # On macOS, the version needs to be set based on the deployment target + # (i.e., the version of the system libraries). + if sys.platform == "darwin" and ( + deployment_target := os.getenv("MACOSX_DEPLOYMENT_TARGET") + ): + target = deployment_target.replace(".", "_") + if platform.endswith("_arm64"): + platform = f"macosx_{target}_arm64" + elif platform.endswith("_x86_64"): + platform = f"macosx_{target}_x86_64" + else: + raise UnsupportedPlatformError(platform) + + return f"cp{python_version}-abi3-{platform}" diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index e68713eea..68eb045a5 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -3,8 +3,224 @@ description = "Python bindings for the Pact FFI library" name = "pact-python-ffi" -license = "MIT" -version = "0.0.0" +# dynamic = ["version"] +keywords = ["pact", "ffi", "pact-python", "contract-testing"] +license = "MIT" +readme = "README.md" +version = "0.4.22.0" authors = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] maintainers = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] + +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 :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python", + "Topic :: Software Development :: Testing", +] + +requires-python = ">=3.9" + +dependencies = ["cffi~=1.0"] + + [project.urls] + "Bug Tracker" = "https://github.com/pact-foundation/pact-python/issues" + "Changelog" = "https://github.com/pact-foundation/pact-python/blob/main/pact-python-ffi/CHANGELOG.md" + "Documentation" = "https://docs.pact.io" + "Homepage" = "https://pact.io" + "Repository" = "https://github.com/pact-foundation/pact-python" + + [project.optional-dependencies] + # Linting and formatting tools use a more narrow specification to ensure + # developper consistency. All other dependencies are as above. + devel = [ + "pact-python-ffi[devel-test]", + "pact-python-ffi[devel-types]", + "ruff==0.12.4", + ] + devel-test = ["pytest-cov~=6.0", "pytest-mock~=3.0", "pytest~=8.0"] + devel-types = ["mypy==1.17.0"] + +################################################################################ +## Build System +################################################################################ +[build-system] +build-backend = "hatchling.build" +requires = ["hatch-vcs", "hatchling", "packaging", "cffi"] + +[tool.hatch] + + [tool.hatch.version] + source = "vcs" + tag-pattern = "^pact-python-ffi/(?P[vV]?\\d+(?:\\.\\d+)*)$" + + [tool.hatch.version.raw-options] + git_describe_command = [ + "git", + "describe", + "--tags", + "--dirty", + "--always", + "--long", + "--match", + "pact-python-ffi/*", + ] + root = ".." + version_scheme = "no-guess-dev" + + [tool.hatch.build] + + [tool.hatch.build.hooks.vcs] + version-file = "src/pact_ffi/__version__.py" + + [tool.hatch.build.targets.wheel] + packages = ["src/pact_ffi"] + + [tool.hatch.build.targets.wheel.hooks.custom] + patch = "hatch_build.py" + + ######################################## + ## Hatch Environment Configuration + ######################################## + [tool.hatch.envs] + + # Install dev dependencies in the default environment to simplify the developer + # workflow. + [tool.hatch.envs.default] + extra-dependencies = ["hatch-vcs", "hatchling", "packaging", "cffi"] + features = ["devel"] + installer = "uv" + + # Update paths to ensure the shared library can be found + # TODO: See if this can be overridden on a per-platform basis + # https://github.com/pypa/hatch/discussions/2024 + # [tool.hatch.envs.default.overrides] + # platform.windows.env-vars = { PATH = "{root}/src/pact_ffi;{env:PATH}" } + + [tool.hatch.envs.default.scripts] + all = ["format", "lint", "test", "typecheck"] + format = "ruff format {args}" + lint = "ruff check --show-fixes {args}" + test = "pytest tests/ {args}" + typecheck = ["typecheck-src", "typecheck-tests"] + typecheck-src = "mypy src/ {args}" + typecheck-tests = "mypy tests/ {args}" + + # Test environment for running unit tests. This automatically tests against all + # supported Python versions. + [tool.hatch.envs.test] + features = ["devel-test"] + installer = "uv" + + # Update paths to ensure the shared library can be found + # TODO: See if this can be overridden on a per-platform basis + # https://github.com/pypa/hatch/discussions/2024 + # [tool.hatch.envs.default.overrides] + # platform.windows.env-vars = { PATH = "{root}/src/pact_ffi;{env:PATH}" } + + [[tool.hatch.envs.test.matrix]] + python = ["3.10", "3.11", "3.12", "3.13", "3.9"] + +################################################################################ +## PyTest Configuration +################################################################################ +[tool.pytest] + + [tool.pytest.ini_options] + addopts = [ + "--import-mode=importlib", + # Coverage options + "--cov-config=pyproject.toml", + "--cov-report=xml", + "--cov=pact_ffi", + ] + + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" + +################################################################################ +## Coverage +################################################################################ +[tool.coverage] + + [tool.coverage.paths] + pact-ffi = ["/src/pact_ffi"] + tests = ["/tests"] + + [tool.coverage.report] + exclude_lines = [ + "@(abc\\.)?abstractmethod", # Ignore abstract methods + "if TYPE_CHECKING:", # Ignore typing + "if __name__ == .__main__.:", # Ignore non-runnable code + "raise NotImplementedError", # Ignore defensive assertions + ] + +################################################################################ +## Ruff Configuration +################################################################################ +[tool.ruff] +extend = "../pyproject.toml" + +exclude = [] + +################################################################################ +## Mypy Configuration +################################################################################ +[tool.mypy] +# Overwrite the exclusions from the root pyproject.toml. +exclude = '' + +################################################################################ +## CI Build Wheel +################################################################################ +[tool.cibuildwheel] +environment.HATCH_VERBOSE = "1" + + [tool.cibuildwheel.linux] + before-build = ["uv pip install --system abi3audit"] + environment.PACT_LIB_DIR = "/tmp/pact_ffi" + repair-wheel-command = [ + "export LD_LIBRARY_PATH=\"$PACT_LIB_DIR:$LD_LIBRARY_PATH\"", + "auditwheel repair -w {dest_dir} {wheel}", + "abi3audit --strict --report {wheel}", + ] + + [tool.cibuildwheel.macos] + before-build = ["pip install abi3audit"] + environment.PACT_LIB_DIR = "/tmp/pact_ffi" + repair-wheel-command = [ + "export DYLD_LIBRARY_PATH=\"$PACT_LIB_DIR:$DYLD_LIBRARY_PATH\"", + "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}", + "abi3audit --strict --report {wheel}", + ] + + [tool.cibuildwheel.windows] + archs = ["auto64"] + before-build = ["pip install abi3audit delvewheel"] + environment.PACT_LIB_DIR = "C:/tmp/pact_ffi" + repair-wheel-command = [ + "delvewheel repair -vv --add-path \"%PACT_LIB_DIR%\" -w {dest_dir} {wheel}", + "abi3audit --strict --report {wheel}", + ] + + [[tool.cibuildwheel.overrides]] + environment.MACOSX_DEPLOYMENT_TARGET = "12.0" + inherit.environment = "append" + select = "*-macosx_x86_64" + + [[tool.cibuildwheel.overrides]] + environment.MACOSX_DEPLOYMENT_TARGET = "12.0" + inherit.environment = "append" + select = "*-macosx_arm64" diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py new file mode 100644 index 000000000..05a6a963b --- /dev/null +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -0,0 +1,7820 @@ +""" +Python bindings for the Pact FFI. + +This module provides a Python interface to the Pact FFI. It is a thin wrapper +around the C API, and is intended to be used by the Pact Python client library +to provide a Pythonic interface to Pact. + +!!! warning + + This module is not intended to be used directly by Pact users. Pact users + should use the Pact Python client library instead. No guarantees are made + about the stability of this module's API. + +## Developer Notes + +This modules should provide the following only: + +- Basic Enum classes +- Simple wrappers around functions, including the casting of input and output + values between the high level Python types and the low level C types. +- Simple wrappers around some of the low-level types. Specifically designed to + automatically handle the freeing of memory when the Python object is + destroyed. + +These low-level functions may then be combined into higher level classes and +modules. Ideally, all code outside of this module should be written in pure +Python and not worry about allocating or freeing memory. + +During initial implementation, a lot of these functions will simply raise a +[`NotImplementedError`][NotImplementedError]. + +For those unfamiliar with CFFI, please make sure to read the [CFFI +documentation](https://cffi.readthedocs.io/en/latest/using.html). + +### Handles + +The Rust library exposes a number of handles to internal data structures. This +is done to avoid exposing the internal implementation details of the library to +users of the library, and avoid unnecessarily casting to and from possibly +complicated structs. + +In the Rust library, the handles are thin wrappers around integers, and +unfortunately the CFFI interface sees this and automatically unwraps them, +exposing the underlying integer. As a result, we must re-wrap the integer +returned by the CFFI interface. This unfortunately means that we may be subject +to changes in private implementation details upstream. + +### Freeing Memory + +Python has a garbage collector, and as a result, we don't need to worry about +manually freeing memory. Having said that, Python's garbace collector is only +aware of Python objects, and not of any memory allocated by the Rust library. + +To ensure that the memory allocated by the Rust library is freed, we must make +sure to define the +[`__del__`](https://docs.python.org/3/reference/datamodel.html#object.__del__) +method to call the appropriate free function whenever the Python object is +destroyed. + +Note that there are some rather subtle details as to when this is called, when +it may never be called, and what global variables are accessible. This is +explained in the documentation for `__del__` above, and in Python's [garbage +collection](https://docs.python.org/3/library/gc.html) module. + +### Error Handling + +The FFI function should handle all errors raised by the function call, and raise +an appropriate Python exception. The exception should be raised using the +appropriate Python exception class, and should be documented in the function's +docstring. +""" + +# The following lints are disabled during initial development and should be +# removed later. +# ruff: noqa: ARG001 (unused-function-argument) +# ruff: noqa: A002 (builtin-argument-shadowing) +# ruff: noqa: D101 (undocumented-public-class) + +# The following lints are disabled for this file. +# ruff: noqa: SLF001 +# private-member-access, as we need access to other handles' internal +# references, without exposing them to the user. +# pyright: reportPrivateUsage=false +# Ignore private member access, as we frequently need to use the +# object's underlying pointer stored in `_ptr`. + +from __future__ import annotations + +import gc +import inspect +import json +import logging +import typing +import warnings +from enum import Enum +from typing import TYPE_CHECKING, Any, Literal + +from pact_ffi.__version__ import __version__ as __version__ +from pact_ffi.__version__ import __version_tuple__ as __version_tuple__ +from pact_ffi.ffi import ffi, lib # type: ignore[import] + +if TYPE_CHECKING: + import datetime + from collections.abc import Collection + from collections.abc import Generator as GeneratorType + from pathlib import Path + + import cffi + from typing_extensions import Self + +logger = logging.getLogger(__name__) + +################################################################################ +# Type aliases +################################################################################ +# The following type aliases provide a nicer interface for end-users of the +# library, especially when it comes to [`Enum`][Enum] classes which offers +# support for string literals as alternative values. + +GeneratorCategoryOptions = Literal[ + "METHOD", "method", + "PATH", "path", + "HEADER", "header", + "QUERY", "query", + "BODY", "body", + "STATUS", "status", + "METADATA", "metadata", +] # fmt: skip +""" +Generator Category Options. + +Type alias for the string literals which represent the Generator Category +Options. +""" + +MatchingRuleCategoryOptions = Literal[ + "METHOD", "method", + "PATH", "path", + "HEADER", "header", + "QUERY", "query", + "BODY", "body", + "STATUS", "status", + "CONTENTS", "contents", + "METADATA", "metadata", +] # fmt: skip + +################################################################################ +# Classes +################################################################################ +# The follow types are classes defined in the Rust code. Ultimately, a Python +# alternative should be implemented, but for now, the follow lines only serve +# to inform the type checker of the existence of these types. + + +class AsynchronousMessage: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: + """ + Initialise a new Asynchronous Message. + + Args: + ptr: + CFFI data structure. + + owned: + Whether the message is owned by something else or not. This + determines whether the message should be freed when the Python + object is destroyed. + + Raises: + TypeError: + If the `ptr` is not a `struct AsynchronousMessage`. + """ + if ffi.typeof(ptr).cname != "struct AsynchronousMessage *": + msg = ( + f"ptr must be a struct AsynchronousMessage, got {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + self._owned = owned + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "AsynchronousMessage" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"AsynchronousMessage({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the AsynchronousMessage. + """ + if not self._owned: + async_message_delete(self) + + @property + def description(self) -> str: + """ + Description of this message interaction. + + This needs to be unique in the pact file. + """ + return async_message_get_description(self) + + def provider_states(self) -> GeneratorType[ProviderState, None, None]: + """ + Optional provider state for the interaction. + """ + yield from async_message_get_provider_state_iter(self) + return # Ensures that the parent object outlives the generator + + @property + def contents(self) -> MessageContents | None: + """ + The contents of the message. + + This may be `None` if the message has no contents. + """ + return async_message_generate_contents(self) + + +class Consumer: ... + + +class Generator: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a generator value. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct Generator`. + """ + if ffi.typeof(ptr).cname != "struct Generator *": + msg = f"ptr must be a struct Generator, got {ffi.typeof(ptr).cname}" + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "Generator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"Generator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Generator. + """ + + @property + def json(self) -> dict[str, Any]: + """ + Dictionary representation of the generator. + """ + return json.loads(generator_to_json(self)) + + def generate_string(self, context: dict[str, Any] | None = None) -> str: + """ + Generate a string from the generator. + + Args: + context: + JSON payload containing any generator context. For example: + + - The context for a `MockServerURL` generator should contain + details about the running mock server. + - The context for a `ProviderStateGenerator` should contain + the values returned from the provider state callback + function. + """ + return generator_generate_string(self, json.dumps(context or {})) + + def generate_integer(self, context: dict[str, Any] | None = None) -> int: + """ + Generate an integer from the generator. + + Args: + context: + JSON payload containing any generator context. For example: + + - The context for a `ProviderStateGenerator` should contain + the values returned from the provider state callback + function. + """ + return generator_generate_integer(self, json.dumps(context or {})) + + +class GeneratorCategoryIterator: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new generator category iterator. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct GeneratorCategoryIterator`. + """ + if ffi.typeof(ptr).cname != "struct GeneratorCategoryIterator *": + msg = ( + "ptr must be a struct GeneratorCategoryIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "GeneratorCategoryIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"GeneratorCategoryIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the GeneratorCategoryIterator. + """ + generators_iter_delete(self) + + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> GeneratorKeyValuePair: + """ + Get the next generator category from the iterator. + """ + return generators_iter_next(self) + + +class GeneratorKeyValuePair: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new key-value generator pair. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct GeneratorKeyValuePair`. + """ + if ffi.typeof(ptr).cname != "struct GeneratorKeyValuePair *": + msg = ( + "ptr must be a struct GeneratorKeyValuePair, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "GeneratorKeyValuePair" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"GeneratorKeyValuePair({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the GeneratorKeyValuePair. + """ + generators_iter_pair_delete(self) + + @property + def path(self) -> str: + """ + Generator path. + """ + s = ffi.string(self._ptr.path) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s + + @property + def generator(self) -> Generator: + """ + Generator value. + """ + return Generator(self._ptr.generator) # type: ignore[attr-defined] + + +class HttpRequest: ... + + +class HttpResponse: ... + + +class InteractionHandle: + """ + Handle to a HTTP Interaction. + + [Rust + `InteractionHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/struct.InteractionHandle.html) + """ + + def __init__(self, ref: int) -> None: + """ + Initialise a new Interaction Handle. + + Args: + ref: + Reference to the Interaction Handle. + """ + self._ref: int = ref + + def __str__(self) -> str: + """ + String representation of the Interaction Handle. + """ + return f"InteractionHandle({self._ref})" + + def __repr__(self) -> str: + """ + String representation of the Interaction Handle. + """ + return f"InteractionHandle({self._ref!r})" + + +class MatchingRule: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new key-value generator pair. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MatchingRule`. + """ + if ffi.typeof(ptr).cname != "struct MatchingRule *": + msg = f"ptr must be a struct MatchingRule, got {ffi.typeof(ptr).cname}" + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MatchingRule" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MatchingRule({self._ptr!r})" + + @property + def json(self) -> dict[str, Any]: + """ + Dictionary representation of the matching rule. + """ + return json.loads(matching_rule_to_json(self)) + + +class MatchingRuleCategoryIterator: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new key-value generator pair. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MatchingRuleCategoryIterator`. + """ + if ffi.typeof(ptr).cname != "struct MatchingRuleCategoryIterator *": + msg = ( + "ptr must be a struct MatchingRuleCategoryIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MatchingRuleCategoryIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MatchingRuleCategoryIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the MatchingRuleCategoryIterator. + """ + matching_rules_iter_delete(self) + + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> MatchingRuleKeyValuePair: + """ + Get the next generator category from the iterator. + """ + return matching_rules_iter_next(self) + + +class MatchingRuleDefinitionResult: ... + + +class MatchingRuleIterator: ... + + +class MatchingRuleKeyValuePair: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new key-value generator pair. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MatchingRuleKeyValuePair`. + """ + if ffi.typeof(ptr).cname != "struct MatchingRuleKeyValuePair *": + msg = ( + "ptr must be a struct MatchingRuleKeyValuePair, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MatchingRuleKeyValuePair" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MatchingRuleKeyValuePair({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the MatchingRuleKeyValuePair. + """ + matching_rules_iter_pair_delete(self) + + @property + def path(self) -> str: + """ + Matching Rule path. + """ + s = ffi.string(self._ptr.path) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s + + @property + def matching_rule(self) -> MatchingRule: + """ + Matching Rule value. + """ + return MatchingRule(self._ptr.matching_rule) # type: ignore[attr-defined] + + +class MatchingRuleResult: ... + + +class MessageContents: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = True) -> None: + """ + Initialise a Message Contents. + + Args: + ptr: + CFFI data structure. + + owned: + Whether the message is owned by something else or not. This + determines whether the message should be freed when the Python + object is destroyed. + + Raises: + TypeError: + If the `ptr` is not a `struct MessageContents`. + """ + if ffi.typeof(ptr).cname != "struct MessageContents *": + msg = f"ptr must be a struct MessageContents, got {ffi.typeof(ptr).cname}" + raise TypeError(msg) + self._ptr = ptr + self._owned = owned + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MessageContents" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MessageContents({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the MessageContents. + """ + if not self._owned: + message_contents_delete(self) + + @property + def contents(self) -> str | bytes | None: + """ + Get the contents of the message. + """ + return message_contents_get_contents_str( + self + ) or message_contents_get_contents_bin(self) + + @property + def metadata(self) -> GeneratorType[MessageMetadataPair, None, None]: + """ + Get the metadata for the message contents. + """ + yield from message_contents_get_metadata_iter(self) + return # Ensures that the parent object outlives the generator + + def matching_rules( + self, + category: MatchingRuleCategoryOptions | MatchingRuleCategory, + ) -> GeneratorType[MatchingRuleKeyValuePair, None, None]: + """ + Get the matching rules for the message contents. + """ + if isinstance(category, str): + category = MatchingRuleCategory(category.upper()) + yield from message_contents_get_matching_rule_iter(self, category) + return # Ensures that the parent object outlives the generator + + def generators( + self, + category: GeneratorCategoryOptions | GeneratorCategory, + ) -> GeneratorType[GeneratorKeyValuePair, None, None]: + """ + Get the generators for the message contents. + """ + if isinstance(category, str): + category = GeneratorCategory(category.upper()) + yield from message_contents_get_generators_iter(self, category) + return # Ensures that the parent object outlives the generator + + +class MessageMetadataIterator: + """ + Iterator over an interaction's metadata. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Message Metadata Iterator. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MessageMetadataIterator`. + """ + if ffi.typeof(ptr).cname != "struct MessageMetadataIterator *": + msg = ( + "ptr must be a struct MessageMetadataIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MessageMetadataIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MessageMetadataIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Interaction Iterator. + """ + message_metadata_iter_delete(self) + + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> MessageMetadataPair: + """ + Get the next interaction from the iterator. + """ + return message_metadata_iter_next(self) + + +class MessageMetadataPair: + """ + A metadata key-value pair. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Message Metadata Pair. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MessageMetadataPair`. + """ + if ffi.typeof(ptr).cname != "struct MessageMetadataPair *": + msg = ( + f"ptr must be a struct MessageMetadataPair, got {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MessageMetadataPair" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MessageMetadataPair({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Interaction Iterator. + """ + message_metadata_pair_delete(self) + + @property + def key(self) -> str: + """ + Metadata key. + """ + s = ffi.string(self._ptr.key) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s + + @property + def value(self) -> str: + """ + Metadata value. + """ + s = ffi.string(self._ptr.value) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s + + +class Mismatch: ... + + +class Mismatches: ... + + +class MismatchesIterator: ... + + +class Pact: ... + + +class PactAsyncMessageIterator: + """ + Iterator over a Pact's asynchronous messages. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Pact Asynchronous Message Iterator. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct PactAsyncMessageIterator`. + """ + if ffi.typeof(ptr).cname != "struct PactAsyncMessageIterator *": + msg = ( + "ptr must be a struct PactAsyncMessageIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "PactAsyncMessageIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"PactAsyncMessageIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Synchronous Message Iterator. + """ + pact_async_message_iter_delete(self) + + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> AsynchronousMessage: + """ + Get the next message from the iterator. + """ + return pact_async_message_iter_next(self) + + +class PactHandle: + """ + Handle to a Pact. + + [Rust + `PactHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/struct.PactHandle.html) + """ + + def __init__(self, ref: int) -> None: + """ + Initialise a new Pact Handle. + + Args: + ref: + Rust library reference to the Pact Handle. + """ + self._ref: int = ref + + def __del__(self) -> None: + """ + Destructor for the Pact Handle. + """ + cleanup_plugins(self) + free_pact_handle(self) + + def __str__(self) -> str: + """ + String representation of the Pact Handle. + """ + return f"PactHandle({self._ref})" + + def __repr__(self) -> str: + """ + String representation of the Pact Handle. + """ + return f"PactHandle({self._ref!r})" + + +class PactServerHandle: + """ + Handle to a Pact Server. + + This does not have an exact correspondence in the Rust library. It is used + to manage the lifecycle of the mock server. + + # Implementation Notes + + The Rust library uses the port number as a unique identifier, in much the + same was as it uses a wrapped integer for the Pact handle. + """ + + def __init__(self, ref: int) -> None: + """ + Initialise a new Pact Server Handle. + + Args: + ref: + Rust library reference to the Pact Server. + """ + self._ref: int = ref + + def __del__(self) -> None: + """ + Destructor for the Pact Server Handle. + """ + cleanup_mock_server(self) + + def __str__(self) -> str: + """ + String representation of the Pact Server Handle. + """ + return f"PactServerHandle({self._ref})" + + def __repr__(self) -> str: + """ + String representation of the Pact Server Handle. + """ + return f"PactServerHandle({self._ref!r})" + + @property + def port(self) -> int: + """ + Port on which the Pact Server is running. + """ + return self._ref + + +class PactInteraction: ... + + +class PactInteractionIterator: + """ + Iterator over a Pact's interactions. + + Interactions encompasses all types of interactions, including HTTP + interactions and messages. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Pact Interaction Iterator. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct PactInteractionIterator`. + """ + if ffi.typeof(ptr).cname != "struct PactInteractionIterator *": + msg = ( + "ptr must be a struct PactInteractionIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "PactInteractionIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"PactInteractionIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Interaction Iterator. + """ + pact_interaction_iter_delete(self) + + def __next__(self) -> PactInteraction: + """ + Get the next interaction from the iterator. + """ + return pact_interaction_iter_next(self) + + +class PactSyncHttpIterator: + """ + Iterator over a Pact's synchronous HTTP interactions. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Pact Synchronous HTTP Iterator. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct PactSyncHttpIterator`. + """ + if ffi.typeof(ptr).cname != "struct PactSyncHttpIterator *": + msg = ( + "ptr must be a struct PactSyncHttpIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "PactSyncHttpIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"PactSyncHttpIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Synchronous HTTP Iterator. + """ + pact_sync_http_iter_delete(self) + + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> SynchronousHttp: + """ + Get the next message from the iterator. + """ + return pact_sync_http_iter_next(self) + + +class PactSyncMessageIterator: + """ + Iterator over a Pact's synchronous messages. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Pact Synchronous Message Iterator. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct PactSyncMessageIterator`. + """ + if ffi.typeof(ptr).cname != "struct PactSyncMessageIterator *": + msg = ( + "ptr must be a struct PactSyncMessageIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "PactSyncMessageIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"PactSyncMessageIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Synchronous Message Iterator. + """ + pact_sync_message_iter_delete(self) + + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> SynchronousMessage: + """ + Get the next message from the iterator. + """ + return pact_sync_message_iter_next(self) + + +class Provider: ... + + +class ProviderState: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new ProviderState. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct ProviderState`. + """ + if ffi.typeof(ptr).cname != "struct ProviderState *": + msg = f"ptr must be a struct ProviderState, got {ffi.typeof(ptr).cname}" + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "ProviderState({self.name!r})" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"ProviderState({self._ptr!r})" + + @property + def name(self) -> str: + """ + Provider State name. + """ + return provider_state_get_name(self) or "" + + def parameters(self) -> GeneratorType[tuple[str, str], None, None]: + """ + Provider State parameters. + + This is a generator that yields key-value pairs. + """ + for p in provider_state_get_param_iter(self): + yield p.key, p.value + return # Ensures that the parent object outlives the generator + + +class ProviderStateIterator: + """ + Iterator over an interactions ProviderStates. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Provider State Iterator. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct ProviderStateIterator`. + """ + if ffi.typeof(ptr).cname != "struct ProviderStateIterator *": + msg = ( + "ptr must be a struct ProviderStateIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "ProviderStateIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"ProviderStateIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Provider State Iterator. + """ + provider_state_iter_delete(self) + + def __iter__(self) -> ProviderStateIterator: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> ProviderState: + """ + Get the next message from the iterator. + """ + return provider_state_iter_next(self) + + +class ProviderStateParamIterator: + """ + Iterator over a Provider States Parameters. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Provider State Param Iterator. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct ProviderStateParamIterator`. + """ + if ffi.typeof(ptr).cname != "struct ProviderStateParamIterator *": + msg = ( + "ptr must be a struct ProviderStateParamIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "ProviderStateParamIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"ProviderStateParamIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Provider State Param Iterator. + """ + provider_state_param_iter_delete(self) + + def __iter__(self) -> ProviderStateParamIterator: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> ProviderStateParamPair: + """ + Get the next message from the iterator. + """ + return provider_state_param_iter_next(self) + + +class ProviderStateParamPair: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new ProviderStateParamPair. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct ProviderStateParamPair`. + """ + if ffi.typeof(ptr).cname != "struct ProviderStateParamPair *": + msg = ( + "ptr must be a struct ProviderStateParamPair, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "ProviderStateParamPair" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"ProviderStateParamPair({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Provider State Param Pair. + """ + provider_state_param_pair_delete(self) + + @property + def key(self) -> str: + """ + Provider State Param key. + """ + s = ffi.string(self._ptr.key) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s + + @property + def value(self) -> str: + """ + Provider State Param value. + """ + s = ffi.string(self._ptr.value) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s + + +class SynchronousHttp: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: + """ + Initialise a new Synchronous HTTP Interaction. + + Args: + ptr: + CFFI data structure. + + owned: + Whether the message is owned by something else or not. This + determines whether the message should be freed when the Python + object is destroyed. + + Raises: + TypeError: + If the `ptr` is not a `struct SynchronousHttp`. + """ + if ffi.typeof(ptr).cname != "struct SynchronousHttp *": + msg = f"ptr must be a struct SynchronousHttp, got {ffi.typeof(ptr).cname}" + raise TypeError(msg) + self._ptr = ptr + self._owned = owned + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "SynchronousHttp" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"SynchronousHttp({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the SynchronousHttp. + """ + if not self._owned: + sync_http_delete(self) + + @property + def description(self) -> str: + """ + Description of this message interaction. + + This needs to be unique in the pact file. + """ + return sync_http_get_description(self) + + def provider_states(self) -> GeneratorType[ProviderState, None, None]: + """ + Optional provider state for the interaction. + """ + yield from sync_http_get_provider_state_iter(self) + return # Ensures that the parent object outlives the generator + + @property + def request_contents(self) -> str | bytes | None: + """ + The contents of the request. + """ + return sync_http_get_request_contents( + self + ) or sync_http_get_request_contents_bin(self) + + @property + def response_contents(self) -> str | bytes | None: + """ + The contents of the response. + """ + return sync_http_get_response_contents( + self + ) or sync_http_get_response_contents_bin(self) + + +class SynchronousMessage: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: + """ + Initialise a new Synchronous Message. + + Args: + ptr: + CFFI data structure. + + owned: + Whether the message is owned by something else or not. This + determines whether the message should be freed when the Python + object is destroyed. + + Raises: + TypeError: + If the `ptr` is not a `struct SynchronousMessage`. + """ + if ffi.typeof(ptr).cname != "struct SynchronousMessage *": + msg = ( + f"ptr must be a struct SynchronousMessage, got {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + self._owned = owned + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "SynchronousMessage" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"SynchronousMessage({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the SynchronousMessage. + """ + if not self._owned: + sync_message_delete(self) + + @property + def description(self) -> str: + """ + Description of this message interaction. + + This needs to be unique in the pact file. + """ + return sync_message_get_description(self) + + def provider_states(self) -> GeneratorType[ProviderState, None, None]: + """ + Optional provider state for the interaction. + """ + yield from sync_message_get_provider_state_iter(self) + return # Ensures that the parent object outlives the generator + + @property + def request_contents(self) -> MessageContents: + """ + The contents of the message. + """ + return sync_message_generate_request_contents(self) + + @property + def response_contents(self) -> GeneratorType[MessageContents, None, None]: + """ + The contents of the responses. + """ + yield from ( + sync_message_generate_response_contents(self, i) + for i in range(sync_message_get_number_responses(self)) + ) + return # Ensures that the parent object outlives the generator + + +class VerifierHandle: + """ + Handle to a Verifier. + + [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/verifier/handle/struct.VerifierHandle.html) + """ + + def __init__(self, ref: cffi.FFI.CData) -> None: + """ + Initialise a new Verifier Handle. + + Args: + ref: + Rust library reference to the Verifier Handle. + """ + self._ref = ref + + def __del__(self) -> None: + """ + Destructor for the Verifier Handle. + """ + verifier_shutdown(self) + + def __str__(self) -> str: + """ + String representation of the Verifier Handle. + """ + return f"VerifierHandle({hex(id(self._ref))})" + + def __repr__(self) -> str: + """ + String representation of the Verifier Handle. + """ + return f"" + + +class ExpressionValueType(Enum): + """ + Expression Value Type. + + [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/expressions/enum.ExpressionValueType.html) + """ + + UNKNOWN = lib.ExpressionValueType_Unknown + STRING = lib.ExpressionValueType_String + NUMBER = lib.ExpressionValueType_Number + INTEGER = lib.ExpressionValueType_Integer + DECIMAL = lib.ExpressionValueType_Decimal + BOOLEAN = lib.ExpressionValueType_Boolean + + def __str__(self) -> str: + """ + Informal string representation of the Expression Value Type. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the Expression Value Type. + """ + return f"ExpressionValueType.{self.name}" + + +class GeneratorCategory(Enum): + """ + Generator Category. + + [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/generators/enum.GeneratorCategory.html) + """ + + METHOD = lib.GeneratorCategory_METHOD + PATH = lib.GeneratorCategory_PATH + HEADER = lib.GeneratorCategory_HEADER + QUERY = lib.GeneratorCategory_QUERY + BODY = lib.GeneratorCategory_BODY + STATUS = lib.GeneratorCategory_STATUS + METADATA = lib.GeneratorCategory_METADATA + + def __str__(self) -> str: + """ + Informal string representation of the Generator Category. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the Generator Category. + """ + return f"GeneratorCategory.{self.name}" + + +class InteractionPart(Enum): + """ + Interaction Part. + + [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/enum.InteractionPart.html) + """ + + REQUEST = lib.InteractionPart_Request + RESPONSE = lib.InteractionPart_Response + + def __str__(self) -> str: + """ + Informal string representation of the Interaction Part. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the Interaction Part. + """ + return f"InteractionPath.{self.name}" + + +class LevelFilter(Enum): + """Level Filter.""" + + OFF = lib.LevelFilter_Off + ERROR = lib.LevelFilter_Error + WARN = lib.LevelFilter_Warn + INFO = lib.LevelFilter_Info + DEBUG = lib.LevelFilter_Debug + TRACE = lib.LevelFilter_Trace + + def __str__(self) -> str: + """ + Informal string representation of the Level Filter. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the Level Filter. + """ + return f"LevelFilter.{self.name}" + + +class MatchingRuleCategory(Enum): + """ + Matching Rule Category. + + [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) + """ + + METHOD = lib.MatchingRuleCategory_METHOD + PATH = lib.MatchingRuleCategory_PATH + HEADER = lib.MatchingRuleCategory_HEADER + QUERY = lib.MatchingRuleCategory_QUERY + BODY = lib.MatchingRuleCategory_BODY + STATUS = lib.MatchingRuleCategory_STATUS + CONTENTS = lib.MatchingRuleCategory_CONTENTS + METADATA = lib.MatchingRuleCategory_METADATA + + def __str__(self) -> str: + """ + Informal string representation of the Matching Rule Category. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the Matching Rule Category. + """ + return f"MatchingRuleCategory.{self.name}" + + +class PactSpecification(Enum): + """ + Pact Specification. + + [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/pact_specification/enum.PactSpecification.html) + """ + + UNKNOWN = lib.PactSpecification_Unknown + V1 = lib.PactSpecification_V1 + V1_1 = lib.PactSpecification_V1_1 + V2 = lib.PactSpecification_V2 + V3 = lib.PactSpecification_V3 + V4 = lib.PactSpecification_V4 + + @classmethod + def from_str(cls, version: str) -> PactSpecification: + """ + Instantiate a Pact Specification from a string. + + This method is case-insensitive, and allows for the version to be + specified with or without a leading "V", and with either a dot or an + underscore as the separator. + + Args: + version: + The version of the Pact Specification. + + Returns: + The Pact Specification. + """ + version = version.upper().replace(".", "_") + if version.startswith("V"): + return cls[version] + return cls["V" + version] + + def __str__(self) -> str: + """ + Informal string representation of the Pact Specification. + """ + return self.name + + def __repr__(self) -> str: + """ + Information-rich string representation of the Pact Specification. + """ + return f"PactSpecification.{self.name}" + + +class StringResult: + """ + String result. + """ + + class _StringResult(Enum): + """ + Internal enum from Pact FFI. + + [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/enum.StringResult.html) + """ + + FAILED = lib.StringResult_Failed + OK = lib.StringResult_Ok + + class _StringResultCData: + tag: int + ok: cffi.FFI.CData + failed: cffi.FFI.CData + + def __init__(self, cdata: cffi.FFI.CData) -> None: + """ + Initialise a new String Result. + + Args: + cdata: + CFFI data structure. + + Raises: + TypeError: + If the `cdata` is not a `struct StringResult`. + """ + if ffi.typeof(cdata).cname != "struct StringResult": + msg = f"cdata must be a struct StringResult, got {ffi.typeof(cdata).cname}" + raise TypeError(msg) + self._cdata = typing.cast("StringResult._StringResultCData", cdata) + + def __str__(self) -> str: + """ + String representation of the String Result. + """ + return self.text + + def __repr__(self) -> str: + """ + Debugging string representation of the String Result. + """ + return f"" + + @property + def is_failed(self) -> bool: + """ + Whether the result is an error. + """ + return self._cdata.tag == StringResult._StringResult.FAILED.value + + @property + def is_ok(self) -> bool: + """ + Whether the result is ok. + """ + return self._cdata.tag == StringResult._StringResult.OK.value + + @property + def text(self) -> str: + """ + The text of the result. + """ + # The specific `.ok` or `.failed` does not matter. + s = ffi.string(self._cdata.ok) + if isinstance(s, bytes): + return s.decode("utf-8") + return s + + def raise_exception(self) -> None: + """ + Raise an exception with the text of the result. + + Raises: + RuntimeError: + If the result is an error. + + Raises: + RuntimeError: + If the result is an error. + """ + if self.is_failed: + raise RuntimeError(self.text) + + +class OwnedString(str): + """ + A string that owns its own memory. + + This is used to ensure that the memory is freed when the string is + destroyed. + + As this is subclassed from `str`, it can be used in place of a normal string + in most cases. + """ + + __slots__ = ("_ptr", "_string") + + def __new__(cls, ptr: cffi.FFI.CData) -> Self: + """ + Create a new Owned String. + + As this is a subclass of the immutable type `str`, we need to override + the `__new__` method to ensure that the string is initialised correctly. + """ + s = ffi.string(ptr) + return super().__new__(cls, s if isinstance(s, str) else s.decode("utf-8")) + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Owned String. + + Args: + ptr: + CFFI data structure. + """ + self._ptr = ptr + s = ffi.string(ptr) + self._string = s if isinstance(s, str) else s.decode("utf-8") + + def __str__(self) -> str: + """ + String representation of the Owned String. + """ + return self._string + + def __repr__(self) -> str: + """ + Debugging string representation of the Owned String. + """ + return f"" + + def __del__(self) -> None: + """ + Destructor for the Owned String. + """ + string_delete(self) + + def __eq__(self, other: object) -> bool: + """ + Equality comparison. + + Args: + other: + The object to compare to. + + Returns: + Whether the two objects are equal. + """ + if isinstance(other, OwnedString): + return self._ptr == other._ptr + if isinstance(other, str): + return self._string == other + return super().__eq__(other) + + def __hash__(self) -> int: + """ + Hash the Owned String. + + Returns: + The hash of the Owned String. + """ + return hash(self._string) + + +def version() -> str: + """ + Return the version of the pact_ffi library. + + [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_version) + + Returns: + The version of the pact_ffi library as a string, in the form of `x.y.z`. + """ + v = ffi.string(lib.pactffi_version()) + if isinstance(v, bytes): + return v.decode("utf-8") + return v + + +def init(log_env_var: str) -> None: + """ + Initialise the mock server library. + + This can provide an environment variable name to use to set the log levels. + This function should only be called once, as it tries to install a global + tracing subscriber. + + [Rust + `pactffi_init`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_init) + + # Safety + + log_env_var must be a valid NULL terminated UTF-8 string. + """ + raise NotImplementedError + + +def init_with_log_level(level: str = "INFO") -> None: + """ + Initialises logging, and sets the log level explicitly. + + This function should only be called once, as it tries to install a global + tracing subscriber. + + [Rust + `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_init_with_log_level) + + Args: + level: + One of TRACE, DEBUG, INFO, WARN, ERROR, NONE/OFF. Case-insensitive. + + # Safety + + Exported functions are inherently unsafe. + """ + raise NotImplementedError + + +def enable_ansi_support() -> None: + """ + Enable ANSI coloured output on Windows. + + On non-Windows platforms, this function is a no-op. + + [Rust + `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_enable_ansi_support) + + # Safety + + This function is safe. + """ + raise NotImplementedError + + +def log_message( + message: str, + log_level: LevelFilter | str = LevelFilter.ERROR, + source: str | None = None, +) -> None: + """ + Log using the shared core logging facility. + + [Rust + `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_message) + + This is useful for callers to have a single set of logs. + + Args: + message: + The contents written to the log + + log_level: + The verbosity at which this message should be logged. + + source: + The source of the log, such as the class, module or caller. + """ + if isinstance(log_level, str): + log_level = LevelFilter[log_level.upper()] + if source is None: + source = inspect.stack()[1].function + lib.pactffi_log_message( + source.encode("utf-8"), + log_level.name.encode("utf-8"), + message.encode("utf-8"), + ) + + +def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: + """ + Get an iterator over mismatches. + + [Rust + `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_get_iter) + """ + raise NotImplementedError + + +def mismatches_delete(mismatches: Mismatches) -> None: + """ + Delete mismatches. + + [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_delete) + """ + raise NotImplementedError + + +def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: + """ + Get the next mismatch from a mismatches iterator. + + [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_iter_next) + + Returns a null pointer if no mismatches remain. + """ + raise NotImplementedError + + +def mismatches_iter_delete(iter: MismatchesIterator) -> None: + """ + Delete a mismatches iterator when you're done with it. + + [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_iter_delete) + """ + raise NotImplementedError + + +def mismatch_to_json(mismatch: Mismatch) -> str: + """ + Get a JSON representation of the mismatch. + + [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_to_json) + """ + raise NotImplementedError + + +def mismatch_type(mismatch: Mismatch) -> str: + """ + Get the type of a mismatch. + + [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_type) + """ + raise NotImplementedError + + +def mismatch_summary(mismatch: Mismatch) -> str: + """ + Get a summary of a mismatch. + + [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_summary) + """ + raise NotImplementedError + + +def mismatch_description(mismatch: Mismatch) -> str: + """ + Get a description of a mismatch. + + [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_description) + """ + raise NotImplementedError + + +def mismatch_ansi_description(mismatch: Mismatch) -> str: + """ + Get an ANSI-compatible description of a mismatch. + + [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_ansi_description) + """ + raise NotImplementedError + + +def get_error_message(length: int = 1024) -> str | None: + """ + Provide the error message from `LAST_ERROR` to the calling C code. + + [Rust + `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_get_error_message) + + This function should be called after any other function in the pact_matching + FFI indicates a failure with its own error message, if the caller wants to + get more context on why the error happened. + + Do note that this error-reporting mechanism only reports the top-level error + message, not any source information embedded in the original Rust error + type. If you want more detailed information for debugging purposes, use the + logging interface. + + Args: + length: + The length of the buffer to allocate for the error message. If the + error message is longer than this, it will be truncated. + + Returns: + A string containing the error message, or None if there is no error + message. + + Raises: + RuntimeError: + If the error message could not be retrieved. + """ + buffer = ffi.new("char[]", length) + ret: int = lib.pactffi_get_error_message(buffer, length) + + if ret >= 0: + # While the documentation says that the return value is the number of bytes + # written, the actually return value is always 0 on success. + if msg := ffi.string(buffer): + if isinstance(msg, bytes): + return msg.decode("utf-8") + return msg + return None + if ret == -1: + msg = "The provided buffer is a null pointer." + elif ret == -2: # noqa: PLR2004 + # Instead of returning an error here, we call the function again with a + # larger buffer. + return get_error_message(length * 32) + elif ret == -3: # noqa: PLR2004 + msg = "The write failed for some other reason." + elif ret == -4: # noqa: PLR2004 + msg = "The error message had an interior NULL." + else: + msg = "An unknown error occurred." + raise RuntimeError(msg) + + +def log_to_stdout(level_filter: LevelFilter) -> int: + """ + Convenience function to direct all logging to stdout. + + [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_stdout) + """ + raise NotImplementedError + + +def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: + """ + Convenience function to direct all logging to stderr. + + [Rust + `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_stderr) + + Args: + level_filter: + The level of logs to filter to. If a string is given, it must match + one of the [`LevelFilter`][pact.v3.ffi.LevelFilter] values (case + insensitive). + + Raises: + RuntimeError: + If there was an error setting the logger. + """ + if isinstance(level_filter, str): + level_filter = LevelFilter[level_filter.upper()] + ret: int = lib.pactffi_log_to_stderr(level_filter.value) + if ret != 0: + msg = "There was an unknown error setting the logger." + raise RuntimeError(msg) + + +def log_to_file(file_name: str, level_filter: LevelFilter) -> int: + """ + Convenience function to direct all logging to a file. + + [Rust + `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_file) + + # Safety + + This function will fail if the file_name pointer is invalid or does not + point to a NULL terminated string. + """ + raise NotImplementedError + + +def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: + """ + Convenience function to direct all logging to a task local memory buffer. + + [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_buffer) + + Raises: + RuntimeError: + If there was an error setting the logger. + """ + if isinstance(level_filter, str): + level_filter = LevelFilter[level_filter.upper()] + ret: int = lib.pactffi_log_to_buffer(level_filter.value) + if ret != 0: + msg = "There was an unknown error setting the logger." + raise RuntimeError(msg) + + +def logger_init() -> None: + """ + Initialize the FFI logger with no sinks. + + [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_init) + + This initialized logger does nothing until `pactffi_logger_apply` has been called. + + # Usage + + ```c + pactffi_logger_init(); + ``` + + # Safety + + This function is always safe to call. + """ + raise NotImplementedError + + +def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: + """ + Attach an additional sink to the thread-local logger. + + [Rust + `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_attach_sink) + + This logger does nothing until `pactffi_logger_apply` has been called. + + Types of sinks can be specified: + + - stdout (`pactffi_logger_attach_sink("stdout", LevelFilter_Info)`) + - stderr (`pactffi_logger_attach_sink("stderr", LevelFilter_Debug)`) + - file w/ file path (`pactffi_logger_attach_sink("file /some/file/path", + LevelFilter_Trace)`) + - buffer (`pactffi_logger_attach_sink("buffer", LevelFilter_Debug)`) + + # Usage + + ```c + int result = pactffi_logger_attach_sink("file /some/file/path", LogLevel_Filter); + ``` + + # Error Handling + + The return error codes are as follows: + + - `-1`: Can't set logger (applying the logger failed, perhaps because one is + applied already). + - `-2`: No logger has been initialized (call `pactffi_logger_init` before + any other log function). + - `-3`: The sink specifier was not UTF-8 encoded. + - `-4`: The sink type specified is not a known type (known types: "stdout", + "stderr", or "file /some/path"). + - `-5`: No file path was specified in a file-type sink specification. + - `-6`: Opening a sink to the specified file path failed (check + permissions). + + # Safety + + This function checks the validity of the passed-in sink specifier, and + errors out if the specifier isn't valid UTF-8. Passing in an invalid or NULL + pointer will result in undefined behaviour. + """ + raise NotImplementedError + + +def logger_apply() -> int: + """ + Apply the previously configured sinks and levels to the program. + + [Rust + `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_apply) + + If no sinks have been setup, will set the log level to info and the target + to standard out. + + This function will install a global tracing subscriber. Any attempts to + modify the logger after the call to `logger_apply` will fail. + """ + raise NotImplementedError + + +def fetch_log_buffer(log_id: str) -> str: + """ + Fetch the in-memory logger buffer contents. + + [Rust + `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_fetch_log_buffer) + + This will only have any contents if the `buffer` sink has been configured to + log to. The contents will be allocated on the heap and will need to be freed + with `pactffi_string_delete`. + + Fetches the logs associated with the provided identifier, or uses the + "global" one if the identifier is not specified (i.e. NULL). + + Returns a NULL pointer if the buffer can't be fetched. This can occur is + there is not sufficient memory to make a copy of the contents or the buffer + contains non-UTF-8 characters. + + # Safety + + This function will fail if the log_id pointer is invalid or does not point + to a NULL terminated string. + """ + raise NotImplementedError + + +def parse_pact_json(json: str) -> Pact: + """ + Parses the provided JSON into a Pact model. + + [Rust + `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_parse_pact_json) + + The returned Pact model must be freed with the `pactffi_pact_model_delete` + function when no longer needed. + + # Error Handling + + This function will return a NULL pointer if passed a NULL pointer or if an + error occurs. + """ + raise NotImplementedError + + +def pact_model_delete(pact: Pact) -> None: + """ + Frees the memory used by the Pact model. + + [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_model_delete) + """ + raise NotImplementedError + + +def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: + """ + Returns an iterator over all the interactions in the Pact. + + [Rust + `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_model_interaction_iterator) + + The iterator will have to be deleted using the + `pactffi_pact_interaction_iter_delete` function. The iterator will contain a + copy of the interactions, so it will not be affected but mutations to the + Pact model and will still function if the Pact model is deleted. + + # Safety This function is safe as long as the Pact pointer is a valid + pointer. + + # Errors On any error, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def pact_spec_version(pact: Pact) -> PactSpecification: + """ + Returns the Pact specification enum that the Pact is for. + + [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_spec_version) + """ + raise NotImplementedError + + +def pact_interaction_delete(interaction: PactInteraction) -> None: + """ + Frees the memory used by the Pact interaction model. + + [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_delete) + """ + raise NotImplementedError + + +def async_message_new() -> AsynchronousMessage: + """ + Get a mutable pointer to a newly-created default message on the heap. + + [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_new) + + # Safety + + This function is safe. + + # Error Handling + + Returns NULL on error. + """ + raise NotImplementedError + + +def async_message_delete(message: AsynchronousMessage) -> None: + """ + Destroy the `AsynchronousMessage` being pointed to. + + [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_delete) + """ + lib.pactffi_async_message_delete(message._ptr) + + +def async_message_get_contents(message: AsynchronousMessage) -> MessageContents | None: + """ + Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. + + [Rust + `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents) + + If the message contents are missing, this function will return `None`. + """ + return MessageContents(lib.pactffi_async_message_get_contents(message._ptr)) + + +def async_message_generate_contents( + message: AsynchronousMessage, +) -> MessageContents | None: + """ + Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. + + This function differs from `async_message_get_contents` in + that it will process the message contents for any generators or matchers + that are present in the message in order to generate the actual message + contents as would be received by the consumer. + + [Rust + `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_generate_contents) + + If the message contents are missing, this function will return `None`. + """ + return MessageContents( + lib.pactffi_async_message_generate_contents(message._ptr), + owned=False, + ) + + +def async_message_get_contents_str(message: AsynchronousMessage) -> str: + """ + Get the message contents of an `AsynchronousMessage` in string form. + + [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_str) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + The returned string can outlive the message. + + # Error Handling + + If the message is NULL, returns NULL. If the body of the message + is missing, then this function also returns NULL. This means there's + no mechanism to differentiate with this function call alone between + a NULL message and a missing message body. + """ + raise NotImplementedError + + +def async_message_set_contents_str( + message: AsynchronousMessage, + contents: str, + content_type: str, +) -> None: + """ + Sets the contents of the message as a string. + + [Rust + `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_contents_str) + + - `message` - the message to set the contents for + - `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The message contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def async_message_get_contents_length(message: AsynchronousMessage) -> int: + """ + Get the length of the contents of a `AsynchronousMessage`. + + [Rust + `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_length) + + # Safety + + This function is safe. + + # Error Handling + + If the message is NULL, returns 0. If the body of the request is missing, + then this function also returns 0. + """ + raise NotImplementedError + + +def async_message_get_contents_bin(message: AsynchronousMessage) -> str: + """ + Get the contents of an `AsynchronousMessage` as bytes. + + [Rust + `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_bin) + + # Safety + + The number of bytes in the buffer will be returned by + `pactffi_async_message_get_contents_length`. It is safe to use the pointer + while the message is not deleted or changed. Using the pointer after the + message is mutated or deleted may lead to undefined behaviour. + + # Error Handling + + If the message is NULL, returns NULL. If the body of the message is missing, + then this function also returns NULL. + """ + raise NotImplementedError + + +def async_message_set_contents_bin( + message: AsynchronousMessage, + contents: str, + len: int, + content_type: str, +) -> None: + """ + Sets the contents of the message as an array of bytes. + + [Rust + `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_contents_bin) + + * `message` - the message to set the contents for + * `contents` - pointer to contents to copy from + * `len` - number of bytes to copy from the contents pointer + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def async_message_get_description(message: AsynchronousMessage) -> str: + r""" + Get a copy of the description. + + [Rust + `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_description) + + Raises: + RuntimeError: + If the description cannot be retrieved. + """ + ptr = lib.pactffi_async_message_get_description(message._ptr) + if ptr == ffi.NULL: + msg = "Unable to get the description from the message." + raise RuntimeError(msg) + return OwnedString(ptr) + + +def async_message_set_description( + message: AsynchronousMessage, + description: str, +) -> int: + """ + Write the `description` field on the `AsynchronousMessage`. + + [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_description) + + # Safety + + `description` must contain valid UTF-8. Invalid UTF-8 + will be replaced with U+FFFD REPLACEMENT CHARACTER. + + This function will only reallocate if the new string + does not fit in the existing buffer. + + # Error Handling + + Errors will be reported with a non-zero return value. + """ + raise NotImplementedError + + +def async_message_get_provider_state( + message: AsynchronousMessage, + index: int, +) -> ProviderState: + r""" + Get a copy of the provider state at the given index from this message. + + [Rust + `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_provider_state) + + Raises: + RuntimeError: + If the provider state cannot be retrieved. + """ + ptr = lib.pactffi_async_message_get_provider_state(message._ptr, index) + if ptr == ffi.NULL: + msg = "Unable to get the provider state from the message." + raise RuntimeError(msg) + return ProviderState(ptr) + + +def async_message_get_provider_state_iter( + message: AsynchronousMessage, +) -> ProviderStateIterator: + """ + Get an iterator over provider states. + + [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) + + # Safety + + The underlying data must not change during iteration. + """ + return ProviderStateIterator( + lib.pactffi_async_message_get_provider_state_iter(message._ptr) + ) + + +def consumer_get_name(consumer: Consumer) -> str: + r""" + Get a copy of this consumer's name. + + [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_consumer_get_name) + + The copy must be deleted with `pactffi_string_delete`. + + # Usage + + ```c + // Assuming `file_name` and `json_str` are already defined. + + MessagePact *message_pact = pactffi_message_pact_new_from_json(file_name, json_str); + if (message_pact == NULLPTR) { + // handle error. + } + + Consumer *consumer = pactffi_message_pact_get_consumer(message_pact); + if (consumer == NULLPTR) { + // handle error. + } + + char *name = pactffi_consumer_get_name(consumer); + if (name == NULL) { + // handle error. + } + + printf("%s\n", name); + + pactffi_string_delete(name); + ``` + + # Errors + + This function will fail if it is passed a NULL pointer, + or the Rust string contains an embedded NULL byte. + In the case of error, a NULL pointer will be returned. + """ + raise NotImplementedError + + +def pact_get_consumer(pact: Pact) -> Consumer: + """ + Get the consumer from a Pact. + + This returns a copy of the consumer model, and needs to be cleaned up with + `pactffi_pact_consumer_delete` when no longer required. + + [Rust + `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_get_consumer) + + # Errors + + This function will fail if it is passed a NULL pointer. In the case of + error, a NULL pointer will be returned. + """ + raise NotImplementedError + + +def pact_consumer_delete(consumer: Consumer) -> None: + """ + Frees the memory used by the Pact consumer. + + [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_consumer_delete) + """ + raise NotImplementedError + + +def message_contents_delete(contents: MessageContents) -> None: + """ + Delete the message contents instance. + + This should only be called on a message contents that require deletion. + The function creating the message contents should document whether it + requires deletion. + + Deleting a message content which is associated with an interaction + will result in undefined behaviour. + + [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_delete) + """ + lib.pactffi_message_contents_delete(contents._ptr) + + +def message_contents_get_contents_str(contents: MessageContents) -> str | None: + """ + Get the message contents in string form. + + [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_str) + + If the message has no contents or contain invalid UTF-8 characters, this + function will return `None`. + """ + ptr = lib.pactffi_message_contents_get_contents_str(contents._ptr) + if ptr == ffi.NULL: + return None + return OwnedString(ptr) + + +def message_contents_set_contents_str( + contents: MessageContents, + contents_str: str, + content_type: str, +) -> None: + """ + Sets the contents of the message as a string. + + [Rust + `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_set_contents_str) + + * `contents` - the message contents to set the contents for + * `contents_str` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The message contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents string is a NULL pointer, it will set the message contents + as null. If the content type is a null pointer, or can't be parsed, it will + set the content type as unknown. + """ + raise NotImplementedError + + +def message_contents_get_contents_length(contents: MessageContents) -> int: + """ + Get the length of the message contents. + + [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_length) + + If the message has not contents, this function will return 0. + """ + return lib.pactffi_message_contents_get_contents_length(contents._ptr) + + +def message_contents_get_contents_bin(contents: MessageContents) -> bytes | None: + """ + Get the contents of a message as a pointer to an array of bytes. + + [Rust + `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_bin) + + If the message has no contents, this function will return `None`. + """ + ptr = lib.pactffi_message_contents_get_contents_bin(contents._ptr) + if ptr == ffi.NULL: + return None + return ffi.buffer( + ptr, + lib.pactffi_message_contents_get_contents_length(contents._ptr), + )[:] + + +def message_contents_set_contents_bin( + contents: MessageContents, + contents_bin: str, + len: int, + content_type: str, +) -> None: + """ + Sets the contents of the message as an array of bytes. + + [Rust + `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_set_contents_bin) + + * `message` - the message contents to set the contents for + * `contents_bin` - pointer to contents to copy from + * `len` - number of bytes to copy from the contents pointer + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def message_contents_get_metadata_iter( + contents: MessageContents, +) -> MessageMetadataIterator: + r""" + Get an iterator over the metadata of a message. + + [Rust + `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) + + # Safety + + This iterator carries a pointer to the message contents, and must not + outlive the message. + + The message metadata also must not be modified during iteration. If it is, + the old iterator must be deleted and a new iterator created. + + Raises: + RuntimeError: + If the metadata iterator cannot be retrieved. + """ + ptr = lib.pactffi_message_contents_get_metadata_iter(contents._ptr) + if ptr == ffi.NULL: + msg = "Unable to get the metadata iterator from the message contents." + raise RuntimeError(msg) + return MessageMetadataIterator(ptr) + + +def message_contents_get_matching_rule_iter( + contents: MessageContents, + category: MatchingRuleCategory, +) -> MatchingRuleCategoryIterator: + r""" + Get an iterator over the matching rules for a category of a message. + + [Rust + `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) + + The returned pointer must be deleted with + `pactffi_matching_rules_iter_delete` when done with it. + + Note that there could be multiple matching rules for the same key, so this + iterator will sequentially return each rule with the same key. + + For sample, given the following rules: + + ``` + "$.a" => Type, + "$.b" => Regex("\\d+"), Number + ``` + + This iterator will return a sequence of 3 values: + + - `("$.a", Type)` + - `("$.b", Regex("\d+"))` + - `("$.b", Number)` + + # Safety + + The iterator contains a copy of the data, so is safe to use when the message + or message contents has been deleted. + + # Error Handling + + On failure, this function will return a NULL pointer. + """ + return MatchingRuleCategoryIterator( + lib.pactffi_message_contents_get_matching_rule_iter(contents._ptr, category) + ) + + +def request_contents_get_matching_rule_iter( + request: HttpRequest, + category: MatchingRuleCategory, +) -> MatchingRuleCategoryIterator: + r""" + Get an iterator over the matching rules for a category of an HTTP request. + + [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) + + The returned pointer must be deleted with + `pactffi_matching_rules_iter_delete` when done with it. + + For sample, given the following rules: + + ``` + "$.a" => Type, + "$.b" => Regex("\d+"), Number + ``` + + This iterator will return a sequence of 3 values: + + - `("$.a", Type)` + - `("$.b", Regex("\d+"))` + - `("$.b", Number)` + + # Safety + + The iterator contains a copy of the data, so is safe to use when the + interaction or request contents has been deleted. + + # Error Handling + + On failure, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def response_contents_get_matching_rule_iter( + response: HttpResponse, + category: MatchingRuleCategory, +) -> MatchingRuleCategoryIterator: + r""" + Get an iterator over the matching rules for a category of an HTTP response. + + [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) + + The returned pointer must be deleted with + `pactffi_matching_rules_iter_delete` when done with it. + + For sample, given the following rules: + + ``` + "$.a" => Type, + "$.b" => Regex("\d+"), Number + ``` + + This iterator will return a sequence of 3 values: + + - `("$.a", Type)` + - `("$.b", Regex("\d+"))` + - `("$.b", Number)` + + # Safety + + The iterator contains a copy of the data, so is safe to use when the + interaction or response contents has been deleted. + + # Error Handling + + On failure, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def message_contents_get_generators_iter( + contents: MessageContents, + category: GeneratorCategory, +) -> GeneratorCategoryIterator: + """ + Get an iterator over the generators for a category of a message. + + [Rust + `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_generators_iter) + + # Safety + + The iterator contains a copy of the data, so is safe to use when the message + or message contents has been deleted. + + Raises: + RuntimeError: + If the generators iterator cannot be retrieved. + """ + ptr = lib.pactffi_message_contents_get_generators_iter(contents._ptr, category) + if ptr == ffi.NULL: + msg = "Unable to get the generators iterator from the message contents." + raise RuntimeError(msg) + return GeneratorCategoryIterator(ptr) + + +def request_contents_get_generators_iter( + request: HttpRequest, + category: GeneratorCategory, +) -> GeneratorCategoryIterator: + """ + Get an iterator over the generators for a category of an HTTP request. + + [Rust + `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_request_contents_get_generators_iter) + + The returned pointer must be deleted with `pactffi_generators_iter_delete` + when done with it. + + # Safety + + The iterator contains a copy of the data, so is safe to use when the + interaction or request contents has been deleted. + + # Error Handling + + On failure, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def response_contents_get_generators_iter( + response: HttpResponse, + category: GeneratorCategory, +) -> GeneratorCategoryIterator: + """ + Get an iterator over the generators for a category of an HTTP response. + + [Rust + `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_contents_get_generators_iter) + + The returned pointer must be deleted with `pactffi_generators_iter_delete` + when done with it. + + # Safety + + The iterator contains a copy of the data, so is safe to use when the + interaction or response contents has been deleted. + + # Error Handling + + On failure, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: + """ + Parse a matcher definition string into a MatchingRuleDefinition. + + The MatchingRuleDefinition contains the example value, and matching rules and + any generator. + + [Rust + `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_parse_matcher_definition) + + The following are examples of matching rule definitions: + + * `matching(type,'Name')` - type matcher with string value 'Name' + * `matching(number,100)` - number matcher + * `matching(datetime, 'yyyy-MM-dd','2000-01-01')` - datetime matcher with + format string + + See [Matching Rule definition + expressions](https://docs.rs/pact_models/latest/pact_models/matchingrules/expressions/index.html). + + The returned value needs to be freed up with the + `pactffi_matcher_definition_delete` function. + + # Errors If the expression is invalid, the MatchingRuleDefinition error will + be set. You can check for this value with the + `pactffi_matcher_definition_error` function. + + # Safety + + This function is safe if the expression is a valid NULL terminated string + pointer. + """ + raise NotImplementedError + + +def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: + """ + Returns any error message from parsing a matching definition expression. + + If there is no error, it will return a NULL pointer, otherwise returns the + error message as a NULL-terminated string. The returned string must be freed + using the `pactffi_string_delete` function once done with it. + + [Rust + `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_error) + """ + raise NotImplementedError + + +def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: + """ + Returns the value from parsing a matching definition expression. + + If there was an error, it will return a NULL pointer, otherwise returns the + value as a NULL-terminated string. The returned string must be freed using + the `pactffi_string_delete` function once done with it. + + [Rust + `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_value) + + Note that different expressions values can have types other than a string. + Use `pactffi_matcher_definition_value_type` to get the actual type of the + value. This function will always return the string representation of the + value. + """ + raise NotImplementedError + + +def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: + """ + Frees the memory used by the result of parsing the matching definition expression. + + [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_delete) + """ + raise NotImplementedError + + +def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Generator: + """ + Returns the generator from parsing a matching definition expression. + + If there was an error or there is no associated generator, it will return a + NULL pointer, otherwise returns the generator as a pointer. + + [Rust + `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_generator) + + The generator pointer will be a valid pointer as long as + `pactffi_matcher_definition_delete` has not been called on the definition. + Using the generator pointer after the definition has been deleted will + result in undefined behaviour. + """ + raise NotImplementedError + + +def matcher_definition_value_type( + definition: MatchingRuleDefinitionResult, +) -> ExpressionValueType: + """ + Returns the type of the value from parsing a matching definition expression. + + If there was an error parsing the expression, it will return Unknown. + + [Rust + `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_value_type) + """ + raise NotImplementedError + + +def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_iter_delete) + """ + raise NotImplementedError + + +def matcher_definition_iter( + definition: MatchingRuleDefinitionResult, +) -> MatchingRuleIterator: + """ + Returns an iterator over the matching rules from the parsed definition. + + The iterator needs to be deleted with the + `pactffi_matching_rule_iter_delete` function once done with it. + + [Rust + `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_iter) + + If there was an error parsing the expression, this function will return a + NULL pointer. + """ + raise NotImplementedError + + +def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: + """ + Get the next matching rule or reference from the iterator. + + As the values returned are owned by the iterator, they do not need to be + deleted but will be cleaned up when the iterator is deleted. + + [Rust + `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_iter_next) + + Will return a NULL pointer when the iterator has advanced past the end of + the list. + + # Safety + + This function is safe. + + # Error Handling + + This function will return a NULL pointer if passed a NULL pointer or if an + error occurs. + """ + raise NotImplementedError + + +def matching_rule_id(rule_result: MatchingRuleResult) -> int: + """ + Return the ID of the matching rule. + + [Rust + `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_id) + + The ID corresponds to the following rules: + + | Rule | ID | + | ---- | -- | + | Equality | 1 | + | Regex | 2 | + | Type | 3 | + | MinType | 4 | + | MaxType | 5 | + | MinMaxType | 6 | + | Timestamp | 7 | + | Time | 8 | + | Date | 9 | + | Include | 10 | + | Number | 11 | + | Integer | 12 | + | Decimal | 13 | + | Null | 14 | + | ContentType | 15 | + | ArrayContains | 16 | + | Values | 17 | + | Boolean | 18 | + | StatusCode | 19 | + | NotEmpty | 20 | + | Semver | 21 | + | EachKey | 22 | + | EachValue | 23 | + + # Safety + + This function is safe as long as the MatchingRuleResult pointer is a valid + pointer and the iterator has not been deleted. + """ + raise NotImplementedError + + +def matching_rule_value(rule_result: MatchingRuleResult) -> str: + """ + Returns the associated value for the matching rule. + + If the matching rule does not have an associated value, will return a NULL + pointer. + + [Rust + `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_value) + + The associated values for the rules are: + + | Rule | ID | VALUE | + | ---- | -- | ----- | + | Equality | 1 | NULL | + | Regex | 2 | Regex value | + | Type | 3 | NULL | + | MinType | 4 | Minimum value | + | MaxType | 5 | Maximum value | + | MinMaxType | 6 | "min:max" | + | Timestamp | 7 | Format string | + | Time | 8 | Format string | + | Date | 9 | Format string | + | Include | 10 | String value | + | Number | 11 | NULL | + | Integer | 12 | NULL | + | Decimal | 13 | NULL | + | Null | 14 | NULL | + | ContentType | 15 | Content type | + | ArrayContains | 16 | NULL | + | Values | 17 | NULL | + | Boolean | 18 | NULL | + | StatusCode | 19 | NULL | + | NotEmpty | 20 | NULL | + | Semver | 21 | NULL | + | EachKey | 22 | NULL | + | EachValue | 23 | NULL | + + Will return a NULL pointer if the matching rule was a reference or does not + have an associated value. + + # Safety + + This function is safe as long as the MatchingRuleResult pointer is a valid + pointer and the iterator it came from has not been deleted. + """ + raise NotImplementedError + + +def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: + """ + Returns the matching rule pointer for the matching rule. + + Will return a NULL pointer if the matching rule result was a reference. + + [Rust + `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_pointer) + + # Safety + + This function is safe as long as the MatchingRuleResult pointer is a valid + pointer and the iterator it came from has not been deleted. + """ + raise NotImplementedError + + +def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: + """ + Return any matching rule reference to a attribute by name. + + This is when the matcher should be configured to match the type of a + structure. I.e., + + [Rust + `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_reference_name) + + ```json + { + "pact:match": "eachValue(matching($'person'))", + "person": { + "name": "Fred", + "age": 100 + } + } + ``` + + Will return a NULL pointer if the matching rule was not a reference. + + # Safety + + This function is safe as long as the MatchingRuleResult pointer is a valid + pointer and the iterator has not been deleted. + """ + raise NotImplementedError + + +def validate_datetime(value: str, format: str) -> None: + """ + Validates the date/time value against the date/time format string. + + [Rust + `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_validate_datetime) + + Raises: + ValueError: + If the value is not a valid date/time for the format string. + + RuntimeError: + For any other error. + """ + ret = lib.pactffi_validate_datetime(value.encode(), format.encode()) + if ret == 0: + return + if ret == 1: + msg = f"Invalid datetime value {value!r}' for format {format!r}" + raise ValueError(msg) + if ret == 2: # noqa: PLR2004 + msg = f"Panic while validating datetime value: {get_error_message()}" + else: + msg = f"Unknown error while validating datetime value: {ret}" + raise RuntimeError(msg) + + +def generator_to_json(generator: Generator) -> str: + """ + Get the JSON form of the generator. + + [Rust + `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_to_json) + + The returned string must be deleted with `pactffi_string_delete`. + + # Safety + + This function will fail if it is passed a NULL pointer, or the owner of the + generator has been deleted. + """ + return OwnedString(lib.pactffi_generator_to_json(generator._ptr)) + + +def generator_generate_string(generator: Generator, context_json: str) -> str: + """ + Generate a string value using the provided generator. + + An optional JSON payload containing any generator context ca be given. The + context value is used for generators like `MockServerURL` (which should + contain details about the running mock server) and `ProviderStateGenerator` + (which should be the values returned from the Provider State callback + function). + + [Rust + `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_generate_string) + + If anything goes wrong, it will return a NULL pointer. + """ + ptr = lib.pactffi_generator_generate_string( + generator._ptr, + context_json.encode("utf-8"), + ) + s = ffi.string(ptr) + if isinstance(s, bytes): + s = s.decode("utf-8") + return s + + +def generator_generate_integer(generator: Generator, context_json: str) -> int: + """ + Generate an integer value using the provided generator. + + An optional JSON payload containing any generator context can be given. The + context value is used for generators like `ProviderStateGenerator` (which + should be the values returned from the Provider State callback function). + + [Rust + `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_generate_integer) + + If anything goes wrong or the generator is not a type that can generate an + integer value, it will return a zero value. + """ + return lib.pactffi_generator_generate_integer( + generator._ptr, + context_json.encode("utf-8"), + ) + + +def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_delete) + """ + lib.pactffi_generators_iter_delete(iter._ptr) + + +def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePair: + """ + Get the next path and generator out of the iterator, if possible. + + [Rust + `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_next) + + The returned pointer must be deleted with + `pactffi_generator_iter_pair_delete`. + + Raises: + StopIteration: + If the iterator has reached the end. + """ + ptr = lib.pactffi_generators_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return GeneratorKeyValuePair(ptr) + + +def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: + """ + Free a pair of key and value returned from `pactffi_generators_iter_next`. + + [Rust + `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_pair_delete) + """ + lib.pactffi_generators_iter_pair_delete(pair._ptr) + + +def sync_http_new() -> SynchronousHttp: + """ + Get a mutable pointer to a newly-created default interaction on the heap. + + [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_new) + + # Safety + + This function is safe. + + # Error Handling + + Returns NULL on error. + """ + raise NotImplementedError + + +def sync_http_delete(interaction: SynchronousHttp) -> None: + """ + Destroy the `SynchronousHttp` interaction being pointed to. + + [Rust + `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_delete) + """ + lib.pactffi_sync_http_delete(interaction) + + +def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: + """ + Get the request of a `SynchronousHttp` interaction. + + [Rust + `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request) + + # Safety + + The data pointed to by the pointer this function returns will be deleted + when the interaction is deleted. Trying to use if after the interaction is + deleted will result in undefined behaviour. + + # Error Handling + + If the interaction is NULL, returns NULL. + """ + raise NotImplementedError + + +def sync_http_get_request_contents(interaction: SynchronousHttp) -> str | None: + """ + Get the request contents of a `SynchronousHttp` interaction in string form. + + [Rust + `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents) + + Note that this function will return `None` if either the body is missing or + is `null`. + """ + ptr = lib.pactffi_sync_http_get_request_contents(interaction._ptr) + if ptr == ffi.NULL: + return None + return OwnedString(ptr) + + +def sync_http_set_request_contents( + interaction: SynchronousHttp, + contents: str, + content_type: str, +) -> None: + """ + Sets the request contents of the interaction. + + [Rust + `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_request_contents) + + - `interaction` - the interaction to set the request contents for + - `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The request contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the request contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: + """ + Get the length of the request contents of a `SynchronousHttp` interaction. + + [Rust + `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) + + This function will return 0 if the body is missing. + """ + return lib.pactffi_sync_http_get_request_contents_length(interaction._ptr) + + +def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes | None: + """ + Get the request contents of a `SynchronousHttp` interaction as bytes. + + [Rust + `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) + + Note that this function will return `None` if either the body is missing or + is `null`. + """ + ptr = lib.pactffi_sync_http_get_request_contents_bin(interaction._ptr) + if ptr == ffi.NULL: + return None + return ffi.buffer( + ptr, + sync_http_get_request_contents_length(interaction), + )[:] + + +def sync_http_set_request_contents_bin( + interaction: SynchronousHttp, + contents: str, + len: int, + content_type: str, +) -> None: + """ + Sets the request contents of the interaction as an array of bytes. + + [Rust + `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) + + - `interaction` - the interaction to set the request contents for + - `contents` - pointer to contents to copy from + - `len` - number of bytes to copy from the contents pointer + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the request contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: + """ + Get the response of a `SynchronousHttp` interaction. + + [Rust + `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response) + + # Safety + + The data pointed to by the pointer this function returns will be deleted + when the interaction is deleted. Trying to use if after the interaction is + deleted will result in undefined behaviour. + + # Error Handling + + If the interaction is NULL, returns NULL. + """ + raise NotImplementedError + + +def sync_http_get_response_contents(interaction: SynchronousHttp) -> str | None: + """ + Get the response contents of a `SynchronousHttp` interaction in string form. + + [Rust + `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents) + + Note that this function will return `None` if either the body is missing or + is `null`. + """ + ptr = lib.pactffi_sync_http_get_response_contents(interaction._ptr) + if ptr == ffi.NULL: + return None + return OwnedString(ptr) + + +def sync_http_set_response_contents( + interaction: SynchronousHttp, + contents: str, + content_type: str, +) -> None: + """ + Sets the response contents of the interaction. + + [Rust + `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_response_contents) + + - `interaction` - the interaction to set the response contents for + - `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The response contents and content type must either be NULL pointers, or + point to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the response contents as + null. If the content type is a null pointer, or can't be parsed, it will set + the content type as unknown. + """ + raise NotImplementedError + + +def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: + """ + Get the length of the response contents of a `SynchronousHttp` interaction. + + [Rust + `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) + + This function will return 0 if the body is missing. + """ + return lib.pactffi_sync_http_get_response_contents_length(interaction._ptr) + + +def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes | None: + """ + Get the response contents of a `SynchronousHttp` interaction as bytes. + + [Rust + `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) + + Note that this function will return `None` if either the body is missing or + is `null`. + """ + ptr = lib.pactffi_sync_http_get_response_contents_bin(interaction._ptr) + if ptr == ffi.NULL: + return None + return ffi.buffer( + ptr, + sync_http_get_response_contents_length(interaction), + )[:] + + +def sync_http_set_response_contents_bin( + interaction: SynchronousHttp, + contents: str, + len: int, + content_type: str, +) -> None: + """ + Sets the response contents of the `SynchronousHttp` interaction as bytes. + + [Rust + `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) + + - `interaction` - the interaction to set the response contents for + - `contents` - pointer to contents to copy from + - `len` - number of bytes to copy + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the response contents as + null. If the content type is a null pointer, or can't be parsed, it will set + the content type as unknown. + """ + raise NotImplementedError + + +def sync_http_get_description(interaction: SynchronousHttp) -> str: + r""" + Get a copy of the description. + + [Rust + `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_description) + + Raises: + RuntimeError: + If the description cannot be retrieved + """ + ptr = lib.pactffi_sync_http_get_description(interaction._ptr) + if ptr == ffi.NULL: + msg = "Failed to get description" + raise RuntimeError(msg) + return OwnedString(ptr) + + +def sync_http_set_description(interaction: SynchronousHttp, description: str) -> int: + """ + Write the `description` field on the `SynchronousHttp`. + + [Rust + `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_description) + + # Safety + + `description` must contain valid UTF-8. Invalid UTF-8 will be replaced with + U+FFFD REPLACEMENT CHARACTER. + + This function will only reallocate if the new string does not fit in the + existing buffer. + + # Error Handling + + Errors will be reported with a non-zero return value. + """ + raise NotImplementedError + + +def sync_http_get_provider_state( + interaction: SynchronousHttp, + index: int, +) -> ProviderState: + r""" + Get a copy of the provider state at the given index from this interaction. + + [Rust + `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_provider_state) + + # Safety + + The returned structure must be deleted with `provider_state_delete`. + + Since it is a copy, the returned structure may safely outlive the + `SynchronousHttp`. + + # Error Handling + + On failure, this function will return a variant other than Success. + + This function may fail if the index requested is out of bounds, or if any of + the Rust strings contain embedded null ('\0') bytes. + """ + raise NotImplementedError + + +def sync_http_get_provider_state_iter( + interaction: SynchronousHttp, +) -> ProviderStateIterator: + """ + Get an iterator over provider states. + + [Rust + `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) + + # Safety + + The underlying data must not change during iteration. + + Raises: + RuntimeError: + If the iterator cannot be retrieved + """ + ptr = lib.pactffi_sync_http_get_provider_state_iter(interaction._ptr) + if ptr == ffi.NULL: + msg = "Failed to get provider state iterator" + raise RuntimeError(msg) + return ProviderStateIterator(ptr) + + +def pact_interaction_as_synchronous_http( + interaction: PactInteraction, +) -> SynchronousHttp: + r""" + Casts this interaction to a `SynchronousHttp` interaction. + + Returns a NULL pointer if the interaction can not be casted to a + `SynchronousHttp` interaction (for instance, it is a message interaction). + The returned pointer must be freed with `pactffi_sync_http_delete` when no + longer required. + + [Rust + `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) + + # Safety This function is safe as long as the interaction pointer is a valid + pointer. + + # Errors On any error, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def pact_interaction_as_asynchronous_message( + interaction: PactInteraction, +) -> AsynchronousMessage: + """ + Casts this interaction to a `AsynchronousMessage` interaction. + + Returns a NULL pointer if the interaction can not be casted to a + `AsynchronousMessage` interaction (for instance, it is a http interaction). + The returned pointer must be freed with `pactffi_async_message_delete` when + no longer required. + + [Rust + `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) + + Note that if the interaction is a V3 `Message`, it will be converted to a V4 + `AsynchronousMessage` before being returned. + + # Safety This function is safe as long as the interaction pointer is a valid + pointer. + + # Errors On any error, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def pact_interaction_as_synchronous_message( + interaction: PactInteraction, +) -> SynchronousMessage: + """ + Casts this interaction to a `SynchronousMessage` interaction. + + Returns a NULL pointer if the interaction can not be casted to a + `SynchronousMessage` interaction (for instance, it is a http interaction). + The returned pointer must be freed with `pactffi_sync_message_delete` when + no longer required. + + [Rust + `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) + + # Safety This function is safe as long as the interaction pointer is a valid + pointer. + + # Errors On any error, this function will return a NULL pointer. + """ + raise NotImplementedError + + +def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> AsynchronousMessage: + """ + Get the next asynchronous message from the iterator. + + [Rust + `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_async_message_iter_next) + + Raises: + StopIteration: + If the iterator has reached the end. + """ + ptr = lib.pactffi_pact_async_message_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return AsynchronousMessage(ptr, owned=True) + + +def pact_async_message_iter_delete(iter: PactAsyncMessageIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_async_message_iter_delete) + """ + lib.pactffi_pact_async_message_iter_delete(iter._ptr) + + +def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMessage: + """ + Get the next synchronous request/response message from the V4 pact. + + [Rust + `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_message_iter_next) + + Raises: + StopIteration: + If the iterator has reached the end. + """ + ptr = lib.pactffi_pact_sync_message_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return SynchronousMessage(ptr, owned=True) + + +def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) + """ + lib.pactffi_pact_sync_message_iter_delete(iter._ptr) + + +def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: + """ + Get the next synchronous HTTP request/response interaction from the V4 pact. + + [Rust + `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_http_iter_next) + + Raises: + StopIteration: + If the iterator has reached the end. + """ + ptr = lib.pactffi_pact_sync_http_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return SynchronousHttp(ptr, owned=True) + + +def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) + """ + lib.pactffi_pact_sync_http_iter_delete(iter._ptr) + + +def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction: + """ + Get the next interaction from the pact. + + [Rust + `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_iter_next) + + Raises: + StopIteration: + If the iterator has reached the end. + """ + ptr = lib.pactffi_pact_interaction_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + raise NotImplementedError + return PactInteraction(ptr) + + +def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_iter_delete) + """ + lib.pactffi_pact_interaction_iter_delete(iter._ptr) + + +def matching_rule_to_json(rule: MatchingRule) -> str: + """ + Get the JSON form of the matching rule. + + [Rust + `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_to_json) + + The returned string must be deleted with `pactffi_string_delete`. + + # Safety + + This function will fail if it is passed a NULL pointer, or the iterator that + owns the value of the matching rule has been deleted. + """ + return OwnedString(lib.pactffi_matching_rule_to_json(rule._ptr)) + + +def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_delete) + """ + lib.pactffi_matching_rules_iter_delete(iter._ptr) + + +def matching_rules_iter_next( + iter: MatchingRuleCategoryIterator, +) -> MatchingRuleKeyValuePair: + """ + Get the next path and matching rule out of the iterator, if possible. + + [Rust + `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_next) + + The returned pointer must be deleted with + `pactffi_matching_rules_iter_pair_delete`. + + # Safety + + The underlying data is owned by the `MatchingRuleKeyValuePair`, so is always + safe to use. + + # Error Handling + + If no further data is present, returns NULL. + """ + return MatchingRuleKeyValuePair(lib.pactffi_matching_rules_iter_next(iter._ptr)) + + +def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: + """ + Free a pair of key and value returned from `message_metadata_iter_next`. + + [Rust + `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) + """ + lib.pactffi_matching_rules_iter_pair_delete(pair._ptr) + + +def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: + """ + Get the next value from the iterator. + + [Rust + `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_iter_next) + + # Safety + + The underlying data must not change during iteration. + + Raises: + StopIteration: + If no further data is present, or if an internal error occurs. + """ + provider_state = lib.pactffi_provider_state_iter_next(iter._ptr) + if provider_state == ffi.NULL: + raise StopIteration + return ProviderState(provider_state) + + +def provider_state_iter_delete(iter: ProviderStateIterator) -> None: + """ + Delete the iterator. + + [Rust + `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_iter_delete) + """ + lib.pactffi_provider_state_iter_delete(iter._ptr) + + +def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadataPair: + """ + Get the next key and value out of the iterator, if possible. + + [Rust + `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_iter_next) + + The returned pointer must be deleted with + `pactffi_message_metadata_pair_delete`. + + # Safety + + The underlying data must not change during iteration. This function must + only ever be called from a foreign language. Calling it from a Rust function + that has a Tokio runtime in its call stack can result in a deadlock. + + Raises: + StopIteration: + If no further data is present. + """ + ptr = lib.pactffi_message_metadata_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return MessageMetadataPair(ptr) + + +def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: + """ + Free the metadata iterator when you're done using it. + + [Rust + `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_iter_delete) + """ + lib.pactffi_message_metadata_iter_delete(iter._ptr) + + +def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: + """ + Free a pair of key and value returned from `message_metadata_iter_next`. + + [Rust + `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_pair_delete) + """ + lib.pactffi_message_metadata_pair_delete(pair._ptr) + + +def provider_get_name(provider: Provider) -> str: + r""" + Get a copy of this provider's name. + + [Rust + `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_get_name) + + The copy must be deleted with `pactffi_string_delete`. + + # Usage + + ```c + // Assuming `file_name` and `json_str` are already defined. + + MessagePact *message_pact = pactffi_message_pact_new_from_json(file_name, json_str); + if (message_pact == NULLPTR) { + // handle error. + } + + Provider *provider = pactffi_message_pact_get_provider(message_pact); + if (provider == NULLPTR) { + // handle error. + } + + char *name = pactffi_provider_get_name(provider); + if (name == NULL) { + // handle error. + } + + printf("%s\n", name); + + pactffi_string_delete(name); + ``` + + # Errors + + This function will fail if it is passed a NULL pointer, or the Rust string + contains an embedded NULL byte. In the case of error, a NULL pointer will be + returned. + """ + raise NotImplementedError + + +def pact_get_provider(pact: Pact) -> Provider: + """ + Get the provider from a Pact. + + This returns a copy of the provider model, and needs to be cleaned up with + `pactffi_pact_provider_delete` when no longer required. + + [Rust + `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_get_provider) + + # Errors + + This function will fail if it is passed a NULL pointer. In the case of + error, a NULL pointer will be returned. + """ + raise NotImplementedError + + +def pact_provider_delete(provider: Provider) -> None: + """ + Frees the memory used by the Pact provider. + + [Rust + `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_provider_delete) + """ + raise NotImplementedError + + +def provider_state_get_name(provider_state: ProviderState) -> str | None: + """ + Get the name of the provider state as a string. + + [Rust + `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_get_name) + + Raises: + RuntimeError: + If the name could not be retrieved. + """ + ptr = lib.pactffi_provider_state_get_name(provider_state._ptr) + if ptr == ffi.NULL: + msg = "Failed to get provider state name." + raise RuntimeError(msg) + return OwnedString(ptr) + + +def provider_state_get_param_iter( + provider_state: ProviderState, +) -> ProviderStateParamIterator: + r""" + Get an iterator over the params of a provider state. + + [Rust + `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_get_param_iter) + + # Safety + + This iterator carries a pointer to the provider state, and must not outlive + the provider state. + + The provider state params also must not be modified during iteration. If it + is, the old iterator must be deleted and a new iterator created. + + Raises: + RuntimeError: + If the iterator could not be created. + """ + ptr = lib.pactffi_provider_state_get_param_iter(provider_state._ptr) + if ptr == ffi.NULL: + msg = "Failed to get provider state param iterator." + raise RuntimeError(msg) + return ProviderStateParamIterator(ptr) + + +def provider_state_param_iter_next( + iter: ProviderStateParamIterator, +) -> ProviderStateParamPair: + """ + Get the next key and value out of the iterator, if possible. + + [Rust + `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_iter_next) + + # Safety + + The underlying data must not be modified during iteration. + + Raises: + StopIteration: + If no further data is present. + """ + provider_state_param = lib.pactffi_provider_state_param_iter_next(iter._ptr) + if provider_state_param == ffi.NULL: + raise StopIteration + return ProviderStateParamPair(provider_state_param) + + +def provider_state_delete(provider_state: ProviderState) -> None: + """ + Free the provider state when you're done using it. + + [Rust + `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_delete) + """ + raise NotImplementedError + + +def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: + """ + Free the provider state param iterator when you're done using it. + + [Rust + `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_iter_delete) + """ + lib.pactffi_provider_state_param_iter_delete(iter._ptr) + + +def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: + """ + Free a pair of key and value. + + [Rust + `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_pair_delete) + """ + lib.pactffi_provider_state_param_pair_delete(pair._ptr) + + +def sync_message_new() -> SynchronousMessage: + """ + Get a mutable pointer to a newly-created default message on the heap. + + [Rust + `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_new) + + # Safety + + This function is safe. + + # Error Handling + + Returns NULL on error. + """ + raise NotImplementedError + + +def sync_message_delete(message: SynchronousMessage) -> None: + """ + Destroy the `Message` being pointed to. + + [Rust + `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_delete) + """ + lib.pactffi_sync_message_delete(message._ptr) + + +def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: + """ + Get the request contents of a `SynchronousMessage` in string form. + + [Rust + `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + The returned string can outlive the message. + + # Error Handling + + If the message is NULL, returns NULL. If the body of the request message is + missing, then this function also returns NULL. This means there's no + mechanism to differentiate with this function call alone between a NULL + message and a missing message body. + """ + raise NotImplementedError + + +def sync_message_set_request_contents_str( + message: SynchronousMessage, + contents: str, + content_type: str, +) -> None: + """ + Sets the request contents of the message. + + [Rust + `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) + + - `message` - the message to set the request contents for + - `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The message contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def sync_message_get_request_contents_length(message: SynchronousMessage) -> int: + """ + Get the length of the request contents of a `SynchronousMessage`. + + [Rust + `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) + + # Safety + + This function is safe. + + # Error Handling + + If the message is NULL, returns 0. If the body of the request is missing, + then this function also returns 0. + """ + raise NotImplementedError + + +def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes: + """ + Get the request contents of a `SynchronousMessage` as a bytes. + + [Rust + `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) + + # Safety + + The number of bytes in the buffer will be returned by + `pactffi_sync_message_get_request_contents_length`. It is safe to use the + pointer while the message is not deleted or changed. Using the pointer after + the message is mutated or deleted may lead to undefined behaviour. + + # Error Handling + + If the message is NULL, returns NULL. If the body of the message is missing, + then this function also returns NULL. + """ + raise NotImplementedError + + +def sync_message_set_request_contents_bin( + message: SynchronousMessage, + contents: str, + len: int, + content_type: str, +) -> None: + """ + Sets the request contents of the message as an array of bytes. + + [Rust + `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) + + * `message` - the message to set the request contents for + * `contents` - pointer to contents to copy from + * `len` - number of bytes to copy from the contents pointer + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def sync_message_get_request_contents(message: SynchronousMessage) -> MessageContents: + """ + Get the request contents of an `SynchronousMessage` as a `MessageContents`. + + [Rust + `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents) + + # Safety + + The data pointed to by the pointer this function returns will be deleted + when the message is deleted. Trying to use if after the message is deleted + will result in undefined behaviour. + + # Error Handling + + If the message is NULL, returns NULL. + """ + raise NotImplementedError + + +def sync_message_generate_request_contents( + message: SynchronousMessage, +) -> MessageContents: + """ + Get the request contents of an `SynchronousMessage` as a `MessageContents`. + + This function differs from `pactffi_sync_message_get_request_contents` in + that it will process the message contents for any generators or matchers + that are present in the message in order to generate the actual message + contents as would be received by the consumer. + + [Rust + `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_generate_request_contents) + + Raises: + RuntimeError: + If the request contents cannot be generated + """ + ptr = lib.pactffi_sync_message_generate_request_contents(message._ptr) + if ptr == ffi.NULL: + msg = "Failed to generate request contents" + raise RuntimeError(msg) + return MessageContents(ptr, owned=False) + + +def sync_message_get_number_responses(message: SynchronousMessage) -> int: + """ + Get the number of response messages in the `SynchronousMessage`. + + [Rust + `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_number_responses) + + If the message is null, this function will return 0. + """ + return lib.pactffi_sync_message_get_number_responses(message._ptr) + + +def sync_message_get_response_contents_str( + message: SynchronousMessage, + index: int, +) -> str: + """ + Get the response contents of a `SynchronousMessage` in string form. + + [Rust + `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) + + # Safety + + The returned string must be deleted with `pactffi_string_delete`. + + The returned string can outlive the message. + + # Error Handling + + If the message is NULL or the index is not valid, returns NULL. + + If the body of the response message is missing, then this function also + returns NULL. This means there's no mechanism to differentiate with this + function call alone between a NULL message and a missing message body. + """ + raise NotImplementedError + + +def sync_message_set_response_contents_str( + message: SynchronousMessage, + index: int, + contents: str, + content_type: str, +) -> None: + """ + Sets the response contents of the message as a string. + + If index is greater + than the number of responses in the message, the responses will be padded + with default values. + + [Rust + `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) + + * `message` - the message to set the response contents for + * `index` - index of the response to set. 0 is the first response. + * `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The message contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the response contents as + null. If the content type is a null pointer, or can't be parsed, it will set + the content type as unknown. + """ + raise NotImplementedError + + +def sync_message_get_response_contents_length( + message: SynchronousMessage, + index: int, +) -> int: + """ + Get the length of the response contents of a `SynchronousMessage`. + + [Rust + `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) + + # Safety + + This function is safe. + + # Error Handling + + If the message is NULL or the index is not valid, returns 0. If the body of + the request is missing, then this function also returns 0. + """ + raise NotImplementedError + + +def sync_message_get_response_contents_bin( + message: SynchronousMessage, + index: int, +) -> bytes: + """ + Get the response contents of a `SynchronousMessage` as bytes. + + [Rust + `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) + + # Safety + + The number of bytes in the buffer will be returned by + `pactffi_sync_message_get_response_contents_length`. It is safe to use the + pointer while the message is not deleted or changed. Using the pointer after + the message is mutated or deleted may lead to undefined behaviour. + + # Error Handling + + If the message is NULL or the index is not valid, returns NULL. If the body + of the message is missing, then this function also returns NULL. + """ + raise NotImplementedError + + +def sync_message_set_response_contents_bin( + message: SynchronousMessage, + index: int, + contents: str, + len: int, + content_type: str, +) -> None: + """ + Sets the response contents of the message at the given index as bytes. + + If index is greater than the number of responses in the message, the + responses will be padded with default values. + + [Rust + `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) + + * `message` - the message to set the response contents for + * `index` - index of the response to set. 0 is the first response + * `contents` - pointer to contents to copy from + * `len` - number of bytes to copy + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. + """ + raise NotImplementedError + + +def sync_message_get_response_contents( + message: SynchronousMessage, + index: int, +) -> MessageContents: + """ + Get the response contents of an `SynchronousMessage` as a `MessageContents`. + + [Rust + `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents) + + # Safety + + The data pointed to by the pointer this function returns will be deleted + when the message is deleted. Trying to use if after the message is deleted + will result in undefined behaviour. + + # Error Handling + + If the message is NULL or the index is not valid, returns NULL. + """ + raise NotImplementedError + + +def sync_message_generate_response_contents( + message: SynchronousMessage, + index: int, +) -> MessageContents: + """ + Get the response contents of an `SynchronousMessage` as a `MessageContents`. + + This function differs from + `sync_message_get_response_contents` in that it will process + the message contents for any generators or matchers that are present in + the message in order to generate the actual message contents as would be + received by the consumer. + + [Rust + `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_generate_response_contents) + + Raises: + RuntimeError: + If the response contents could not be generated. + """ + ptr = lib.pactffi_sync_message_generate_response_contents(message._ptr, index) + if ptr == ffi.NULL: + msg = "Failed to generate response contents." + raise RuntimeError(msg) + return MessageContents(ptr, owned=False) + + +def sync_message_get_description(message: SynchronousMessage) -> str: + r""" + Get a copy of the description. + + [Rust + `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_description) + + Raises: + RuntimeError: + If the description could not be retrieved + """ + ptr = lib.pactffi_sync_message_get_description(message._ptr) + if ptr == ffi.NULL: + msg = "Failed to get description." + raise RuntimeError(msg) + return OwnedString(ptr) + + +def sync_message_set_description(message: SynchronousMessage, description: str) -> int: + """ + Write the `description` field on the `SynchronousMessage`. + + [Rust + `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_description) + + # Safety + + `description` must contain valid UTF-8. Invalid UTF-8 will be replaced with + U+FFFD REPLACEMENT CHARACTER. + + This function will only reallocate if the new string does not fit in the + existing buffer. + + # Error Handling + + Errors will be reported with a non-zero return value. + """ + raise NotImplementedError + + +def sync_message_get_provider_state( + message: SynchronousMessage, + index: int, +) -> ProviderState: + r""" + Get a copy of the provider state at the given index from this message. + + [Rust + `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_provider_state) + + # Safety + + The returned structure must be deleted with `provider_state_delete`. + + Since it is a copy, the returned structure may safely outlive the + `SynchronousMessage`. + + # Error Handling + + On failure, this function will return a variant other than Success. + + This function may fail if the index requested is out of bounds, or if any of + the Rust strings contain embedded null ('\0') bytes. + """ + raise NotImplementedError + + +def sync_message_get_provider_state_iter( + message: SynchronousMessage, +) -> ProviderStateIterator: + """ + Get an iterator over provider states. + + [Rust + `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) + + # Safety + + The underlying data must not change during iteration. + + Raises: + RuntimeError: + If the iterator could not be created. + """ + ptr = lib.pactffi_sync_message_get_provider_state_iter(message._ptr) + if ptr == ffi.NULL: + msg = "Failed to get provider state iterator." + raise RuntimeError(msg) + return ProviderStateIterator(ptr) + + +def string_delete(string: OwnedString) -> None: + """ + Delete a string previously returned by this FFI. + + [Rust + `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_string_delete) + """ + lib.pactffi_string_delete(string._ptr) + + +def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: + """ + [DEPRECATED] External interface to create a HTTP mock server. + + A pointer to the pact JSON as a NULL-terminated C string is passed in, as + well as the port for the mock server to run on. A value of 0 for the port + will result in a port being allocated by the operating system. The port of + the mock server is returned. + + [Rust + `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server) + + * `pact_str` - Pact JSON + * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:80) + * `tls` - boolean flag to indicate of the mock server should use TLS (using + a self-signed certificate) + + This function is deprecated and replaced with + `pactffi_create_mock_server_for_transport`. + + # Errors + + Errors are returned as negative values. + + | Error | Description | + |-------|-------------| + | -1 | A null pointer was received | + | -2 | The pact JSON could not be parsed | + | -3 | The mock server could not be started | + | -4 | The method panicked | + | -5 | The address is not valid | + | -6 | Could not create the TLS configuration with the self-signed certificate | + """ + warnings.warn( + "This function is deprecated, use create_mock_server_for_transport instead", + DeprecationWarning, + stacklevel=2, + ) + raise NotImplementedError + + +def get_tls_ca_certificate() -> OwnedString: + """ + Fetch the CA Certificate used to generate the self-signed certificate. + + [Rust + `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_get_tls_ca_certificate) + + **NOTE:** The string for the result is allocated on the heap, and will have + to be freed by the caller using pactffi_string_delete. + + # Errors + + An empty string indicates an error reading the pem file. + """ + return OwnedString(lib.pactffi_get_tls_ca_certificate()) + + +def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) -> int: + """ + [DEPRECATED] External interface to create a HTTP mock server. + + A Pact handle is passed in, as well as the port for the mock server to run + on. A value of 0 for the port will result in a port being allocated by the + operating system. The port of the mock server is returned. + + [Rust + `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server_for_pact) + + * `pact` - Handle to a Pact model created with created with + `pactffi_new_pact`. + * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:0). + Must be a valid UTF-8 NULL-terminated string. + * `tls` - boolean flag to indicate of the mock server should use TLS (using + a self-signed certificate) + + This function is deprecated and replaced with + `pactffi_create_mock_server_for_transport`. + + # Errors + + Errors are returned as negative values. + + | Error | Description | + |-------|-------------| + | -1 | An invalid handle was received. Handles should be created with `pactffi_new_pact` | + | -3 | The mock server could not be started | + | -4 | The method panicked | + | -5 | The address is not valid | + | -6 | Could not create the TLS configuration with the self-signed certificate | + """ # noqa: E501 + warnings.warn( + "This function is deprecated, use create_mock_server_for_transport instead", + DeprecationWarning, + stacklevel=2, + ) + raise NotImplementedError + + +def create_mock_server_for_transport( + pact: PactHandle, + addr: str, + port: int, + transport: str, + transport_config: str | None, +) -> PactServerHandle: + """ + Create a mock server for the provided Pact handle and transport. + + [Rust + `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server_for_transport) + + Args: + pact: + Handle to the Pact model. + + addr: + The address to bind to. + + port: + The port number to bind to. A value of zero will result in the + operating system allocating an available port. + + transport: + The transport to use (i.e. http, https, grpc). The underlying Pact + library will interpret this, typically in a case-sensitive way. + + transport_config: + Configuration to be passed to the transport. This must be a valid + JSON string, or `None` if not required. + + Returns: + A handle to the mock server. + + Raises: + RuntimeError: + If the mock server could not be created. The error message will + contain details of the error. + """ + ret: int = lib.pactffi_create_mock_server_for_transport( + pact._ref, + addr.encode("utf-8"), + port, + transport.encode("utf-8"), + (transport_config.encode("utf-8") if transport_config else ffi.NULL), + ) + if ret > 0: + return PactServerHandle(ret) + + if ret == -1: + msg = f"An invalid Pact handle was received: {pact}." + elif ret == -2: # noqa: PLR2004 + msg = "Invalid transport_config JSON." + elif ret == -3: # noqa: PLR2004 + msg = f"Pact mock server could not be started for {pact}." + elif ret == -4: # noqa: PLR2004 + msg = f"Panick during Pact mock server creation for {pact}." + elif ret == -5: # noqa: PLR2004 + msg = f"Address is invalid: {addr}." + else: + msg = f"An unknown error occurred during Pact mock server creation for {pact}." + raise RuntimeError(msg) + + +def mock_server_matched(mock_server_handle: PactServerHandle) -> bool: + """ + External interface to check if a mock server has matched all its requests. + + If all requests have been matched, `true` is returned. `false` is returned + if any request has not been successfully matched, or the method panics. + + [Rust + `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_matched) + """ + return lib.pactffi_mock_server_matched(mock_server_handle._ref) + + +def mock_server_mismatches( + mock_server_handle: PactServerHandle, +) -> list[dict[str, Any]]: + """ + External interface to get all the mismatches from a mock server. + + [Rust + `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_mismatches) + + # Errors + + Raises: + RuntimeError: + If there is no mock server with the provided port number, or the + function panics. + """ + ptr = lib.pactffi_mock_server_mismatches(mock_server_handle._ref) + if ptr == ffi.NULL: + msg = f"No mock server found with port {mock_server_handle}." + raise RuntimeError(msg) + string = ffi.string(ptr) + if isinstance(string, bytes): + string = string.decode("utf-8") + return json.loads(string) + + +def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: + """ + External interface to cleanup a mock server. + + This function will try terminate the mock server with the given port number + and cleanup any memory allocated for it. + + [Rust + `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_cleanup_mock_server) + + Args: + mock_server_handle: + Handle to the mock server to cleanup. + + Raises: + RuntimeError: + If the mock server could not be cleaned up. + """ + success: bool = lib.pactffi_cleanup_mock_server(mock_server_handle._ref) + if not success: + msg = f"Could not cleanup mock server with port {mock_server_handle._ref}" + raise RuntimeError(msg) + + +def write_pact_file( + mock_server_handle: PactServerHandle, + directory: str | Path, + *, + overwrite: bool, +) -> None: + """ + External interface to trigger a mock server to write out its pact file. + + This function should be called if all the consumer tests have passed. The + directory to write the file to is passed as the second parameter. + + [Rust + `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_write_pact_file) + + Args: + mock_server_handle: + Handle to the mock server to write the pact file for. + + directory: + Directory to write the pact file to. + + overwrite: + Whether to overwrite any existing pact files. If this is false, the + pact file will be merged with any existing pact file. + + Raises: + RuntimeError: + If there was an error writing the pact file. + """ + ret: int = lib.pactffi_write_pact_file( + mock_server_handle._ref, + str(directory).encode("utf-8"), + overwrite, + ) + if ret == 0: + return + if ret == 1: + msg = ( + f"The function panicked while writing the Pact for {mock_server_handle} in" + f" {directory}." + ) + elif ret == 2: # noqa: PLR2004 + msg = ( + f"The Pact file for {mock_server_handle} could not be written in" + f" {directory}." + ) + elif ret == 3: # noqa: PLR2004 + msg = f"The Pact for the {mock_server_handle} was not found." + else: + msg = ( + "An unknown error occurred while writing the Pact for" + f" {mock_server_handle} in {directory}." + ) + raise RuntimeError(msg) + + +def mock_server_logs(mock_server_handle: PactServerHandle) -> str: + """ + Fetch the logs for the mock server. + + This needs the memory buffer log sink to be setup before the mock server is + started. + + [Rust + `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_logs) + + Raises: + RuntimeError: + If the logs for the mock server can not be retrieved. + """ + ptr = lib.pactffi_mock_server_logs(mock_server_handle._ref) + if ptr == ffi.NULL: + msg = f"Unable to obtain logs from {mock_server_handle!r}" + raise RuntimeError(msg) + string = ffi.string(ptr) + if isinstance(string, bytes): + string = string.decode("utf-8") + return string + + +def generate_datetime_string(format: str) -> StringResult: + """ + Generates a datetime value from the provided format string. + + This uses the current system date and time NOTE: The memory for the returned + string needs to be freed with the `pactffi_string_delete` function + + [Rust + `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generate_datetime_string) + + # Safety + + If the format string pointer is NULL or has invalid UTF-8 characters, an + error result will be returned. If the format string pointer is not a valid + pointer or is not a NULL-terminated string, this will lead to undefined + behaviour. + """ + raise NotImplementedError + + +def check_regex(regex: str, example: str) -> bool: + """ + Checks that the example string matches the given regex. + + [Rust + `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_check_regex) + + # Safety + + Both the regex and example pointers must be valid pointers to + NULL-terminated strings. Invalid pointers will result in undefined + behaviour. + """ + raise NotImplementedError + + +def generate_regex_value(regex: str) -> StringResult: + """ + Generates an example string based on the provided regex. + + NOTE: The memory for the returned string needs to be freed with the + `pactffi_string_delete` function. + + [Rust + `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generate_regex_value) + + # Safety + + The regex pointer must be a valid pointer to a NULL-terminated string. + Invalid pointers will result in undefined behaviour. + """ + raise NotImplementedError + + +def free_string(s: str) -> None: + """ + [DEPRECATED] Frees the memory allocated to a string by another function. + + [Rust + `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_free_string) + + This function is deprecated. Use `pactffi_string_delete` instead. + + # Safety + + The string pointer can be NULL (which is a no-op), but if it is not a valid + pointer the call will result in undefined behaviour. + """ + warnings.warn( + "This function is deprecated, use string_delete instead", + DeprecationWarning, + stacklevel=2, + ) + raise NotImplementedError + + +def new_pact(consumer_name: str, provider_name: str) -> PactHandle: + """ + Creates a new Pact model and returns a handle to it. + + [Rust + `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_pact) + + Args: + consumer_name: + The name of the consumer for the pact. + + provider_name: + The name of the provider for the pact. + + Returns: + Handle to the new Pact model. + """ + return PactHandle( + lib.pactffi_new_pact( + consumer_name.encode("utf-8"), + provider_name.encode("utf-8"), + ), + ) + + +def pact_handle_to_pointer(pact: PactHandle) -> Pact: + """ + Unwraps a Pact handle to the underlying Pact. + + The Pact model which has been cloned from the Pact handle's inner Pact + model. + + The returned Pact model must be freed with the `pactffi_pact_model_delete` + function when no longer needed. + """ + raise NotImplementedError + + +def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: + """ + Creates a new HTTP Interaction and returns a handle to it. + + Calling this function with the same description as an existing interaction + will result in that interaction being replaced with the new one. + + [Rust + `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_interaction) + + Args: + pact: + Handle to the Pact model. + + description: + The interaction description. It needs to be unique for each Pact. + + Returns: + Handle to the new Interaction. + """ + return InteractionHandle( + lib.pactffi_new_interaction( + pact._ref, + description.encode("utf-8"), + ), + ) + + +def new_message_interaction(pact: PactHandle, description: str) -> InteractionHandle: + """ + Creates a new message interaction and returns a handle to it. + + Calling this function with the same description as an existing interaction + will result in that interaction being replaced with the new one. + + [Rust + `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_message_interaction) + + Args: + pact: + Handle to the Pact model. + + description: + The interaction description. It needs to be unique for each Pact. + + Returns: + Handle to the new Interaction + """ + return InteractionHandle( + lib.pactffi_new_message_interaction( + pact._ref, + description.encode("utf-8"), + ), + ) + + +def new_sync_message_interaction( + pact: PactHandle, + description: str, +) -> InteractionHandle: + """ + Creates a new synchronous message interaction and returns a handle to it. + + Calling this function with the same description as an existing interaction + will result in that interaction being replaced with the new one. + + [Rust + `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_sync_message_interaction) + + Args: + pact: + Handle to the Pact model. + + description: + The interaction description. It needs to be unique for each Pact. + + Returns: + Handle to the new Interaction + """ + return InteractionHandle( + lib.pactffi_new_sync_message_interaction( + pact._ref, + description.encode("utf-8"), + ), + ) + + +def upon_receiving(interaction: InteractionHandle, description: str) -> None: + """ + Sets the description for the Interaction. + + [Rust + `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_upon_receiving) + + This function + + Args: + interaction: + Handle to the Interaction. + + description: + The interaction description. It needs to be unique for each Pact. + + Raises: + NotImplementedError: + This function has intentionally been left unimplemented. + + RuntimeError: + If the interaction description could not be set. + """ + # This function has intentionally been left unimplemented. The rationale is + # to avoid code of the form: + # + # ```python + # .with_request("GET", "/") + # .upon_receiving("some new description") + # ``` + raise NotImplementedError + + success: bool = lib.pactffi_upon_receiving( + interaction._ref, + description.encode("utf-8"), + ) + if not success: + msg = "The interaction description could not be set." + raise RuntimeError(msg) + + +def given(interaction: InteractionHandle, description: str) -> None: + """ + Adds a provider state to the Interaction. + + [Rust + `pactffi_given`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given) + + Args: + interaction: + Handle to the Interaction. + + description: + The provider state description. It needs to be unique. + + Raises: + RuntimeError: + If the provider state could not be specified. + """ + success: bool = lib.pactffi_given(interaction._ref, description.encode("utf-8")) + if not success: + msg = "The provider state could not be specified." + raise RuntimeError(msg) + + +def interaction_test_name(interaction: InteractionHandle, test_name: str) -> None: + """ + Sets the test name annotation for the interaction. + + This allows capturing the name of the test as metadata. This can only be + used with V4 interactions. + + [Rust + `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_interaction_test_name) + + Args: + interaction: + Handle to the Interaction. + + test_name: + The test name to set. + + Raises: + RuntimeError: + If the test name can not be set. + + """ + ret: int = lib.pactffi_interaction_test_name( + interaction._ref, + test_name.encode("utf-8"), + ) + if ret == 0: + return + if ret == 1: + msg = f"Function panicked: {get_error_message()}" + elif ret == 2: # noqa: PLR2004 + msg = f"Invalid handle: {interaction}." + elif ret == 3: # noqa: PLR2004 + msg = f"Mock server for {interaction} has already started." + elif ret == 4: # noqa: PLR2004 + msg = f"Interaction {interaction} is not a V4 interaction." + else: + msg = f"Unknown error setting test name for {interaction}." + raise RuntimeError(msg) + + +def given_with_param( + interaction: InteractionHandle, + description: str, + name: str, + value: str, +) -> None: + """ + Adds a parameter key and value to a provider state to the Interaction. + + If the provider state does not exist, a new one will be created, otherwise + the parameter will be merged into the existing one. The parameter value will + be parsed as JSON. + + [Rust + `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given_with_param) + + Args: + interaction: + Handle to the Interaction. + + description: + The provider state description. + + name: + Parameter name. + + value: + Parameter value as JSON. + + Raises: + RuntimeError: + If the interaction state could not be updated. + """ + success: bool = lib.pactffi_given_with_param( + interaction._ref, + description.encode("utf-8"), + name.encode("utf-8"), + value.encode("utf-8"), + ) + if not success: + msg = "The interaction state could not be updated." + raise RuntimeError(msg) + + +def given_with_params( + interaction: InteractionHandle, + description: str, + params: str, +) -> None: + """ + Adds a provider state to the Interaction. + + If the params is not an JSON object, it will add it as a single parameter + with a `value` key. + + [Rust + `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given_with_params) + + Args: + interaction: + Handle to the Interaction. + + description: + The provider state description. + + params: + Parameter values as a JSON fragment. + + Raises: + RuntimeError: + If the interaction state could not be updated. + """ + ret: int = lib.pactffi_given_with_params( + interaction._ref, + description.encode("utf-8"), + params.encode("utf-8"), + ) + if ret == 0: + return + if ret == 1: + msg = "The interaction state could not be updated." + elif ret == 2: # noqa: PLR2004 + msg = f"Internal error: {get_error_message()}" + elif ret == 3: # noqa: PLR2004 + msg = "Invalid C string." + else: + msg = "Unknown error." + raise RuntimeError(msg) + + +def with_request(interaction: InteractionHandle, method: str, path: str) -> None: + r""" + Configures the request for the Interaction. + + [Rust + `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_request) + + Args: + interaction: + Handle to the Interaction. + + method: + The request HTTP method. + + path: + The request path. + + This may be a simple string in which case it will be used as-is, or + it may be a [JSON matching + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) + which allows regex patterns. For examples: + + ```json + { + "value": "/path/to/100", + "pact:matcher:type": "regex", + "regex": "/path/to/\\d+" + } + ``` + + Raises: + RuntimeError: + If the request could not be specified. + """ + success: bool = lib.pactffi_with_request( + interaction._ref, + method.encode("utf-8"), + path.encode("utf-8"), + ) + if not success: + msg = f"The request '{method} {path}' could not be specified for {interaction}." + raise RuntimeError(msg) + + +def with_query_parameter_v2( + interaction: InteractionHandle, + name: str, + index: int, + value: str, +) -> None: + r""" + Configures a query parameter for the Interaction. + + [Rust + `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_query_parameter_v2) + + To setup a query parameter with multiple values, you can either call this + function multiple times with a different index value: + + ```python + with_query_parameter_v2(handle, "version", 0, "2") + with_query_parameter_v2(handle, "version", 0, "3") + ``` + + Or you can call it once with a JSON value that contains multiple values: + + ```python + with_query_parameter_v2( + handle, + "version", + 0, + json.dumps({"value": ["2", "3"]}), + ) + ``` + + The JSON value can also contain a matcher, which will be used to match the + query parameter value. For example, a semver matcher might look like this: + + ```python + with_query_parameter_v2( + handle, + "version", + 0, + json.dumps({ + "value": "1.2.3", + "pact:matcher:type": "regex", + "regex": r"\d+\.\d+\.\d+", + }), + ) + ``` + + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) + + If you want the matching rules to apply to all values (and not just the one + with the given index), make sure to set the value to be an array. + + ```python + with_query_parameter_v2( + handle, + "id", + 0, + json.dumps({ + "value": ["2"], + "pact:matcher:type": "regex", + "regex": r"\d+", + }), + ) + ``` + + For query parameters with no value, two distinct formats are provided: + + 1. Parameters with blank values, as specified by `?foo=&bar=`, require an + empty string: + + ```python + with_query_parameter_v2(handle, "foo", 0, "") + with_query_parameter_v2(handle, "bar", 0, "") + ``` + + 2. Parameters with no associated value, as specified by `?foo&bar`, require + a NULL pointer: + + ```python + with_query_parameter_v2(handle, "foo", 0, None) + with_query_parameter_v2(handle, "bar", 0, None) + ``` + + Args: + interaction: + Handle to the Interaction. + + name: + The query parameter name. + + index: + The index of the value (starts at 0). You can use this to create a + query parameter with multiple values. + + value: + The query parameter value. + + This may be a simple string in which case it will be used as-is, or + it may be a [JSON matching + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). + + Raises: + RuntimeError: + If there was an error setting the query parameter. + """ + success: bool = lib.pactffi_with_query_parameter_v2( + interaction._ref, + name.encode("utf-8"), + index, + value.encode("utf-8"), + ) + if not success: + msg = f"Failed to add query parameter {name} to request {interaction}." + raise RuntimeError(msg) + + +def with_specification(pact: PactHandle, version: PactSpecification) -> None: + """ + Sets the specification version for a given Pact model. + + [Rust + `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_specification) + + Args: + pact: + Handle to a Pact model. + + version: + The spec version to use. + + Raises: + RuntimeError: + If the Pact specification could not be set. + """ + success: bool = lib.pactffi_with_specification(pact._ref, version.value) + if not success: + msg = f"Failed to set Pact specification for {pact}" + raise RuntimeError(msg) + + +def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: + """ + Fetches the Pact specification version for the given Pact model. + + [Rust + `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_handle_get_pact_spec_version) + + Args: + handle: + Handle to a Pact model. + + Returns: + The spec version for the Pact model. + """ + return PactSpecification(lib.pactffi_handle_get_pact_spec_version(handle._ref)) + + +def with_pact_metadata( + pact: PactHandle, + namespace: str, + name: str, + value: str, +) -> None: + """ + Sets the additional metadata on the Pact file. + + Common uses are to add the client library details such as the name and + version Returns false if the interaction or Pact can't be modified (i.e. the + mock server for it has already started) + + [Rust + `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_pact_metadata) + + Args: + pact: + Handle to a Pact model + + namespace: + The top level metadat key to set any key values on + + name: + The key to set + + value: + The value to set + + Raises: + RuntimeError: + If the metadata could not be set. + """ + success: bool = lib.pactffi_with_pact_metadata( + pact._ref, + namespace.encode("utf-8"), + name.encode("utf-8"), + value.encode("utf-8"), + ) + if not success: + msg = f"Failed to set Pact metadata for {pact} with {namespace}.{name}={value}" + raise RuntimeError(msg) + + +def with_metadata( + interaction: InteractionHandle, + key: str, + value: str, + part: InteractionPart, +) -> None: + r""" + Adds metadata to the interaction. + + Metadata is only relevant for message interactions to provide additional + information about the message, such as the queue name, message type, tags, + timestamps, etc. + + * `key` - metadata key + * `value` - metadata value, supports JSON structures with matchers and + generators. Passing a `NULL` point will remove the metadata key instead. + * `part` - the part of the interaction to add the metadata to (only + relevant for synchronous message interactions). + + Returns `true` if the metadata was added successfully, `false` otherwise. + + To include matching rules for the value, include the matching rule JSON + format with the value as a single JSON document. I.e. + + ```python with_metadata( + handle, "TagData", json.dumps({ + "value": {"ID": "sjhdjkshsdjh", "weight": 100.5}, + "pact:matcher:type": "type", + }), + ) + ``` + + See + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) + + # Note + + For HTTP interactions, use [`with_header_v2`][pact.v3.ffi.with_header_v2] + instead. This function will not have any effect on HTTP interactions and + returns `false`. + + For synchronous message interactions, the `part` parameter is required to + specify whether the metadata should be added to the request or response + part. For responses which can have multiple messages, the metadata will be + set on all response messages. This also requires for responses to have been + defined in the interaction. + + The [`with_body`][pact.v3.ffi.with_body] will also contribute to the + metadata of the message (both sync and async) by setting the key + `contentType` with the content type of the message. + + # Safety + + The key and value parameters must be valid pointers to NULL terminated + strings, or `NULL` for the value parameter if the metadata key should be + removed. + + Raises: + RuntimeError: + If the metadata could not be set. + """ + success: bool = lib.pactffi_with_metadata( + interaction._ref, + key.encode("utf-8"), + value.encode("utf-8"), + part.value, + ) + if not success: + msg = f"Failed to set metadata for {interaction} with {key}={value}" + raise RuntimeError(msg) + + +def with_header_v2( + interaction: InteractionHandle, + part: InteractionPart, + name: str, + index: int, + value: str, +) -> None: + r""" + Configures a header for the Interaction. + + [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_header_v2) + + To setup a header with multiple values, you can either call this + function multiple times with a different index value: + + ```python + with_header_v2(handle, part, "Accept-Version", 0, "2") + with_header_v2(handle, part, "Accept-Version", 0, "3") + ``` + + Or you can call it once with a JSON value that contains multiple values: + + ```python + with_header_v2( + handle, + part, + "Accept-Version", + 0, + json.dumps({"value": ["2", "3"]}), + ) + ``` + + The JSON value can also contain a matcher, which will be used to match the + query parameter value. For example, a semver matcher might look like this: + + ```python + with_query_parameter_v2( + handle, + "Accept-Version", + 0, + json.dumps({ + "value": "1.2.3", + "pact:matcher:type": "regex", + "regex": r"\d+\.\d+\.\d+", + }), + ) + ``` + + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) + + Args: + interaction: + Handle to the Interaction. + + part: + The part of the interaction to add the header to (Request or + Response). + + name: + The header name. This is case insensitive. + + index: + The index of the value (starts at 0). You can use this to create a + header with multiple values. + + value: + The header value. + + This may be a simple string in which case it will be used as-is, or + it may be a [JSON matching + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). + + Raises: + RuntimeError: + If there was an error setting the header. + """ + success: bool = lib.pactffi_with_header_v2( + interaction._ref, + part.value, + name.encode("utf-8"), + index, + value.encode("utf-8"), + ) + if not success: + msg = f"The header {name!r} could not be specified for {interaction}." + raise RuntimeError(msg) + + +def set_header( + interaction: InteractionHandle, + part: InteractionPart, + name: str, + value: str, +) -> None: + """ + Sets a header for the Interaction. + + Note that this function will overwrite any previously set header values. + Also, this function will not process the value in any way, so matching rules + and generators can not be configured with it. + + [Rust + `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_header) + + If matching rules are required to be set, use `pactffi_with_header_v2`. + + Args: + interaction: + Handle to the Interaction. + + part: + The part of the interaction to add the header to (Request or + Response). + + name: + The header name. This is case insensitive. + + value: + The header value. This is handled as-is, with no processing. + + Raises: + RuntimeError: + If the header could not be set. + """ + success: bool = lib.pactffi_set_header( + interaction._ref, + part.value, + name.encode("utf-8"), + value.encode("utf-8"), + ) + if not success: + msg = f"The header {name!r} could not be set for {interaction}." + raise RuntimeError(msg) + + +def response_status(interaction: InteractionHandle, status: int) -> None: + """ + Configures the response for the Interaction. + + [Rust + `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_status) + + Args: + interaction: + Handle to the Interaction. + + status: + The response status. Defaults to 200. + + Raises: + RuntimeError: + If the response status could not be set. + """ + success: bool = lib.pactffi_response_status(interaction._ref, status) + if not success: + msg = f"The response status {status} could not be set for {interaction}." + raise RuntimeError(msg) + + +def response_status_v2(interaction: InteractionHandle, status: str) -> None: + """ + Configures the response for the Interaction. + + [Rust + `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_status_v2) + + To include matching rules for the status (only statusCode or integer really + makes sense to use), include the matching rule JSON format with the value as + a single JSON document. I.e. + + ```python + response_status_v2( + handle, + json.dumps({ + "pact:generator:type": "RandomInt", + "min": 100, + "max": 399, + "pact:matcher:type": "statusCode", + "status": "nonError", + }), + ) + ``` + + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) + + Args: + interaction: + Handle to the Interaction. + + status: + The response status. Defaults to 200. + + This may be a simple string in which case it will be used as-is, or + it may be a [JSON matching + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). + + Raises: + RuntimeError: + If the response status could not be set. + """ + success: bool = lib.pactffi_response_status_v2( + interaction._ref, status.encode("utf-8") + ) + if not success: + msg = f"The response status {status} could not be set for {interaction}." + raise RuntimeError(msg) + + +def with_body( + interaction: InteractionHandle, + part: InteractionPart, + content_type: str | None, + body: str | None, +) -> None: + """ + Adds the body for the interaction. + + [Rust + `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_body) + + Returns false if the interaction or Pact can't be modified (i.e. the mock + server for it has already started) + + If the `content_type` is determined as follows, whichever is first: + + - The `content_type` argument to this function + - The `Content-Type` header for HTTP interaction, or `contentType` metadata + entry for message interactions. + - From automatic detection of the body contents. + - Defaults to `text/plain` as a last resort. + + Furthermore, the `Content-Type` header or `contentType` metadata entry will + be updated with the above determined content type, _unless_ it is already + set. + + This function will overwrite the body contents if they exist, with the + exception of the response part of synchronous message interactions, where a + new response will be appended. + + Args: + interaction: + Handle to the Interaction. + + part: + The part of the interaction to add the body to (Request or + Response). This is ignored for asynchronous message interactions. + + content_type: + The content type of the body, or `None` to use the internal logic. + + body: + The body contents. For JSON payloads, matching rules can be embedded + in the body. See + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). + + Raises: + RuntimeError: + If the body could not be specified. + """ + success: bool = lib.pactffi_with_body( + interaction._ref, + part.value, + content_type.encode("utf-8") if content_type else ffi.NULL, + body.encode("utf-8") if body else None, + ) + if not success: + msg = f"Unable to set body for {interaction}." + raise RuntimeError(msg) + + +def with_binary_body( + interaction: InteractionHandle, + part: InteractionPart, + content_type: str | None, + body: bytes | None, +) -> None: + """ + Adds the body for the interaction. + + [Rust + `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_binary_body) + + For HTTP and async message interactions, this will overwrite the body. With + asynchronous messages, the part parameter will be ignored. With synchronous + messages, the request contents will be overwritten, while a new response + will be appended to the message. + + Args: + interaction: + Handle to the Interaction. + + part: + The part of the interaction to add the body to (Request or + Response). + + content_type: + The content type of the body. Will be ignored if a content type + header is already set. If `None`, the content type will be set to + `application/octet-stream`. + + body: + The body contents. If `None`, the body will be set to null. + + Raises: + RuntimeError: + If the body could not be modified. + """ + if len(gc.get_referrers(body)) == 0: + warnings.warn( + "Make sure to assign the body to a variable to avoid having the byte array" + " modified.", + UserWarning, + stacklevel=3, + ) + success: bool = lib.pactffi_with_binary_body( + interaction._ref, + part.value, + content_type.encode("utf-8") if content_type else ffi.NULL, + body if body else ffi.NULL, + len(body) if body else 0, + ) + if not success: + msg = f"Unable to set body for {interaction}." + raise RuntimeError(msg) + + +def with_binary_file( + interaction: InteractionHandle, + part: InteractionPart, + content_type: str | None, + body: bytes | None, +) -> None: + """ + Adds a binary file as the body with the expected content type and contents. + + !!! warning + + This function is deprecated. Use + [`with_binary_body`][pact.v3.ffi.with_binary_body] in order to set the + binary body, and use + [`with_matching_rules`][pact.v3.ffi.with_matching_rules] to set the + matching rules to ensure that only the content type is being matched. + + Will use a mime type matcher to match the body. Returns false if the + interaction or Pact can't be modified (i.e. the mock server for it has + already started) + + [Rust + `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_binary_file) + + For HTTP and async message interactions, this will overwrite the body. With + asynchronous messages, the part parameter will be ignored. With synchronous + messages, the request contents will be overwritten, while a new response + will be appended to the message. + + Args: + interaction: + Handle to the Interaction. + + part: + The part of the interaction to add the body to (Request or + Response). + + content_type: + The content type of the body. Will be ignored if a content type + header is already set. + + body: + The body contents. If `None`, the body will be set to null. + + Raises: + RuntimeError: + If the body could not be set. + """ + if len(gc.get_referrers(body)) == 0: + warnings.warn( + "Make sure to assign the body to a variable to avoid having the byte array" + " modified.", + UserWarning, + stacklevel=3, + ) + success: bool = lib.pactffi_with_binary_file( + interaction._ref, + part.value, + content_type.encode("utf-8") if content_type else ffi.NULL, + body if body else ffi.NULL, + len(body) if body else 0, + ) + if not success: + msg = f"Unable to set body for {interaction}." + raise RuntimeError(msg) + + +def with_matching_rules( + interaction: InteractionHandle, + part: InteractionPart, + rules: str, +) -> None: + """ + Add matching rules to the interaction. + + [Rust + `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_matching_rules) + + This function can be called multiple times, in which case the matching + rules will be merged. + + Args: + interaction: + Handle to the Interaction. + + part: + Request or response part (if applicable). + + rules: + JSON string of the matching rules to add to the interaction. + + Raises: + RuntimeError: + If the rules could not be added. + """ + success: bool = lib.pactffi_with_matching_rules( + interaction._ref, + part.value, + rules.encode("utf-8"), + ) + if not success: + msg = f"Unable to set matching rules for {interaction}." + raise RuntimeError(msg) + + +def with_generators( + interaction: InteractionHandle, + part: InteractionPart, + generators: str, +) -> None: + """ + Add generators to the interaction. + + [Rust + `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_generators) + + This function can be called multiple times, in which case the generators + will be combined (provide they don't clash). + + For synchronous messages which allow multiple responses, the generators will + be added to all the responses. + + Args: + interaction: + Handle to the Interaction. + + part: + Request or response part (if applicable). + + generators: + JSON string of the generators to add to the interaction. + + Raises: + RuntimeError: + If the generators could not be added. + """ + success: bool = lib.pactffi_with_generators( + interaction._ref, + part.value, + generators.encode("utf-8"), + ) + if not success: + msg = f"Unable to set generators for {interaction}." + raise RuntimeError(msg) + + +def with_multipart_file_v2( # noqa: PLR0913 + interaction: InteractionHandle, + part: InteractionPart, + content_type: str | None, + file: Path | None, + part_name: str, + boundary: str | None, +) -> None: + """ + Adds a binary file as the body as a MIME multipart. + + Will use a mime type matcher to match the body. Returns an error if the + interaction or Pact can't be modified (i.e. the mock server for it has + already started) or an error occurs. + + [Rust + `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_multipart_file_v2) + + This function can be called multiple times. In that case, each subsequent + call will be appended to the existing multipart body as a new part. + + Args: + interaction: + Handle to the Interaction. + + part: + The part of the interaction to add the body to (Request or + Response). + + content_type: + The content type of the body. + + file: + Path to the file to add. If `None`, the body will be set to null. + + part_name: + Name for the mime part. + + boundary: + Boundary for the multipart separation. If `None`, a random string + will be used. + """ + result = StringResult( + lib.pactffi_with_multipart_file_v2( + interaction._ref, + part.value, + content_type.encode("utf-8") if content_type else ffi.NULL, + str(file).encode("utf-8") if file else ffi.NULL, + part_name.encode("utf-8"), + boundary.encode("utf-8") if boundary else ffi.NULL, + ), + ) + result.raise_exception() + + +def with_multipart_file( + interaction: InteractionHandle, + part: InteractionPart, + content_type: str, + file: str, + part_name: str, +) -> StringResult: + """ + Adds a binary file as the body as a MIME multipart. + + Will use a mime type matcher to match the body. Returns an error if the + interaction or Pact can't be modified (i.e. the mock server for it has + already started) or an error occurs. + + [Rust + `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_multipart_file) + + * `interaction` - Interaction handle to set the body for. + * `part` - Request or response part. + * `content_type` - Expected content type of the file. + * `file` - path to the example file + * `part_name` - name for the mime part + + This function can be called multiple times. In that case, each subsequent + call will be appended to the existing multipart body as a new part. + + # Safety + + The content type, file path and part name must be valid pointers to UTF-8 + encoded NULL-terminated strings. Passing invalid pointers or pointers to + strings that are not NULL terminated will lead to undefined behaviour. + + # Error Handling + + If the file path is a NULL pointer, it will set the body contents as as an + empty mime-part. If the file path does not point to a valid file, or is not + able to be read, it will return an error result. If the content type is a + null pointer, or can't be parsed, it will return an error result. Returns an + error if the interaction or Pact can't be modified (i.e. the mock server for + it has already started), the interaction is not an HTTP interaction or some + other error occurs. + """ + # This function is intentionally left unimplemented. The + # `with_multipart_file_v2` function should be used instead. + raise NotImplementedError + + +def set_key(interaction: InteractionHandle, key: str | None) -> None: + """ + Sets the key attribute for the interaction. + + [Rust + `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_key) + + Args: + interaction: + Interaction handle to modify. + + key: + Key value. This must be a valid UTF-8 null-terminated string, or + `None` to clear the key. + + Raises: + RuntimeError: + If the key could not be set. + """ + success: bool = lib.pactffi_set_key( + interaction._ref, + key.encode("utf-8") if key else ffi.NULL, + ) + if not success: + msg = f"Failed to set key for {interaction}." + raise RuntimeError(msg) + + +def set_pending(interaction: InteractionHandle, *, pending: bool) -> None: + """ + Mark the interaction as pending. + + [Rust + `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_pending) + + Args: + interaction: + Interaction handle to modify. + + pending: + Boolean value to toggle the pending state of the interaction. + + Raises: + RuntimeError: + If the pending status could not be updated. + """ + success: bool = lib.pactffi_set_pending(interaction._ref, pending) + if not success: + msg = f"Failed to update pending status for {interaction}." + raise RuntimeError(msg) + + +def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> None: + """ + Add a comment to the interaction. + + [Rust + `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_comment) + + Args: + interaction: + Interaction handle to set the comments for. + + key: + Key value + + value: + Comment value. This may be any valid JSON value, or a `None` to + clear the comment. Note that a value that deserialize to a JSON null + will result in a comment being added, with the value being the JSON + null. + + Raises: + RuntimeError: + If the comments could not be updated. + """ + success: bool = lib.pactffi_set_comment( + interaction._ref, + key.encode("utf-8"), + value.encode("utf-8") if value else ffi.NULL, + ) + if not success: + msg = f"Failed to set comment for {interaction}." + raise RuntimeError(msg) + + +def add_text_comment(interaction: InteractionHandle, comment: str) -> None: + """ + Add a text comment to the interaction. + + [Rust + `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_add_text_comment) + + Args: + interaction: + Interaction handle to set the comments for. + + comment: + Comment value. This is a regular string value. + + Raises: + RuntimeError: + If the comment could not be added. + """ + success: bool = lib.pactffi_add_text_comment( + interaction._ref, + comment.encode("utf-8"), + ) + if not success: + msg = f"Failed to add text comment for {interaction}." + raise RuntimeError(msg) + + +def pact_handle_get_async_message_iter(pact: PactHandle) -> PactAsyncMessageIterator: + r""" + Get an iterator over all the asynchronous messages of the Pact. + + The returned iterator needs to be freed with + `pactffi_pact_sync_message_iter_delete`. + + [Rust + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + + # Safety + + The iterator contains a copy of the Pact, so it is always safe to use. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. + """ + return PactAsyncMessageIterator( + lib.pactffi_pact_handle_get_async_message_iter(pact._ref), + ) + + +def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterator: + r""" + Get an iterator over all the synchronous messages of the Pact. + + The returned iterator needs to be freed with + `pactffi_pact_sync_message_iter_delete`. + + [Rust + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + + # Safety + + The iterator contains a copy of the Pact, so it is always safe to use. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. + """ + return PactSyncMessageIterator( + lib.pactffi_pact_handle_get_sync_message_iter(pact._ref), + ) + + +def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: + r""" + Get an iterator over all the synchronous HTTP request/response interactions. + + The returned iterator needs to be freed with + `pactffi_pact_sync_http_iter_delete`. + + [Rust + `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) + + # Safety + + The iterator contains a copy of the Pact, so it is always safe to use. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. + """ + return PactSyncHttpIterator(lib.pactffi_pact_handle_get_sync_http_iter(pact._ref)) + + +def pact_handle_write_file( + pact: PactHandle, + directory: Path | str | None, + *, + overwrite: bool, +) -> None: + """ + External interface to write out the pact file. + + [Rust + `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_write_file) + + This function should be called if all the consumer tests have passed. + + Args: + pact: + Handle to a Pact model. + + directory: + The directory to write the file to. If `None`, the current working + directory is used. + + overwrite: + If `True`, the file will be overwritten with the contents of the + current pact. Otherwise, it will be merged with any existing pact + file. + + Raises: + RuntimeError: + If there was an error writing the pact file. + """ + ret: int = lib.pactffi_pact_handle_write_file( + pact._ref, + str(directory).encode("utf-8") if directory else ffi.NULL, + overwrite, + ) + if ret == 0: + return + if ret == 1: + msg = f"The function panicked while writing {pact} to {directory}." + elif ret == 2: # noqa: PLR2004 + msg = f"The pact file was not able to be written for {pact}." + elif ret == 3: # noqa: PLR2004 + msg = f"The pact for {pact} was not found." + else: + msg = f"Unknown error writing {pact} to {directory}." + raise RuntimeError(msg) + + +def free_pact_handle(pact: PactHandle) -> None: + """ + Delete a Pact handle and free the resources used by it. + + [Rust + `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_free_pact_handle) + + Raises: + RuntimeError: + If the handle could not be freed. + """ + ret: int = lib.pactffi_free_pact_handle(pact._ref) + if ret == 0: + return + if ret == 1: + msg = f"{pact} is not valid or does not refer to a valid Pact." + else: + msg = f"There was an unknown error freeing {pact}." + raise RuntimeError(msg) + + +def verify(args: str) -> int: + """ + External interface to verifier a provider. + + [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verify) + + * `args` - the same as the CLI interface, except newline delimited + + # Errors + + Errors are returned as non-zero numeric values. + + | Error | Description | + |-------|-------------| + | 1 | The verification process failed, see output for errors | + | 2 | A null pointer was received | + | 3 | The method panicked | + | 4 | Invalid arguments were provided to the verification process | + + # Safety + + Exported functions are inherently unsafe. Deal. + """ + raise NotImplementedError + + +def verifier_new_for_application() -> VerifierHandle: + """ + Get a Handle to a newly created verifier. + + By default, verification results will not be published. To enable + publishing, use + [`pactffi_verifier_set_publish_options`][pact.v3.ffi.verifier_set_publish_options] + to set the required values and enable it. + + [Rust + `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_new_for_application) + """ + result: cffi.FFI.CData = lib.pactffi_verifier_new_for_application( + b"pact-python", + __version__.encode("utf-8"), + ) + return VerifierHandle(result) + + +def verifier_shutdown(handle: VerifierHandle) -> None: + """ + Shutdown the verifier and release all resources. + + [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_shutdown) + """ + lib.pactffi_verifier_shutdown(handle._ref) + + +def verifier_set_provider_info( # noqa: PLR0913 + handle: VerifierHandle, + name: str | None, + scheme: str | None, + host: str | None, + port: int | None, + path: str | None, +) -> None: + """ + Set the provider details for the Pact verifier. + + [Rust + `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_provider_info) + + Args: + handle: + The verifier handle to update. + + name: + A user-friendly name to describe the provider. + + scheme: + Determine the scheme to use, typically one of `HTTP` or `HTTPS`. + + host: + The host of the provider. This may be either a hostname to resolve, + or an IP address. + + port: + The port of the provider. + + path: + The path of the provider. + + If any value is `None`, the default value as determined by the underlying + FFI library will be used. + """ + lib.pactffi_verifier_set_provider_info( + handle._ref, + name.encode("utf-8") if name else ffi.NULL, + scheme.encode("utf-8") if scheme else ffi.NULL, + host.encode("utf-8") if host else ffi.NULL, + port, + path.encode("utf-8") if path else ffi.NULL, + ) + + +def verifier_add_provider_transport( + handle: VerifierHandle, + protocol: str | None, + port: int, + path: str | None, + scheme: str | None, +) -> None: + """ + Adds a new transport for the given provider. + + [Rust + `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_provider_transport) + + Args: + handle: + The verifier handle to update. + + protocol: + In this context, the kind of + + port: + The port of the provider. + + path: + The path of the provider. + + scheme: + The scheme to use, typically one of `HTTP` or `HTTPS`. + + If any value is `None`, the default value as determined by the underlying + FFI library will be used. + """ + lib.pactffi_verifier_add_provider_transport( + handle._ref, + protocol.encode("utf-8") if protocol else ffi.NULL, + port, + path.encode("utf-8") if path else ffi.NULL, + scheme.encode("utf-8") if scheme else ffi.NULL, + ) + + +def verifier_set_filter_info( + handle: VerifierHandle, + filter_description: str | None, + filter_state: str | None, + *, + filter_no_state: bool, +) -> None: + """ + Set the filters for the Pact verifier. + + [Rust + `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_filter_info) + + Set filters to narrow down the interactions to verify. + + Args: + handle: + The verifier handle to update. + + filter_description: + A regular expression to filter the interactions by description. + + filter_state: + A regular expression to filter the interactions by state. + + filter_no_state: + If `True`, the option to filter by state will be turned on. + """ + lib.pactffi_verifier_set_filter_info( + handle._ref, + filter_description.encode("utf-8") if filter_description else ffi.NULL, + filter_state.encode("utf-8") if filter_state else ffi.NULL, + filter_no_state, + ) + + +def verifier_set_provider_state( + handle: VerifierHandle, + url: str, + *, + teardown: bool, + body: bool, +) -> None: + """ + Set the provider state URL for the Pact verifier. + + [Rust + `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_provider_state) + + Args: + handle: + The verifier handle to update. + + url: + The URL to use for the provider state. + + teardown: + If teardown state change requests should be made after an + interaction is validated. + + body: + If state change request data should be sent in the body or the + query. + """ + lib.pactffi_verifier_set_provider_state( + handle._ref, + url.encode("utf-8"), + teardown, + body, + ) + + +def verifier_set_verification_options( + handle: VerifierHandle, + *, + disable_ssl_verification: bool, + request_timeout: int, +) -> None: + """ + Set the options used by the verifier when calling the provider. + + [Rust + `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_verification_options) + + Args: + handle: + The verifier handle to update. + + disable_ssl_verification: + If SSL verification should be disabled. + + request_timeout: + The timeout for the request in milliseconds. + + Raises: + RuntimeError: + If the options could not be set. + """ + retval: int = lib.pactffi_verifier_set_verification_options( + handle._ref, + disable_ssl_verification, + request_timeout, + ) + if retval != 0: + msg = f"Failed to set verification options for {handle}." + raise RuntimeError(msg) + + +def verifier_set_coloured_output( + handle: VerifierHandle, + *, + enabled: bool, +) -> None: + """ + Enables or disables coloured output using ANSI escape codes. + + [Rust + `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_coloured_output) + + By default, coloured output is enabled. + + Args: + handle: + The verifier handle to update. + + enabled: + A boolean value to enable or disable coloured output. + + Raises: + RuntimeError: + If the coloured output could not be set. + """ + retval: int = lib.pactffi_verifier_set_coloured_output( + handle._ref, + enabled, + ) + if retval != 0: + msg = f"Failed to set coloured output for {handle}." + raise RuntimeError(msg) + + +def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> None: + """ + Enables or disables if no pacts are found to verify results in an error. + + [Rust + `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) + + Args: + handle: + The verifier handle to update. + + enabled: + If `True`, an error will be raised when no pacts are found to verify. + + Raises: + RuntimeError: + If the no pacts is error setting could not be set. + """ + retval: int = lib.pactffi_verifier_set_no_pacts_is_error( + handle._ref, + enabled, + ) + if retval != 0: + msg = f"Failed to set no pacts is error for {handle}." + raise RuntimeError(msg) + + +def verifier_set_publish_options( + handle: VerifierHandle, + provider_version: str, + build_url: str | None, + provider_tags: list[str] | None, + provider_branch: str | None, +) -> None: + """ + Set the options used when publishing verification results to the Broker. + + [Rust + `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_publish_options) + + Args: + handle: + The verifier handle to update. + + provider_version: + Version of the provider to publish. + + build_url: + URL to the build which ran the verification. + + provider_tags: + Collection of tags for the provider. + + provider_branch: + Name of the branch used for verification. + + Raises: + RuntimeError: + If the publish options could not be set. + """ + retval: int = lib.pactffi_verifier_set_publish_options( + handle._ref, + provider_version.encode("utf-8"), + build_url.encode("utf-8") if build_url else ffi.NULL, + [ffi.new("char[]", t.encode("utf-8")) for t in provider_tags or []], + len(provider_tags or []), + provider_branch.encode("utf-8") if provider_branch else ffi.NULL, + ) + if retval != 0: + msg = f"Failed to set publish options for {handle}." + raise RuntimeError(msg) + + +def verifier_set_consumer_filters( + handle: VerifierHandle, + consumer_filters: Collection[str], +) -> None: + """ + Set the consumer filters for the Pact verifier. + + [Rust + `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_consumer_filters) + """ + lib.pactffi_verifier_set_consumer_filters( + handle._ref, + [ffi.new("char[]", f.encode("utf-8")) for f in consumer_filters], + len(consumer_filters), + ) + + +def verifier_add_custom_header( + handle: VerifierHandle, + header_name: str, + header_value: str, +) -> None: + """ + Adds a custom header to be added to the requests made to the provider. + + [Rust + `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_custom_header) + """ + lib.pactffi_verifier_add_custom_header( + handle._ref, + header_name.encode("utf-8"), + header_value.encode("utf-8"), + ) + + +def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: + """ + Adds a Pact file as a source to verify. + + [Rust + `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_file_source) + """ + lib.pactffi_verifier_add_file_source(handle._ref, file.encode("utf-8")) + + +def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> None: + """ + Adds a Pact directory as a source to verify. + + All pacts from the directory that match the provider name will be verified. + + [Rust + `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_directory_source) + + # Safety + + All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced + with U+FFFD REPLACEMENT CHARACTER. + + """ + lib.pactffi_verifier_add_directory_source(handle._ref, directory.encode("utf-8")) + + +def verifier_url_source( + handle: VerifierHandle, + url: str, + username: str | None, + password: str | None, + token: str | None, +) -> None: + """ + Adds a URL as a source to verify. + + [Rust + `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_url_source) + + Args: + handle: + The verifier handle to update. + + url: + The URL to use as a source for the verifier. + + username: + The username to use when fetching pacts from the URL. + + password: + The password to use when fetching pacts from the URL. + + token: + The token to use when fetching pacts from the URL. This will be used + as a bearer token. It is mutually exclusive with the username and + password. + """ + lib.pactffi_verifier_url_source( + handle._ref, + url.encode("utf-8"), + username.encode("utf-8") if username else ffi.NULL, + password.encode("utf-8") if password else ffi.NULL, + token.encode("utf-8") if token else ffi.NULL, + ) + + +def verifier_broker_source( + handle: VerifierHandle, + url: str, + username: str | None, + password: str | None, + token: str | None, +) -> None: + """ + Adds a Pact broker as a source to verify. + + [Rust + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_broker_source) + + This will fetch all the pact files from the broker that match the provider + name. + + Args: + handle: + The verifier handle to update. + + url: + The URL to use as a source for the verifier. + + username: + The username to use when fetching pacts from the broker. + + password: + The password to use when fetching pacts from the broker. + + token: + The token to use when fetching pacts from the broker. This will be + used as a bearer token. + """ + lib.pactffi_verifier_broker_source( + handle._ref, + url.encode("utf-8"), + username.encode("utf-8") if username else ffi.NULL, + password.encode("utf-8") if password else ffi.NULL, + token.encode("utf-8") if token else ffi.NULL, + ) + + +def verifier_broker_source_with_selectors( # noqa: PLR0913 + handle: VerifierHandle, + url: str, + username: str | None, + password: str | None, + token: str | None, + enable_pending: int, + include_wip_pacts_since: datetime.date | None, + provider_tags: list[str], + provider_branch: str | None, + consumer_version_selectors: list[str], + consumer_version_tags: list[str], +) -> None: + """ + Adds a Pact broker as a source to verify. + + [Rust + `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) + + This will fetch all the pact files from the broker that match the provider + name and the consumer version selectors (See [Consumer Version + Selectors](https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/)). + + If a username and password is given, then basic authentication will be used + when fetching the pact file. If a token is provided, then bearer token + authentication will be used. + + Args: + handle: + The verifier handle to update. + + url: + The URL to use as a source for the verifier. + + username: + The username to use when fetching pacts from the broker. + + password: + The password to use when fetching pacts from the broker. + + token: + The token to use when fetching pacts from the broker. This will be + used as a bearer token. + + enable_pending: + If pending pacts should be included in the verification process. + + include_wip_pacts_since: + The date to use to filter out WIP pacts. + + provider_tags: + The tags to use to filter the provider pacts. + + provider_branch: + The branch to use to filter the provider pacts. + + consumer_version_selectors: + The consumer version selectors to use to filter the consumer pacts. + This must be passed in as a JSON string. + + consumer_version_tags: + The tags to use to filter the consumer pacts. + """ + ret: int = lib.pactffi_verifier_broker_source_with_selectors( + handle._ref, + url.encode("utf-8"), + username.encode("utf-8") if username else ffi.NULL, + password.encode("utf-8") if password else ffi.NULL, + token.encode("utf-8") if token else ffi.NULL, + enable_pending, + ( + include_wip_pacts_since.isoformat().encode("utf-8") + if include_wip_pacts_since + else ffi.NULL + ), + [ffi.new("char[]", t.encode("utf-8")) for t in provider_tags], + len(provider_tags), + provider_branch.encode("utf-8") if provider_branch else ffi.NULL, + [ffi.new("char[]", s.encode("utf-8")) for s in consumer_version_selectors], + len(consumer_version_selectors), + [ffi.new("char[]", t.encode("utf-8")) for t in consumer_version_tags], + len(consumer_version_tags), + ) + if ret == 0: + return + if ret == -1: + msg = "Invalid version selector JSON." + raise ValueError(msg) + msg = "Unknown error adding broker source with selectors." + raise RuntimeError(msg) + + +def verifier_execute(handle: VerifierHandle) -> None: + """ + Runs the verification. + + (https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_execute) + + Raises: + RuntimeError: + If the verifier could not be executed. + """ + success: int = lib.pactffi_verifier_execute(handle._ref) + if success != 0: + msg = f"Failed to execute verifier for {handle}." + raise RuntimeError(msg) + + +def verifier_cli_args() -> str: + """ + External interface to retrieve the CLI options and arguments. + + This available when calling the CLI interface, returning them as a JSON + string. + + [Rust + `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_cli_args) + + The purpose is to then be able to use in other languages which wrap the FFI + library, to implement the same CLI functionality automatically without + manual maintenance of arguments, help descriptions etc. + + # Example structure + + ```json + { + "options": [ + { + "long": "scheme", + "help": "Provider URI scheme (defaults to http)", + "possible_values": [ + "http", + "https" + ], + "default_value": "http" + "multiple": false, + }, + { + "long": "file", + "short": "f", + "help": "Pact file to verify (can be repeated)", + "multiple": true + }, + { + "long": "user", + "help": "Username to use when fetching pacts from URLS", + "multiple": false, + "env": "PACT_BROKER_USERNAME" + } + ], + "flags": [ + { + "long": "disable-ssl-verification", + "help": "Disables validation of SSL certificates", + "multiple": false + } + ] + } + ``` + + # Safety + + Exported functions are inherently unsafe. + """ + raise NotImplementedError + + +def verifier_logs(handle: VerifierHandle) -> OwnedString: + """ + Extracts the logs for the verification run. + + [Rust + `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_logs) + + This needs the memory buffer log sink to be setup before the verification is + executed. The returned string will need to be freed with the `free_string` + function call to avoid leaking memory. + + Raises: + RuntimeError: + If the logs could not be extracted. + """ + ptr = lib.pactffi_verifier_logs(handle._ref) + if ptr == ffi.NULL: + msg = f"Failed to get logs for {handle}." + raise RuntimeError(msg) + return OwnedString(ptr) + + +def verifier_logs_for_provider(provider_name: str) -> OwnedString: + """ + Extracts the logs for the verification run for the provider name. + + [Rust + `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_logs_for_provider) + + This needs the memory buffer log sink to be setup before the verification is + executed. The returned string will need to be freed with the `free_string` + function call to avoid leaking memory. + + Raises: + RuntimeError: + If the logs could not be extracted. + """ + ptr = lib.pactffi_verifier_logs_for_provider(provider_name.encode("utf-8")) + if ptr == ffi.NULL: + msg = f"Failed to get logs for {provider_name}." + raise RuntimeError(msg) + return OwnedString(ptr) + + +def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: + """ + Extracts the standard output for the verification run. + + [Rust + `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_output) + + Args: + handle: + The verifier handle to update. + + strip_ansi: + This parameter controls ANSI escape codes. Setting it to a non-zero + value will cause the ANSI control codes to be stripped from the + output. + + Raises: + RuntimeError: + If the output could not be extracted. + """ + ptr = lib.pactffi_verifier_output(handle._ref, strip_ansi) + if ptr == ffi.NULL: + msg = f"Failed to get output for {handle}." + raise RuntimeError(msg) + return OwnedString(ptr) + + +def verifier_json(handle: VerifierHandle) -> OwnedString: + """ + Extracts the verification result as a JSON document. + + [Rust + `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_json) + + Raises: + RuntimeError: + If the JSON could not be extracted. + """ + ptr = lib.pactffi_verifier_json(handle._ref) + if ptr == ffi.NULL: + msg = f"Failed to get JSON for {handle}." + raise RuntimeError(msg) + return OwnedString(ptr) + + +def using_plugin( + pact: PactHandle, + plugin_name: str, + plugin_version: str | None, +) -> None: + """ + Add a plugin to be used by the test. + + The plugin needs to be installed correctly for this function to work. + + Note that plugins run as separate processes, so will need to be cleaned up + afterwards by calling [`cleanup_plugins`][pact.v3.ffi.cleanup_plugins] + otherwise you will have plugin processes left running. + + [Rust + `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_using_plugin) + + Args: + pact: + Handle to a Pact model. + + plugin_name: + Name of the plugin to use. + + plugin_version: + Version of the plugin to use. If `None`, the latest version will be + used. + + Raises: + RuntimeError: + If the plugin could not be loaded. + """ + ret: int = lib.pactffi_using_plugin( + pact._ref, + plugin_name.encode("utf-8"), + plugin_version.encode("utf-8") if plugin_version else ffi.NULL, + ) + if ret == 0: + return + if ret == 1: + msg = f"A general panic was caught: {get_error_message()}" + elif ret == 2: # noqa: PLR2004 + msg = f"Failed to load the plugin {plugin_name}." + elif ret == 3: # noqa: PLR2004 + msg = f"The Pact handle {pact} is invalid." + else: + msg = f"There was an unknown error loading the plugin {plugin_name}." + raise RuntimeError(msg) + + +def cleanup_plugins(pact: PactHandle) -> None: + """ + Decrement the access count on any plugins that are loaded for the Pact. + + This will shutdown any plugins that are no longer required (access count is + zero). + + [Rust + `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_cleanup_plugins) + """ + lib.pactffi_cleanup_plugins(pact._ref) + + +def interaction_contents( + interaction: InteractionHandle, + part: InteractionPart, + content_type: str, + contents: str, +) -> None: + """ + Setup the interaction part using a plugin. + + The contents is a JSON string that will be passed on to the plugin to + configure the interaction part. Refer to the plugin documentation on the + format of the JSON contents. + + [Rust + `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_interaction_contents) + + Args: + interaction: + Handle to the interaction to configure. + + part: + The part of the interaction to configure (request or response). It + is ignored for messages. + + content_type: + Mime type of the contents. + + contents: + JSON contents that gets passed to the plugin. + + Raises: + RuntimeError: + If the interaction could not be configured + """ + ret: int = lib.pactffi_interaction_contents( + interaction._ref, + part.value, + content_type.encode("utf-8"), + contents.encode("utf-8"), + ) + if ret == 0: + return + if ret == 1: + msg = f"A general panic was caught: {get_error_message()}" + if ret == 2: # noqa: PLR2004 + msg = "The mock server has already been started." + if ret == 3: # noqa: PLR2004 + msg = f"The interaction handle {interaction} is invalid." + if ret == 4: # noqa: PLR2004 + msg = f"The content type {content_type} is not valid." + if ret == 5: # noqa: PLR2004 + msg = "The content is not valid JSON." + if ret == 6: # noqa: PLR2004 + msg = f"The plugin returned an error: {get_error_message()}" + else: + msg = f"There was an unknown error configuring the interaction: {ret}" + raise RuntimeError(msg) + + +def matches_string_value( + matching_rule: MatchingRule, + expected_value: str, + actual_value: str, + cascaded: int, +) -> OwnedString: + """ + Determines if the string value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_string_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get as a NULL terminated string + * actual_value - value to match as a NULL terminated string + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule pointer must be a valid pointer, and the value parameters + must be valid pointers to a NULL terminated strings. + """ + raise NotImplementedError + + +def matches_u64_value( + matching_rule: MatchingRule, + expected_value: int, + actual_value: int, + cascaded: int, +) -> OwnedString: + """ + Determines if the unsigned integer value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_u64_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get + * actual_value - value to match + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule pointer must be a valid pointer. + """ + raise NotImplementedError + + +def matches_i64_value( + matching_rule: MatchingRule, + expected_value: int, + actual_value: int, + cascaded: int, +) -> OwnedString: + """ + Determines if the signed integer value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_i64_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get + * actual_value - value to match + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule pointer must be a valid pointer. + """ + raise NotImplementedError + + +def matches_f64_value( + matching_rule: MatchingRule, + expected_value: float, + actual_value: float, + cascaded: int, +) -> OwnedString: + """ + Determines if the floating point value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_f64_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get + * actual_value - value to match + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule pointer must be a valid pointer. + """ + raise NotImplementedError + + +def matches_bool_value( + matching_rule: MatchingRule, + expected_value: int, + actual_value: int, + cascaded: int, +) -> OwnedString: + """ + Determines if the boolean value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_bool_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get, 0 == false and 1 == true + * actual_value - value to match, 0 == false and 1 == true + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule pointer must be a valid pointer. + """ + raise NotImplementedError + + +def matches_binary_value( # noqa: PLR0913 + matching_rule: MatchingRule, + expected_value: str, + expected_value_len: int, + actual_value: str, + actual_value_len: int, + cascaded: int, +) -> OwnedString: + """ + Determines if the binary value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_binary_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get + * expected_value_len - length of the expected value bytes + * actual_value - value to match + * actual_value_len - length of the actual value bytes + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule, expected value and actual value pointers must be a valid + pointers. expected_value_len and actual_value_len must contain the number of + bytes that the value pointers point to. Passing invalid lengths can lead to + undefined behaviour. + """ + raise NotImplementedError + + +def matches_json_value( + matching_rule: MatchingRule, + expected_value: str, + actual_value: str, + cascaded: int, +) -> OwnedString: + """ + Determines if the JSON value matches the given matching rule. + + If the value matches OK, will return a NULL pointer. If the value does not + match, will return a error message as a NULL terminated string. The error + message pointer will need to be deleted with the `pactffi_string_delete` + function once it is no longer required. + + [Rust + `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_json_value) + + * matching_rule - pointer to a matching rule + * expected_value - value we expect to get as a NULL terminated string + * actual_value - value to match as a NULL terminated string + * cascaded - if the matching rule has been cascaded from a parent. 0 == + false, 1 == true + + # Safety + + The matching rule pointer must be a valid pointer, and the value parameters + must be valid pointers to a NULL terminated strings. + """ + raise NotImplementedError diff --git a/pact-python-ffi/src/pact_ffi/ffi.pyi b/pact-python-ffi/src/pact_ffi/ffi.pyi new file mode 100644 index 000000000..897259dca --- /dev/null +++ b/pact-python-ffi/src/pact_ffi/ffi.pyi @@ -0,0 +1,6 @@ +import ctypes + +import cffi + +lib: ctypes.CDLL +ffi: cffi.FFI diff --git a/pact-python-ffi/src/pact_ffi/py.typed b/pact-python-ffi/src/pact_ffi/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/pact-python-ffi/tests/.ruff.toml b/pact-python-ffi/tests/.ruff.toml new file mode 100644 index 000000000..54c8b9dfc --- /dev/null +++ b/pact-python-ffi/tests/.ruff.toml @@ -0,0 +1,10 @@ +#:schema https://json.schemastore.org/ruff.json +extend = "../pyproject.toml" + +[lint] +ignore = [ + "D103", # Require docstrings on public functions + "INP001", # Forbid implicit namespaces + "PLR2004", # Forbid magic numbers + "S101", # Disable assert +] diff --git a/pact-python-ffi/tests/test_init.py b/pact-python-ffi/tests/test_init.py new file mode 100644 index 000000000..30520a668 --- /dev/null +++ b/pact-python-ffi/tests/test_init.py @@ -0,0 +1,76 @@ +""" +Test the FFI interface. + +Note that these tests are not intended to be exhaustive, as the full +functionality should be tested through the core Pact Python library. + +Instead, these tests fall under two broad categories: + +- Tests that ensure the FFI interface is working correctly. +- Tests that ensure some of the thin wrappers around the FFI interface + are functioning as expected. +""" + +import re + +import pact_ffi +import pytest +from pact_ffi.ffi import lib + + +def test_version() -> None: + assert isinstance(pact_ffi.version(), str) + assert len(pact_ffi.version()) > 0 + assert pact_ffi.version().count(".") == 2 + + +def test_string_result_ok() -> None: + result = pact_ffi.StringResult(lib.pactffi_generate_datetime_string(b"yyyy")) + assert result.is_ok + assert not result.is_failed + assert re.match(r"^\d{4}$", result.text) + assert str(result) == result.text + assert repr(result) == f"" + result.raise_exception() + + +def test_string_result_failed() -> None: + result = pact_ffi.StringResult(lib.pactffi_generate_datetime_string(b"t")) + assert not result.is_ok + assert result.is_failed + assert result.text.startswith("Error parsing") + with pytest.raises(RuntimeError): + result.raise_exception() + + +def test_datetime_valid() -> None: + pact_ffi.validate_datetime("2023-01-01", "yyyy-MM-dd") + + +def test_datetime_invalid() -> None: + with pytest.raises(ValueError, match=r"Invalid datetime value.*"): + pact_ffi.validate_datetime("01/01/2023", "yyyy-MM-dd") + + +def test_get_error_message() -> None: + # The first bit makes sure that an error is generated. + invalid_utf8 = b"\xc3\x28" + ret: int = lib.pactffi_validate_datetime(invalid_utf8, invalid_utf8) + assert ret == 2 + assert pact_ffi.get_error_message() == "error parsing value as UTF-8" + + +def test_owned_string() -> None: + string = pact_ffi.get_tls_ca_certificate() + assert isinstance(string, str) + assert len(string) > 0 + assert str(string) == string + assert repr(string).startswith("") + assert string.startswith("-----BEGIN CERTIFICATE-----") + assert string.endswith( + ( + "-----END CERTIFICATE-----\n", + "-----END CERTIFICATE-----\r\n", + ), + ) diff --git a/pyproject.toml b/pyproject.toml index b2492237b..72dfced3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -195,6 +195,12 @@ requires = [ # See: https://github.com/pypa/hatch/issues/1639 pre-install-commands = ["uv pip install -e .[devel]"] + # Update paths to ensure the shared library can be found + # TODO: See if this can be overridden on a per-platform basis + # https://github.com/pypa/hatch/discussions/2024 + # [tool.hatch.envs.default.overrides] + # platform.windows.env-vars = { PATH = "{root}/src/pact_ffi;{env:PATH}" } + [tool.hatch.envs.default.scripts] all = ["example", "format", "lint", "test", "typecheck"] docs = "mkdocs serve {args}" @@ -214,6 +220,12 @@ requires = [ features = ["devel-test"] installer = "uv" + # Update paths to ensure the shared library can be found + # TODO: See if this can be overridden on a per-platform basis + # https://github.com/pypa/hatch/discussions/2024 + # [tool.hatch.envs.default.overrides] + # platform.windows.env-vars = { PATH = "{root}/src/pact_ffi;{env:PATH}" } + [[tool.hatch.envs.test.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.9"] From 49fa4cc7acac46197fd5b95ed018076b305c5a61 Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:29:43 +0000 Subject: [PATCH 0888/1376] docs: update changelog for pact-python-ffi/0.4.22.0 --- pact-python-ffi/CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 pact-python-ffi/CHANGELOG.md diff --git a/pact-python-ffi/CHANGELOG.md b/pact-python-ffi/CHANGELOG.md new file mode 100644 index 000000000..43d2a406b --- /dev/null +++ b/pact-python-ffi/CHANGELOG.md @@ -0,0 +1,25 @@ +# Pact Python FFI Changelog + +All notable changes to this project will be documented in this file. + +Note that this _only_ includes changes to the Python FFI interface. For changes to the Pact FFI itself, see the [Pact FFI changelog](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/CHANGELOG.md). + + + + + +## [pact-python-ffi/0.4.22.0] _2025-07-29_ + +### 🚀 Features + +- _(ffi)_ Add standalone ffi package + +### ⚙️ Miscellaneous Tasks + +- Create cli and ffi packages + +### Contributors + +- @JP-Ellis + + From 0c3bb2dcc29edb5fdd9884e69865d1dc2f9a18c2 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 30 Jul 2025 10:32:49 +1000 Subject: [PATCH 0889/1376] feat(v3)!: remove pact.v3.ffi module The `pact.v3.ffi` module has been replaced with the `pact_ffi` standalone package. All other higher-level Pact Python API remain unchanged. BREAKING CHANGE: `pact.v3.ffi` is removed, and to be replaced by `pact_ffi`. That is, `pact.v3.ffi.$fn` should be replaced by `pact_ffi.$fn`. Signed-off-by: JP-Ellis --- examples/conftest.py | 4 +- hatch_build.py | 343 - pact-python-ffi/tests/test_init.py | 3 +- pyproject.toml | 85 +- src/pact/v3/_ffi.pyi | 6 - src/pact/v3/ffi.py | 7819 ----------------- .../interaction/_async_message_interaction.py | 14 +- src/pact/v3/interaction/_base.py | 48 +- src/pact/v3/interaction/_http_interaction.py | 26 +- .../interaction/_sync_message_interaction.py | 14 +- src/pact/v3/pact.py | 60 +- src/pact/v3/verifier.py | 50 +- tests/v3/conftest.py | 4 +- tests/v3/test_ffi.py | 71 - tests/v3/test_pact.py | 2 +- 15 files changed, 123 insertions(+), 8426 deletions(-) delete mode 100644 hatch_build.py delete mode 100644 src/pact/v3/_ffi.pyi delete mode 100644 src/pact/v3/ffi.py delete mode 100644 tests/v3/test_ffi.py diff --git a/examples/conftest.py b/examples/conftest.py index 80b35807b..513fee6b5 100644 --- a/examples/conftest.py +++ b/examples/conftest.py @@ -22,7 +22,7 @@ from testcontainers.compose import DockerCompose # type: ignore[import-untyped] from yarl import URL -from pact.v3 import ffi +import pact_ffi if TYPE_CHECKING: from collections.abc import Generator, Sequence @@ -102,4 +102,4 @@ def _setup_pact_logging() -> None: """ Set up logging for the pact package. """ - ffi.log_to_stderr("INFO") + pact_ffi.log_to_stderr("INFO") diff --git a/hatch_build.py b/hatch_build.py deleted file mode 100644 index e472ec9d3..000000000 --- a/hatch_build.py +++ /dev/null @@ -1,343 +0,0 @@ -""" -Hatchling build hook for binary downloads. - -Pact Python is built on top of the Ruby Pact binaries and the Rust Pact library. -This build script downloads the binaries and library for the current platform -and installs them in the `pact` directory under `/bin` and `/lib`. - -The version of the binaries and library can be controlled with the -`PACT_BIN_VERSION` and `PACT_LIB_VERSION` environment variables. If these are -not set, a pinned version will be used instead. -""" - -from __future__ import annotations - -import gzip -import os -import shutil -import tempfile -import warnings -from pathlib import Path -from typing import Any - -import cffi -import requests -from hatchling.builders.hooks.plugin.interface import BuildHookInterface -from packaging.tags import sys_tags - -PACT_ROOT_DIR = Path(__file__).parent.resolve() / "src" / "pact" - -# Latest version available at: -# https://github.com/pact-foundation/pact-reference/releases -PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.22") -PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{machine}.{ext}" - - -class UnsupportedPlatformError(RuntimeError): - """Raised when the current platform is not supported.""" - - def __init__(self, platform: str) -> None: - """ - Initialize the exception. - - Args: - platform: The unsupported platform. - """ - self.platform = platform - super().__init__(f"Unsupported platform {platform}") - - -class PactBuildHook(BuildHookInterface[Any]): - """Custom hook to download Pact binaries.""" - - PLUGIN_NAME = "custom" - - def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 - """ - Initialize the build hook. - - For this hook, we additionally define the lib extension based on the - current platform. - """ - super().__init__(*args, **kwargs) - self.tmpdir = Path(tempfile.TemporaryDirectory().name) - self.tmpdir.mkdir(parents=True, exist_ok=True) - - 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(PACT_ROOT_DIR / subdir, ignore_errors=True) - - for ffi in (PACT_ROOT_DIR / "v3").glob("_ffi.*"): - if ffi.name == "_ffi.pyi": - continue - ffi.unlink() - - 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 - - binaries_included = False - try: - self.pact_lib_install(PACT_LIB_VERSION) - binaries_included = True - except UnsupportedPlatformError as err: - msg = f"Pact library is not available for {err.platform}" - warnings.warn(msg, RuntimeWarning, stacklevel=2) - - if not binaries_included: - msg = "Wheel does not include any binaries. Aborting." - raise UnsupportedPlatformError(msg) - - def pact_lib_install(self, version: str) -> None: - """ - Install the Pact library binary. - - The library is installed in `pact/lib`, and the relevant version for - the current operating system is determined automatically. - - Args: - version: The Pact version to install. - """ - url = self._pact_lib_url(version) - artifact = self._download(url) - self._pact_lib_extract(artifact) - includes = self._pact_lib_header(url) - self._pact_lib_cffi(includes) - - def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 - """ - Generate the download URL for the Pact library. - - Generate the download URL for the Pact library based on the current - platform and specified version. This function mainly contains a lot of - matching logic to determine the correct URL to use, due to the - inconsistencies in naming conventions between ecosystems. - - Args: - version: The upstream Pact version. - - Returns: - The URL to download the Pact library from. - - Raises: - ValueError: - If the current platform is not supported. - """ - platform = next(sys_tags()).platform - - if platform.startswith("macosx"): - os = "macos" - if platform.endswith("arm64"): - machine = "aarch64" - elif platform.endswith("x86_64"): - machine = "x86_64" - else: - raise UnsupportedPlatformError(platform) - return PACT_LIB_URL.format( - prefix="lib", - version=version, - os=os, - machine=machine, - ext="a.gz", - ) - - if platform.startswith("win"): - os = "windows" - - if platform.endswith("amd64"): - machine = "x86_64" - elif platform.endswith(("arm64", "aarch64")): - machine = "aarch64" - else: - raise UnsupportedPlatformError(platform) - return PACT_LIB_URL.format( - prefix="", - version=version, - os=os, - machine=machine, - ext="lib.gz", - ) - - if "musllinux" in platform: - os = "linux" - if platform.endswith("x86_64"): - machine = "x86_64-musl" - elif platform.endswith("aarch64"): - machine = "aarch64-musl" - else: - raise UnsupportedPlatformError(platform) - return PACT_LIB_URL.format( - prefix="lib", - version=version, - os=os, - machine=machine, - ext="a.gz", - ) - - if "manylinux" in platform: - os = "linux" - if platform.endswith("x86_64"): - machine = "x86_64" - elif platform.endswith("aarch64"): - machine = "aarch64" - else: - raise UnsupportedPlatformError(platform) - - return PACT_LIB_URL.format( - prefix="lib", - version=version, - os=os, - machine=machine, - ext="a.gz", - ) - - raise UnsupportedPlatformError(platform) - - def _pact_lib_extract(self, artifact: Path) -> None: - """ - Extract the Pact library. - - Extract the Pact library from the downloaded artifact and place it in - `pact/lib`. - - Args: - artifact: The URL to download the Pact binaries from. - """ - # Pypy does not guarantee that the directory exists. - self.tmpdir.mkdir(parents=True, exist_ok=True) - - if not str(artifact).endswith(".gz"): - msg = f"Unknown artifact type {artifact}" - raise ValueError(msg) - - with ( - gzip.open(artifact, "rb") as f_in, - (self.tmpdir / (artifact.name.split("-")[0] + artifact.suffixes[0])).open( - "wb" - ) as f_out, - ): - shutil.copyfileobj(f_in, f_out) - - def _pact_lib_header(self, url: str) -> list[str]: - """ - Download the Pact library header. - - Download the Pact library header from GitHub and place it in - `pact/include`. This uses the same URL as for the artifact, replacing - the final segment with `pact.h`. - - This also processes the header to strip out elements which are not - supported by CFFI (i.e., any line starting with `#`). The list of - `#include` statements is returned for use in the CFFI bindings. - - Args: - url: The URL pointing to the Pact library artifact. - """ - # Pypy does not guarantee that the directory exists. - self.tmpdir.mkdir(parents=True, exist_ok=True) - - url = url.rsplit("/", 1)[0] + "/pact.h" - artifact = self._download(url) - includes: list[str] = [] - with ( - artifact.open("r", encoding="utf-8") as f_in, - (self.tmpdir / "pact.h").open("w", encoding="utf-8") as f_out, - ): - for line in f_in: - sline = line.strip() - if sline.startswith("#include"): - includes.append(sline) - continue - if sline.startswith("#"): - continue - - f_out.write(line) - return includes - - def _pact_lib_cffi(self, includes: list[str]) -> None: - """ - Build the CFFI bindings for the Pact library. - - This will build the CFFI bindings for the Pact library and place them in - `pact/lib`. - - A list of additional `#include` statements can be passed to this - function, which will be included in the generated bindings. - - Args: - includes: - A list of additional `#include` statements to include in the - generated bindings. - """ - if os.name == "nt": - extra_libs = [ - "advapi32", - "bcrypt", - "crypt32", - "iphlpapi", - "ncrypt", - "netapi32", - "ntdll", - "ole32", - "oleaut32", - "pdh", - "powrprof", - "psapi", - "secur32", - "shell32", - "user32", - "userenv", - "ws2_32", - ] - else: - extra_libs = [] - - ffibuilder = cffi.FFI() - with (self.tmpdir / "pact.h").open( - "r", - encoding="utf-8", - ) as f: - ffibuilder.cdef(f.read()) - ffibuilder.set_source( - "_ffi", - "\n".join([*includes, '#include "pact.h"']), - libraries=["pact_ffi", *extra_libs], - library_dirs=[str(self.tmpdir)], - ) - output = Path(ffibuilder.compile(verbose=True, tmpdir=str(self.tmpdir))) - shutil.copy(output, PACT_ROOT_DIR / "v3") - - def _download(self, url: str) -> Path: - """ - Download the target URL. - - This will download the target URL to the `pact/data` directory. If the - download artifact is already present, its path will be returned. - - Args: - url: The URL to download - - Return: - The path to the downloaded artifact. - """ - filename = url.split("/")[-1] - artifact = PACT_ROOT_DIR / "data" / filename - artifact.parent.mkdir(parents=True, exist_ok=True) - - if not artifact.exists(): - response = requests.get(url, timeout=30) - try: - response.raise_for_status() - except requests.HTTPError as e: - msg = f"Failed to download from {url}." - raise RuntimeError(msg) from e - with artifact.open("wb") as f: - f.write(response.content) - - return artifact diff --git a/pact-python-ffi/tests/test_init.py b/pact-python-ffi/tests/test_init.py index 30520a668..941658bb4 100644 --- a/pact-python-ffi/tests/test_init.py +++ b/pact-python-ffi/tests/test_init.py @@ -13,8 +13,9 @@ import re -import pact_ffi import pytest + +import pact_ffi from pact_ffi.ffi import lib diff --git a/pyproject.toml b/pyproject.toml index 72dfced3e..ee3f92b73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ requires-python = ">=3.9" dependencies = [ # Pact dependencies "pact-python-cli~=2.0", + "pact-python-ffi~=0.4.0", # All other dependencies "cffi~=1.0", "click~=8.0", @@ -69,6 +70,7 @@ dependencies = [ # developper consistency. All other dependencies are as above. devel = [ "pact-python-cli[devel]", + "pact-python-ffi[devel]", "pact-python[devel-docs]", "pact-python[devel-test]", "pact-python[devel-types]", @@ -109,14 +111,7 @@ dependencies = [ [build-system] build-backend = "hatchling.build" -requires = [ - "cffi", - "hatch-vcs", - "hatchling", - "packaging", - "requests", - "setuptools ; python_version >= '3.12'", -] +requires = ["hatch-vcs", "hatchling"] ################################################################################ ## Hatch Configuration @@ -136,7 +131,7 @@ requires = [ "--always", "--long", "--match", - "pact-python-cli/*", + "pact-python/*", ] root = "." version_scheme = "no-guess-dev" @@ -146,34 +141,9 @@ requires = [ [tool.hatch.build.hooks.vcs] version-file = "src/pact/__version__.py" - [tool.hatch.build.targets.sdist] - include = [ - # Source - "/src/pact/**/*.py", - "/src/pact/**/*.pyi", - "/src/pact/**/py.typed", - - # Metadata - "*.md", - "LICENSE", - ] - [tool.hatch.build.targets.wheel] - artifacts = [ - "/src/pact/bin/*", # Ruby executables - "/src/pact/lib/*", # Ruby library - "/src/pact/v3/_ffi.*", # Rust library - ] - include = [ - # Source - "/src/pact/**/*.py", - "/src/pact/**/*.pyi", - "/src/pact/**/py.typed", - ] packages = ["/src/pact"] - [tool.hatch.build.targets.wheel.hooks.custom] - ######################################## ## Hatch Environment Configuration ######################################## @@ -182,15 +152,9 @@ requires = [ # Install dev dependencies in the default environment to simplify the developer # workflow. [tool.hatch.envs.default] - extra-dependencies = [ - "cffi", - "hatchling", - "packaging", - "requests", - "setuptools ; python_version >= '3.12'", - ] - features = ["devel"] - installer = "uv" + extra-dependencies = ["hatchling", "hatch-vcs"] + features = ["devel"] + installer = "uv" # This is require to get around an incompatibility between hatch and uv # See: https://github.com/pypa/hatch/issues/1639 pre-install-commands = ["uv pip install -e .[devel]"] @@ -235,9 +199,10 @@ requires = [ [tool.uv] [tool.uv.sources] pact-python-cli = { workspace = true } + pact-python-ffi = { workspace = true } [tool.uv.workspace] - members = ["pact-python-cli"] + members = ["pact-python-cli", "pact-python-ffi"] ################################################################################ ## PyTest Configuration @@ -364,7 +329,7 @@ extend-exclude = [ convention = "google" [tool.ruff.lint.isort] - known-first-party = ["pact", "pact_cli"] + known-first-party = ["pact", "pact_cli", "pact_ffi"] [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" @@ -383,36 +348,6 @@ exclude = '^(src/pact|tests|examples|examples/tests)/(?!v3).+\.py$' ## CI Build Wheel ################################################################################ [tool.cibuildwheel] -before-build = """ -rm -rvf src/pact/v3/bin -rm -rvf src/pact/v3/data -rm -rvf src/pact/v3/lib -mv -v src/pact/v3/_ffi.pyi _ffi.pyi -rm -rvf src/pact/v3/_ffi.* -mv -v _ffi.pyi src/pact/v3/_ffi.pyi -""" -skip = "pp*" -test-command = """ -python -c \ -"from pact import EachLike; \ -assert \ - EachLike(1).generate() \ - == {'json_class': 'Pact::ArrayLike', 'contents': 1, 'min': 1}; \ -import pact.v3.ffi; \ -assert isinstance(pact.v3.ffi.version(), str);\"""" - - [tool.cibuildwheel.macos] - # The repair tool unfortunately did not like the bundled Ruby distributable. - # TODO: Check whether delocate-wheel can be configured. - repair-wheel-command = "" - - [tool.cibuildwheel.windows] - before-build = [ - 'FOR /R src\pact\v3 %G IN (_ffi.*) DO IF NOT %~nxG == _ffi.pyi DEL /F /Q "%G"', - 'IF EXIST src\pact\v3\bin\ RMDIR /S /Q src\pact\v3\bin', - 'IF EXIST src\pact\v3\data\ RMDIR /S /Q src\pact\v3\data', - 'IF EXIST src\pact\v3\lib\ RMDIR /S /Q src\pact\v3\lib', - ] ################################################################################ ## Typos diff --git a/src/pact/v3/_ffi.pyi b/src/pact/v3/_ffi.pyi deleted file mode 100644 index 897259dca..000000000 --- a/src/pact/v3/_ffi.pyi +++ /dev/null @@ -1,6 +0,0 @@ -import ctypes - -import cffi - -lib: ctypes.CDLL -ffi: cffi.FFI diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py deleted file mode 100644 index 6266524c3..000000000 --- a/src/pact/v3/ffi.py +++ /dev/null @@ -1,7819 +0,0 @@ -""" -Python bindings for the Pact FFI. - -This module provides a Python interface to the Pact FFI. It is a thin wrapper -around the C API, and is intended to be used by the Pact Python client library -to provide a Pythonic interface to Pact. - -!!! warning - - This module is not intended to be used directly by Pact users. Pact users - should use the Pact Python client library instead. No guarantees are made - about the stability of this module's API. - -## Developer Notes - -This modules should provide the following only: - -- Basic Enum classes -- Simple wrappers around functions, including the casting of input and output - values between the high level Python types and the low level C types. -- Simple wrappers around some of the low-level types. Specifically designed to - automatically handle the freeing of memory when the Python object is - destroyed. - -These low-level functions may then be combined into higher level classes and -modules. Ideally, all code outside of this module should be written in pure -Python and not worry about allocating or freeing memory. - -During initial implementation, a lot of these functions will simply raise a -[`NotImplementedError`][NotImplementedError]. - -For those unfamiliar with CFFI, please make sure to read the [CFFI -documentation](https://cffi.readthedocs.io/en/latest/using.html). - -### Handles - -The Rust library exposes a number of handles to internal data structures. This -is done to avoid exposing the internal implementation details of the library to -users of the library, and avoid unnecessarily casting to and from possibly -complicated structs. - -In the Rust library, the handles are thin wrappers around integers, and -unfortunately the CFFI interface sees this and automatically unwraps them, -exposing the underlying integer. As a result, we must re-wrap the integer -returned by the CFFI interface. This unfortunately means that we may be subject -to changes in private implementation details upstream. - -### Freeing Memory - -Python has a garbage collector, and as a result, we don't need to worry about -manually freeing memory. Having said that, Python's garbace collector is only -aware of Python objects, and not of any memory allocated by the Rust library. - -To ensure that the memory allocated by the Rust library is freed, we must make -sure to define the -[`__del__`](https://docs.python.org/3/reference/datamodel.html#object.__del__) -method to call the appropriate free function whenever the Python object is -destroyed. - -Note that there are some rather subtle details as to when this is called, when -it may never be called, and what global variables are accessible. This is -explained in the documentation for `__del__` above, and in Python's [garbage -collection](https://docs.python.org/3/library/gc.html) module. - -### Error Handling - -The FFI function should handle all errors raised by the function call, and raise -an appropriate Python exception. The exception should be raised using the -appropriate Python exception class, and should be documented in the function's -docstring. -""" - -# The following lints are disabled during initial development and should be -# removed later. -# ruff: noqa: ARG001 (unused-function-argument) -# ruff: noqa: A002 (builtin-argument-shadowing) -# ruff: noqa: D101 (undocumented-public-class) - -# The following lints are disabled for this file. -# ruff: noqa: SLF001 -# private-member-access, as we need access to other handles' internal -# references, without exposing them to the user. -# pyright: reportPrivateUsage=false -# Ignore private member access, as we frequently need to use the -# object's underlying pointer stored in `_ptr`. - -from __future__ import annotations - -import gc -import inspect -import json -import logging -import typing -import warnings -from enum import Enum -from typing import TYPE_CHECKING, Any, Literal - -from pact import __version__ -from pact.v3._ffi import ffi, lib # type: ignore[import] - -if TYPE_CHECKING: - import datetime - from collections.abc import Collection - from collections.abc import Generator as GeneratorType - from pathlib import Path - - import cffi - from typing_extensions import Self - -logger = logging.getLogger(__name__) - -################################################################################ -# Type aliases -################################################################################ -# The following type aliases provide a nicer interface for end-users of the -# library, especially when it comes to [`Enum`][Enum] classes which offers -# support for string literals as alternative values. - -GeneratorCategoryOptions = Literal[ - "METHOD", "method", - "PATH", "path", - "HEADER", "header", - "QUERY", "query", - "BODY", "body", - "STATUS", "status", - "METADATA", "metadata", -] # fmt: skip -""" -Generator Category Options. - -Type alias for the string literals which represent the Generator Category -Options. -""" - -MatchingRuleCategoryOptions = Literal[ - "METHOD", "method", - "PATH", "path", - "HEADER", "header", - "QUERY", "query", - "BODY", "body", - "STATUS", "status", - "CONTENTS", "contents", - "METADATA", "metadata", -] # fmt: skip - -################################################################################ -# Classes -################################################################################ -# The follow types are classes defined in the Rust code. Ultimately, a Python -# alternative should be implemented, but for now, the follow lines only serve -# to inform the type checker of the existence of these types. - - -class AsynchronousMessage: - def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: - """ - Initialise a new Asynchronous Message. - - Args: - ptr: - CFFI data structure. - - owned: - Whether the message is owned by something else or not. This - determines whether the message should be freed when the Python - object is destroyed. - - Raises: - TypeError: - If the `ptr` is not a `struct AsynchronousMessage`. - """ - if ffi.typeof(ptr).cname != "struct AsynchronousMessage *": - msg = ( - f"ptr must be a struct AsynchronousMessage, got {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - self._owned = owned - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "AsynchronousMessage" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"AsynchronousMessage({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the AsynchronousMessage. - """ - if not self._owned: - async_message_delete(self) - - @property - def description(self) -> str: - """ - Description of this message interaction. - - This needs to be unique in the pact file. - """ - return async_message_get_description(self) - - def provider_states(self) -> GeneratorType[ProviderState, None, None]: - """ - Optional provider state for the interaction. - """ - yield from async_message_get_provider_state_iter(self) - return # Ensures that the parent object outlives the generator - - @property - def contents(self) -> MessageContents | None: - """ - The contents of the message. - - This may be `None` if the message has no contents. - """ - return async_message_generate_contents(self) - - -class Consumer: ... - - -class Generator: - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a generator value. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct Generator`. - """ - if ffi.typeof(ptr).cname != "struct Generator *": - msg = f"ptr must be a struct Generator, got {ffi.typeof(ptr).cname}" - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "Generator" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"Generator({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the Generator. - """ - - @property - def json(self) -> dict[str, Any]: - """ - Dictionary representation of the generator. - """ - return json.loads(generator_to_json(self)) - - def generate_string(self, context: dict[str, Any] | None = None) -> str: - """ - Generate a string from the generator. - - Args: - context: - JSON payload containing any generator context. For example: - - - The context for a `MockServerURL` generator should contain - details about the running mock server. - - The context for a `ProviderStateGenerator` should contain - the values returned from the provider state callback - function. - """ - return generator_generate_string(self, json.dumps(context or {})) - - def generate_integer(self, context: dict[str, Any] | None = None) -> int: - """ - Generate an integer from the generator. - - Args: - context: - JSON payload containing any generator context. For example: - - - The context for a `ProviderStateGenerator` should contain - the values returned from the provider state callback - function. - """ - return generator_generate_integer(self, json.dumps(context or {})) - - -class GeneratorCategoryIterator: - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new generator category iterator. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct GeneratorCategoryIterator`. - """ - if ffi.typeof(ptr).cname != "struct GeneratorCategoryIterator *": - msg = ( - "ptr must be a struct GeneratorCategoryIterator, got" - f" {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "GeneratorCategoryIterator" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"GeneratorCategoryIterator({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the GeneratorCategoryIterator. - """ - generators_iter_delete(self) - - def __iter__(self) -> Self: - """ - Return the iterator itself. - """ - return self - - def __next__(self) -> GeneratorKeyValuePair: - """ - Get the next generator category from the iterator. - """ - return generators_iter_next(self) - - -class GeneratorKeyValuePair: - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new key-value generator pair. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct GeneratorKeyValuePair`. - """ - if ffi.typeof(ptr).cname != "struct GeneratorKeyValuePair *": - msg = ( - "ptr must be a struct GeneratorKeyValuePair, got" - f" {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "GeneratorKeyValuePair" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"GeneratorKeyValuePair({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the GeneratorKeyValuePair. - """ - generators_iter_pair_delete(self) - - @property - def path(self) -> str: - """ - Generator path. - """ - s = ffi.string(self._ptr.path) # type: ignore[attr-defined] - if isinstance(s, bytes): - s = s.decode("utf-8") - return s - - @property - def generator(self) -> Generator: - """ - Generator value. - """ - return Generator(self._ptr.generator) # type: ignore[attr-defined] - - -class HttpRequest: ... - - -class HttpResponse: ... - - -class InteractionHandle: - """ - Handle to a HTTP Interaction. - - [Rust - `InteractionHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/struct.InteractionHandle.html) - """ - - def __init__(self, ref: int) -> None: - """ - Initialise a new Interaction Handle. - - Args: - ref: - Reference to the Interaction Handle. - """ - self._ref: int = ref - - def __str__(self) -> str: - """ - String representation of the Interaction Handle. - """ - return f"InteractionHandle({self._ref})" - - def __repr__(self) -> str: - """ - String representation of the Interaction Handle. - """ - return f"InteractionHandle({self._ref!r})" - - -class MatchingRule: - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new key-value generator pair. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct MatchingRule`. - """ - if ffi.typeof(ptr).cname != "struct MatchingRule *": - msg = f"ptr must be a struct MatchingRule, got {ffi.typeof(ptr).cname}" - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "MatchingRule" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"MatchingRule({self._ptr!r})" - - @property - def json(self) -> dict[str, Any]: - """ - Dictionary representation of the matching rule. - """ - return json.loads(matching_rule_to_json(self)) - - -class MatchingRuleCategoryIterator: - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new key-value generator pair. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct MatchingRuleCategoryIterator`. - """ - if ffi.typeof(ptr).cname != "struct MatchingRuleCategoryIterator *": - msg = ( - "ptr must be a struct MatchingRuleCategoryIterator, got" - f" {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "MatchingRuleCategoryIterator" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"MatchingRuleCategoryIterator({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the MatchingRuleCategoryIterator. - """ - matching_rules_iter_delete(self) - - def __iter__(self) -> Self: - """ - Return the iterator itself. - """ - return self - - def __next__(self) -> MatchingRuleKeyValuePair: - """ - Get the next generator category from the iterator. - """ - return matching_rules_iter_next(self) - - -class MatchingRuleDefinitionResult: ... - - -class MatchingRuleIterator: ... - - -class MatchingRuleKeyValuePair: - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new key-value generator pair. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct MatchingRuleKeyValuePair`. - """ - if ffi.typeof(ptr).cname != "struct MatchingRuleKeyValuePair *": - msg = ( - "ptr must be a struct MatchingRuleKeyValuePair, got" - f" {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "MatchingRuleKeyValuePair" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"MatchingRuleKeyValuePair({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the MatchingRuleKeyValuePair. - """ - matching_rules_iter_pair_delete(self) - - @property - def path(self) -> str: - """ - Matching Rule path. - """ - s = ffi.string(self._ptr.path) # type: ignore[attr-defined] - if isinstance(s, bytes): - s = s.decode("utf-8") - return s - - @property - def matching_rule(self) -> MatchingRule: - """ - Matching Rule value. - """ - return MatchingRule(self._ptr.matching_rule) # type: ignore[attr-defined] - - -class MatchingRuleResult: ... - - -class MessageContents: - def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = True) -> None: - """ - Initialise a Message Contents. - - Args: - ptr: - CFFI data structure. - - owned: - Whether the message is owned by something else or not. This - determines whether the message should be freed when the Python - object is destroyed. - - Raises: - TypeError: - If the `ptr` is not a `struct MessageContents`. - """ - if ffi.typeof(ptr).cname != "struct MessageContents *": - msg = f"ptr must be a struct MessageContents, got {ffi.typeof(ptr).cname}" - raise TypeError(msg) - self._ptr = ptr - self._owned = owned - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "MessageContents" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"MessageContents({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the MessageContents. - """ - if not self._owned: - message_contents_delete(self) - - @property - def contents(self) -> str | bytes | None: - """ - Get the contents of the message. - """ - return message_contents_get_contents_str( - self - ) or message_contents_get_contents_bin(self) - - @property - def metadata(self) -> GeneratorType[MessageMetadataPair, None, None]: - """ - Get the metadata for the message contents. - """ - yield from message_contents_get_metadata_iter(self) - return # Ensures that the parent object outlives the generator - - def matching_rules( - self, - category: MatchingRuleCategoryOptions | MatchingRuleCategory, - ) -> GeneratorType[MatchingRuleKeyValuePair, None, None]: - """ - Get the matching rules for the message contents. - """ - if isinstance(category, str): - category = MatchingRuleCategory(category.upper()) - yield from message_contents_get_matching_rule_iter(self, category) - return # Ensures that the parent object outlives the generator - - def generators( - self, - category: GeneratorCategoryOptions | GeneratorCategory, - ) -> GeneratorType[GeneratorKeyValuePair, None, None]: - """ - Get the generators for the message contents. - """ - if isinstance(category, str): - category = GeneratorCategory(category.upper()) - yield from message_contents_get_generators_iter(self, category) - return # Ensures that the parent object outlives the generator - - -class MessageMetadataIterator: - """ - Iterator over an interaction's metadata. - """ - - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new Message Metadata Iterator. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct MessageMetadataIterator`. - """ - if ffi.typeof(ptr).cname != "struct MessageMetadataIterator *": - msg = ( - "ptr must be a struct MessageMetadataIterator, got" - f" {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "MessageMetadataIterator" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"MessageMetadataIterator({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the Pact Interaction Iterator. - """ - message_metadata_iter_delete(self) - - def __iter__(self) -> Self: - """ - Return the iterator itself. - """ - return self - - def __next__(self) -> MessageMetadataPair: - """ - Get the next interaction from the iterator. - """ - return message_metadata_iter_next(self) - - -class MessageMetadataPair: - """ - A metadata key-value pair. - """ - - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new Message Metadata Pair. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct MessageMetadataPair`. - """ - if ffi.typeof(ptr).cname != "struct MessageMetadataPair *": - msg = ( - f"ptr must be a struct MessageMetadataPair, got {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "MessageMetadataPair" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"MessageMetadataPair({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the Pact Interaction Iterator. - """ - message_metadata_pair_delete(self) - - @property - def key(self) -> str: - """ - Metadata key. - """ - s = ffi.string(self._ptr.key) # type: ignore[attr-defined] - if isinstance(s, bytes): - s = s.decode("utf-8") - return s - - @property - def value(self) -> str: - """ - Metadata value. - """ - s = ffi.string(self._ptr.value) # type: ignore[attr-defined] - if isinstance(s, bytes): - s = s.decode("utf-8") - return s - - -class Mismatch: ... - - -class Mismatches: ... - - -class MismatchesIterator: ... - - -class Pact: ... - - -class PactAsyncMessageIterator: - """ - Iterator over a Pact's asynchronous messages. - """ - - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new Pact Asynchronous Message Iterator. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct PactAsyncMessageIterator`. - """ - if ffi.typeof(ptr).cname != "struct PactAsyncMessageIterator *": - msg = ( - "ptr must be a struct PactAsyncMessageIterator, got" - f" {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "PactAsyncMessageIterator" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"PactAsyncMessageIterator({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the Pact Synchronous Message Iterator. - """ - pact_async_message_iter_delete(self) - - def __iter__(self) -> Self: - """ - Return the iterator itself. - """ - return self - - def __next__(self) -> AsynchronousMessage: - """ - Get the next message from the iterator. - """ - return pact_async_message_iter_next(self) - - -class PactHandle: - """ - Handle to a Pact. - - [Rust - `PactHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/struct.PactHandle.html) - """ - - def __init__(self, ref: int) -> None: - """ - Initialise a new Pact Handle. - - Args: - ref: - Rust library reference to the Pact Handle. - """ - self._ref: int = ref - - def __del__(self) -> None: - """ - Destructor for the Pact Handle. - """ - cleanup_plugins(self) - free_pact_handle(self) - - def __str__(self) -> str: - """ - String representation of the Pact Handle. - """ - return f"PactHandle({self._ref})" - - def __repr__(self) -> str: - """ - String representation of the Pact Handle. - """ - return f"PactHandle({self._ref!r})" - - -class PactServerHandle: - """ - Handle to a Pact Server. - - This does not have an exact correspondence in the Rust library. It is used - to manage the lifecycle of the mock server. - - # Implementation Notes - - The Rust library uses the port number as a unique identifier, in much the - same was as it uses a wrapped integer for the Pact handle. - """ - - def __init__(self, ref: int) -> None: - """ - Initialise a new Pact Server Handle. - - Args: - ref: - Rust library reference to the Pact Server. - """ - self._ref: int = ref - - def __del__(self) -> None: - """ - Destructor for the Pact Server Handle. - """ - cleanup_mock_server(self) - - def __str__(self) -> str: - """ - String representation of the Pact Server Handle. - """ - return f"PactServerHandle({self._ref})" - - def __repr__(self) -> str: - """ - String representation of the Pact Server Handle. - """ - return f"PactServerHandle({self._ref!r})" - - @property - def port(self) -> int: - """ - Port on which the Pact Server is running. - """ - return self._ref - - -class PactInteraction: ... - - -class PactInteractionIterator: - """ - Iterator over a Pact's interactions. - - Interactions encompasses all types of interactions, including HTTP - interactions and messages. - """ - - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new Pact Interaction Iterator. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct PactInteractionIterator`. - """ - if ffi.typeof(ptr).cname != "struct PactInteractionIterator *": - msg = ( - "ptr must be a struct PactInteractionIterator, got" - f" {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "PactInteractionIterator" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"PactInteractionIterator({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the Pact Interaction Iterator. - """ - pact_interaction_iter_delete(self) - - def __next__(self) -> PactInteraction: - """ - Get the next interaction from the iterator. - """ - return pact_interaction_iter_next(self) - - -class PactSyncHttpIterator: - """ - Iterator over a Pact's synchronous HTTP interactions. - """ - - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new Pact Synchronous HTTP Iterator. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct PactSyncHttpIterator`. - """ - if ffi.typeof(ptr).cname != "struct PactSyncHttpIterator *": - msg = ( - "ptr must be a struct PactSyncHttpIterator, got" - f" {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "PactSyncHttpIterator" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"PactSyncHttpIterator({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the Pact Synchronous HTTP Iterator. - """ - pact_sync_http_iter_delete(self) - - def __iter__(self) -> Self: - """ - Return the iterator itself. - """ - return self - - def __next__(self) -> SynchronousHttp: - """ - Get the next message from the iterator. - """ - return pact_sync_http_iter_next(self) - - -class PactSyncMessageIterator: - """ - Iterator over a Pact's synchronous messages. - """ - - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new Pact Synchronous Message Iterator. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct PactSyncMessageIterator`. - """ - if ffi.typeof(ptr).cname != "struct PactSyncMessageIterator *": - msg = ( - "ptr must be a struct PactSyncMessageIterator, got" - f" {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "PactSyncMessageIterator" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"PactSyncMessageIterator({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the Pact Synchronous Message Iterator. - """ - pact_sync_message_iter_delete(self) - - def __iter__(self) -> Self: - """ - Return the iterator itself. - """ - return self - - def __next__(self) -> SynchronousMessage: - """ - Get the next message from the iterator. - """ - return pact_sync_message_iter_next(self) - - -class Provider: ... - - -class ProviderState: - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new ProviderState. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct ProviderState`. - """ - if ffi.typeof(ptr).cname != "struct ProviderState *": - msg = f"ptr must be a struct ProviderState, got {ffi.typeof(ptr).cname}" - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "ProviderState({self.name!r})" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"ProviderState({self._ptr!r})" - - @property - def name(self) -> str: - """ - Provider State name. - """ - return provider_state_get_name(self) or "" - - def parameters(self) -> GeneratorType[tuple[str, str], None, None]: - """ - Provider State parameters. - - This is a generator that yields key-value pairs. - """ - for p in provider_state_get_param_iter(self): - yield p.key, p.value - return # Ensures that the parent object outlives the generator - - -class ProviderStateIterator: - """ - Iterator over an interactions ProviderStates. - """ - - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new Provider State Iterator. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct ProviderStateIterator`. - """ - if ffi.typeof(ptr).cname != "struct ProviderStateIterator *": - msg = ( - "ptr must be a struct ProviderStateIterator, got" - f" {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "ProviderStateIterator" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"ProviderStateIterator({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the Provider State Iterator. - """ - provider_state_iter_delete(self) - - def __iter__(self) -> ProviderStateIterator: - """ - Return the iterator itself. - """ - return self - - def __next__(self) -> ProviderState: - """ - Get the next message from the iterator. - """ - return provider_state_iter_next(self) - - -class ProviderStateParamIterator: - """ - Iterator over a Provider States Parameters. - """ - - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new Provider State Param Iterator. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct ProviderStateParamIterator`. - """ - if ffi.typeof(ptr).cname != "struct ProviderStateParamIterator *": - msg = ( - "ptr must be a struct ProviderStateParamIterator, got" - f" {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "ProviderStateParamIterator" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"ProviderStateParamIterator({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the Provider State Param Iterator. - """ - provider_state_param_iter_delete(self) - - def __iter__(self) -> ProviderStateParamIterator: - """ - Return the iterator itself. - """ - return self - - def __next__(self) -> ProviderStateParamPair: - """ - Get the next message from the iterator. - """ - return provider_state_param_iter_next(self) - - -class ProviderStateParamPair: - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new ProviderStateParamPair. - - Args: - ptr: - CFFI data structure. - - Raises: - TypeError: - If the `ptr` is not a `struct ProviderStateParamPair`. - """ - if ffi.typeof(ptr).cname != "struct ProviderStateParamPair *": - msg = ( - "ptr must be a struct ProviderStateParamPair, got" - f" {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "ProviderStateParamPair" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"ProviderStateParamPair({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the Provider State Param Pair. - """ - provider_state_param_pair_delete(self) - - @property - def key(self) -> str: - """ - Provider State Param key. - """ - s = ffi.string(self._ptr.key) # type: ignore[attr-defined] - if isinstance(s, bytes): - s = s.decode("utf-8") - return s - - @property - def value(self) -> str: - """ - Provider State Param value. - """ - s = ffi.string(self._ptr.value) # type: ignore[attr-defined] - if isinstance(s, bytes): - s = s.decode("utf-8") - return s - - -class SynchronousHttp: - def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: - """ - Initialise a new Synchronous HTTP Interaction. - - Args: - ptr: - CFFI data structure. - - owned: - Whether the message is owned by something else or not. This - determines whether the message should be freed when the Python - object is destroyed. - - Raises: - TypeError: - If the `ptr` is not a `struct SynchronousHttp`. - """ - if ffi.typeof(ptr).cname != "struct SynchronousHttp *": - msg = f"ptr must be a struct SynchronousHttp, got {ffi.typeof(ptr).cname}" - raise TypeError(msg) - self._ptr = ptr - self._owned = owned - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "SynchronousHttp" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"SynchronousHttp({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the SynchronousHttp. - """ - if not self._owned: - sync_http_delete(self) - - @property - def description(self) -> str: - """ - Description of this message interaction. - - This needs to be unique in the pact file. - """ - return sync_http_get_description(self) - - def provider_states(self) -> GeneratorType[ProviderState, None, None]: - """ - Optional provider state for the interaction. - """ - yield from sync_http_get_provider_state_iter(self) - return # Ensures that the parent object outlives the generator - - @property - def request_contents(self) -> str | bytes | None: - """ - The contents of the request. - """ - return sync_http_get_request_contents( - self - ) or sync_http_get_request_contents_bin(self) - - @property - def response_contents(self) -> str | bytes | None: - """ - The contents of the response. - """ - return sync_http_get_response_contents( - self - ) or sync_http_get_response_contents_bin(self) - - -class SynchronousMessage: - def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: - """ - Initialise a new Synchronous Message. - - Args: - ptr: - CFFI data structure. - - owned: - Whether the message is owned by something else or not. This - determines whether the message should be freed when the Python - object is destroyed. - - Raises: - TypeError: - If the `ptr` is not a `struct SynchronousMessage`. - """ - if ffi.typeof(ptr).cname != "struct SynchronousMessage *": - msg = ( - f"ptr must be a struct SynchronousMessage, got {ffi.typeof(ptr).cname}" - ) - raise TypeError(msg) - self._ptr = ptr - self._owned = owned - - def __str__(self) -> str: - """ - Nice string representation. - """ - return "SynchronousMessage" - - def __repr__(self) -> str: - """ - Debugging representation. - """ - return f"SynchronousMessage({self._ptr!r})" - - def __del__(self) -> None: - """ - Destructor for the SynchronousMessage. - """ - if not self._owned: - sync_message_delete(self) - - @property - def description(self) -> str: - """ - Description of this message interaction. - - This needs to be unique in the pact file. - """ - return sync_message_get_description(self) - - def provider_states(self) -> GeneratorType[ProviderState, None, None]: - """ - Optional provider state for the interaction. - """ - yield from sync_message_get_provider_state_iter(self) - return # Ensures that the parent object outlives the generator - - @property - def request_contents(self) -> MessageContents: - """ - The contents of the message. - """ - return sync_message_generate_request_contents(self) - - @property - def response_contents(self) -> GeneratorType[MessageContents, None, None]: - """ - The contents of the responses. - """ - yield from ( - sync_message_generate_response_contents(self, i) - for i in range(sync_message_get_number_responses(self)) - ) - return # Ensures that the parent object outlives the generator - - -class VerifierHandle: - """ - Handle to a Verifier. - - [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/verifier/handle/struct.VerifierHandle.html) - """ - - def __init__(self, ref: cffi.FFI.CData) -> None: - """ - Initialise a new Verifier Handle. - - Args: - ref: - Rust library reference to the Verifier Handle. - """ - self._ref = ref - - def __del__(self) -> None: - """ - Destructor for the Verifier Handle. - """ - verifier_shutdown(self) - - def __str__(self) -> str: - """ - String representation of the Verifier Handle. - """ - return f"VerifierHandle({hex(id(self._ref))})" - - def __repr__(self) -> str: - """ - String representation of the Verifier Handle. - """ - return f"" - - -class ExpressionValueType(Enum): - """ - Expression Value Type. - - [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/expressions/enum.ExpressionValueType.html) - """ - - UNKNOWN = lib.ExpressionValueType_Unknown - STRING = lib.ExpressionValueType_String - NUMBER = lib.ExpressionValueType_Number - INTEGER = lib.ExpressionValueType_Integer - DECIMAL = lib.ExpressionValueType_Decimal - BOOLEAN = lib.ExpressionValueType_Boolean - - def __str__(self) -> str: - """ - Informal string representation of the Expression Value Type. - """ - return self.name - - def __repr__(self) -> str: - """ - Information-rich string representation of the Expression Value Type. - """ - return f"ExpressionValueType.{self.name}" - - -class GeneratorCategory(Enum): - """ - Generator Category. - - [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/generators/enum.GeneratorCategory.html) - """ - - METHOD = lib.GeneratorCategory_METHOD - PATH = lib.GeneratorCategory_PATH - HEADER = lib.GeneratorCategory_HEADER - QUERY = lib.GeneratorCategory_QUERY - BODY = lib.GeneratorCategory_BODY - STATUS = lib.GeneratorCategory_STATUS - METADATA = lib.GeneratorCategory_METADATA - - def __str__(self) -> str: - """ - Informal string representation of the Generator Category. - """ - return self.name - - def __repr__(self) -> str: - """ - Information-rich string representation of the Generator Category. - """ - return f"GeneratorCategory.{self.name}" - - -class InteractionPart(Enum): - """ - Interaction Part. - - [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/enum.InteractionPart.html) - """ - - REQUEST = lib.InteractionPart_Request - RESPONSE = lib.InteractionPart_Response - - def __str__(self) -> str: - """ - Informal string representation of the Interaction Part. - """ - return self.name - - def __repr__(self) -> str: - """ - Information-rich string representation of the Interaction Part. - """ - return f"InteractionPath.{self.name}" - - -class LevelFilter(Enum): - """Level Filter.""" - - OFF = lib.LevelFilter_Off - ERROR = lib.LevelFilter_Error - WARN = lib.LevelFilter_Warn - INFO = lib.LevelFilter_Info - DEBUG = lib.LevelFilter_Debug - TRACE = lib.LevelFilter_Trace - - def __str__(self) -> str: - """ - Informal string representation of the Level Filter. - """ - return self.name - - def __repr__(self) -> str: - """ - Information-rich string representation of the Level Filter. - """ - return f"LevelFilter.{self.name}" - - -class MatchingRuleCategory(Enum): - """ - Matching Rule Category. - - [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) - """ - - METHOD = lib.MatchingRuleCategory_METHOD - PATH = lib.MatchingRuleCategory_PATH - HEADER = lib.MatchingRuleCategory_HEADER - QUERY = lib.MatchingRuleCategory_QUERY - BODY = lib.MatchingRuleCategory_BODY - STATUS = lib.MatchingRuleCategory_STATUS - CONTENTS = lib.MatchingRuleCategory_CONTENTS - METADATA = lib.MatchingRuleCategory_METADATA - - def __str__(self) -> str: - """ - Informal string representation of the Matching Rule Category. - """ - return self.name - - def __repr__(self) -> str: - """ - Information-rich string representation of the Matching Rule Category. - """ - return f"MatchingRuleCategory.{self.name}" - - -class PactSpecification(Enum): - """ - Pact Specification. - - [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/pact_specification/enum.PactSpecification.html) - """ - - UNKNOWN = lib.PactSpecification_Unknown - V1 = lib.PactSpecification_V1 - V1_1 = lib.PactSpecification_V1_1 - V2 = lib.PactSpecification_V2 - V3 = lib.PactSpecification_V3 - V4 = lib.PactSpecification_V4 - - @classmethod - def from_str(cls, version: str) -> PactSpecification: - """ - Instantiate a Pact Specification from a string. - - This method is case-insensitive, and allows for the version to be - specified with or without a leading "V", and with either a dot or an - underscore as the separator. - - Args: - version: - The version of the Pact Specification. - - Returns: - The Pact Specification. - """ - version = version.upper().replace(".", "_") - if version.startswith("V"): - return cls[version] - return cls["V" + version] - - def __str__(self) -> str: - """ - Informal string representation of the Pact Specification. - """ - return self.name - - def __repr__(self) -> str: - """ - Information-rich string representation of the Pact Specification. - """ - return f"PactSpecification.{self.name}" - - -class StringResult: - """ - String result. - """ - - class _StringResult(Enum): - """ - Internal enum from Pact FFI. - - [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/enum.StringResult.html) - """ - - FAILED = lib.StringResult_Failed - OK = lib.StringResult_Ok - - class _StringResultCData: - tag: int - ok: cffi.FFI.CData - failed: cffi.FFI.CData - - def __init__(self, cdata: cffi.FFI.CData) -> None: - """ - Initialise a new String Result. - - Args: - cdata: - CFFI data structure. - - Raises: - TypeError: - If the `cdata` is not a `struct StringResult`. - """ - if ffi.typeof(cdata).cname != "struct StringResult": - msg = f"cdata must be a struct StringResult, got {ffi.typeof(cdata).cname}" - raise TypeError(msg) - self._cdata = typing.cast("StringResult._StringResultCData", cdata) - - def __str__(self) -> str: - """ - String representation of the String Result. - """ - return self.text - - def __repr__(self) -> str: - """ - Debugging string representation of the String Result. - """ - return f"" - - @property - def is_failed(self) -> bool: - """ - Whether the result is an error. - """ - return self._cdata.tag == StringResult._StringResult.FAILED.value - - @property - def is_ok(self) -> bool: - """ - Whether the result is ok. - """ - return self._cdata.tag == StringResult._StringResult.OK.value - - @property - def text(self) -> str: - """ - The text of the result. - """ - # The specific `.ok` or `.failed` does not matter. - s = ffi.string(self._cdata.ok) - if isinstance(s, bytes): - return s.decode("utf-8") - return s - - def raise_exception(self) -> None: - """ - Raise an exception with the text of the result. - - Raises: - RuntimeError: - If the result is an error. - - Raises: - RuntimeError: - If the result is an error. - """ - if self.is_failed: - raise RuntimeError(self.text) - - -class OwnedString(str): - """ - A string that owns its own memory. - - This is used to ensure that the memory is freed when the string is - destroyed. - - As this is subclassed from `str`, it can be used in place of a normal string - in most cases. - """ - - __slots__ = ("_ptr", "_string") - - def __new__(cls, ptr: cffi.FFI.CData) -> Self: - """ - Create a new Owned String. - - As this is a subclass of the immutable type `str`, we need to override - the `__new__` method to ensure that the string is initialised correctly. - """ - s = ffi.string(ptr) - return super().__new__(cls, s if isinstance(s, str) else s.decode("utf-8")) - - def __init__(self, ptr: cffi.FFI.CData) -> None: - """ - Initialise a new Owned String. - - Args: - ptr: - CFFI data structure. - """ - self._ptr = ptr - s = ffi.string(ptr) - self._string = s if isinstance(s, str) else s.decode("utf-8") - - def __str__(self) -> str: - """ - String representation of the Owned String. - """ - return self._string - - def __repr__(self) -> str: - """ - Debugging string representation of the Owned String. - """ - return f"" - - def __del__(self) -> None: - """ - Destructor for the Owned String. - """ - string_delete(self) - - def __eq__(self, other: object) -> bool: - """ - Equality comparison. - - Args: - other: - The object to compare to. - - Returns: - Whether the two objects are equal. - """ - if isinstance(other, OwnedString): - return self._ptr == other._ptr - if isinstance(other, str): - return self._string == other - return super().__eq__(other) - - def __hash__(self) -> int: - """ - Hash the Owned String. - - Returns: - The hash of the Owned String. - """ - return hash(self._string) - - -def version() -> str: - """ - Return the version of the pact_ffi library. - - [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_version) - - Returns: - The version of the pact_ffi library as a string, in the form of `x.y.z`. - """ - v = ffi.string(lib.pactffi_version()) - if isinstance(v, bytes): - return v.decode("utf-8") - return v - - -def init(log_env_var: str) -> None: - """ - Initialise the mock server library. - - This can provide an environment variable name to use to set the log levels. - This function should only be called once, as it tries to install a global - tracing subscriber. - - [Rust - `pactffi_init`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_init) - - # Safety - - log_env_var must be a valid NULL terminated UTF-8 string. - """ - raise NotImplementedError - - -def init_with_log_level(level: str = "INFO") -> None: - """ - Initialises logging, and sets the log level explicitly. - - This function should only be called once, as it tries to install a global - tracing subscriber. - - [Rust - `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_init_with_log_level) - - Args: - level: - One of TRACE, DEBUG, INFO, WARN, ERROR, NONE/OFF. Case-insensitive. - - # Safety - - Exported functions are inherently unsafe. - """ - raise NotImplementedError - - -def enable_ansi_support() -> None: - """ - Enable ANSI coloured output on Windows. - - On non-Windows platforms, this function is a no-op. - - [Rust - `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_enable_ansi_support) - - # Safety - - This function is safe. - """ - raise NotImplementedError - - -def log_message( - message: str, - log_level: LevelFilter | str = LevelFilter.ERROR, - source: str | None = None, -) -> None: - """ - Log using the shared core logging facility. - - [Rust - `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_message) - - This is useful for callers to have a single set of logs. - - Args: - message: - The contents written to the log - - log_level: - The verbosity at which this message should be logged. - - source: - The source of the log, such as the class, module or caller. - """ - if isinstance(log_level, str): - log_level = LevelFilter[log_level.upper()] - if source is None: - source = inspect.stack()[1].function - lib.pactffi_log_message( - source.encode("utf-8"), - log_level.name.encode("utf-8"), - message.encode("utf-8"), - ) - - -def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: - """ - Get an iterator over mismatches. - - [Rust - `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_get_iter) - """ - raise NotImplementedError - - -def mismatches_delete(mismatches: Mismatches) -> None: - """ - Delete mismatches. - - [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_delete) - """ - raise NotImplementedError - - -def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: - """ - Get the next mismatch from a mismatches iterator. - - [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_iter_next) - - Returns a null pointer if no mismatches remain. - """ - raise NotImplementedError - - -def mismatches_iter_delete(iter: MismatchesIterator) -> None: - """ - Delete a mismatches iterator when you're done with it. - - [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_iter_delete) - """ - raise NotImplementedError - - -def mismatch_to_json(mismatch: Mismatch) -> str: - """ - Get a JSON representation of the mismatch. - - [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_to_json) - """ - raise NotImplementedError - - -def mismatch_type(mismatch: Mismatch) -> str: - """ - Get the type of a mismatch. - - [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_type) - """ - raise NotImplementedError - - -def mismatch_summary(mismatch: Mismatch) -> str: - """ - Get a summary of a mismatch. - - [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_summary) - """ - raise NotImplementedError - - -def mismatch_description(mismatch: Mismatch) -> str: - """ - Get a description of a mismatch. - - [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_description) - """ - raise NotImplementedError - - -def mismatch_ansi_description(mismatch: Mismatch) -> str: - """ - Get an ANSI-compatible description of a mismatch. - - [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_ansi_description) - """ - raise NotImplementedError - - -def get_error_message(length: int = 1024) -> str | None: - """ - Provide the error message from `LAST_ERROR` to the calling C code. - - [Rust - `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_get_error_message) - - This function should be called after any other function in the pact_matching - FFI indicates a failure with its own error message, if the caller wants to - get more context on why the error happened. - - Do note that this error-reporting mechanism only reports the top-level error - message, not any source information embedded in the original Rust error - type. If you want more detailed information for debugging purposes, use the - logging interface. - - Args: - length: - The length of the buffer to allocate for the error message. If the - error message is longer than this, it will be truncated. - - Returns: - A string containing the error message, or None if there is no error - message. - - Raises: - RuntimeError: - If the error message could not be retrieved. - """ - buffer = ffi.new("char[]", length) - ret: int = lib.pactffi_get_error_message(buffer, length) - - if ret >= 0: - # While the documentation says that the return value is the number of bytes - # written, the actually return value is always 0 on success. - if msg := ffi.string(buffer): - if isinstance(msg, bytes): - return msg.decode("utf-8") - return msg - return None - if ret == -1: - msg = "The provided buffer is a null pointer." - elif ret == -2: # noqa: PLR2004 - # Instead of returning an error here, we call the function again with a - # larger buffer. - return get_error_message(length * 32) - elif ret == -3: # noqa: PLR2004 - msg = "The write failed for some other reason." - elif ret == -4: # noqa: PLR2004 - msg = "The error message had an interior NULL." - else: - msg = "An unknown error occurred." - raise RuntimeError(msg) - - -def log_to_stdout(level_filter: LevelFilter) -> int: - """ - Convenience function to direct all logging to stdout. - - [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_stdout) - """ - raise NotImplementedError - - -def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: - """ - Convenience function to direct all logging to stderr. - - [Rust - `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_stderr) - - Args: - level_filter: - The level of logs to filter to. If a string is given, it must match - one of the [`LevelFilter`][pact.v3.ffi.LevelFilter] values (case - insensitive). - - Raises: - RuntimeError: - If there was an error setting the logger. - """ - if isinstance(level_filter, str): - level_filter = LevelFilter[level_filter.upper()] - ret: int = lib.pactffi_log_to_stderr(level_filter.value) - if ret != 0: - msg = "There was an unknown error setting the logger." - raise RuntimeError(msg) - - -def log_to_file(file_name: str, level_filter: LevelFilter) -> int: - """ - Convenience function to direct all logging to a file. - - [Rust - `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_file) - - # Safety - - This function will fail if the file_name pointer is invalid or does not - point to a NULL terminated string. - """ - raise NotImplementedError - - -def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: - """ - Convenience function to direct all logging to a task local memory buffer. - - [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_buffer) - - Raises: - RuntimeError: - If there was an error setting the logger. - """ - if isinstance(level_filter, str): - level_filter = LevelFilter[level_filter.upper()] - ret: int = lib.pactffi_log_to_buffer(level_filter.value) - if ret != 0: - msg = "There was an unknown error setting the logger." - raise RuntimeError(msg) - - -def logger_init() -> None: - """ - Initialize the FFI logger with no sinks. - - [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_init) - - This initialized logger does nothing until `pactffi_logger_apply` has been called. - - # Usage - - ```c - pactffi_logger_init(); - ``` - - # Safety - - This function is always safe to call. - """ - raise NotImplementedError - - -def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: - """ - Attach an additional sink to the thread-local logger. - - [Rust - `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_attach_sink) - - This logger does nothing until `pactffi_logger_apply` has been called. - - Types of sinks can be specified: - - - stdout (`pactffi_logger_attach_sink("stdout", LevelFilter_Info)`) - - stderr (`pactffi_logger_attach_sink("stderr", LevelFilter_Debug)`) - - file w/ file path (`pactffi_logger_attach_sink("file /some/file/path", - LevelFilter_Trace)`) - - buffer (`pactffi_logger_attach_sink("buffer", LevelFilter_Debug)`) - - # Usage - - ```c - int result = pactffi_logger_attach_sink("file /some/file/path", LogLevel_Filter); - ``` - - # Error Handling - - The return error codes are as follows: - - - `-1`: Can't set logger (applying the logger failed, perhaps because one is - applied already). - - `-2`: No logger has been initialized (call `pactffi_logger_init` before - any other log function). - - `-3`: The sink specifier was not UTF-8 encoded. - - `-4`: The sink type specified is not a known type (known types: "stdout", - "stderr", or "file /some/path"). - - `-5`: No file path was specified in a file-type sink specification. - - `-6`: Opening a sink to the specified file path failed (check - permissions). - - # Safety - - This function checks the validity of the passed-in sink specifier, and - errors out if the specifier isn't valid UTF-8. Passing in an invalid or NULL - pointer will result in undefined behaviour. - """ - raise NotImplementedError - - -def logger_apply() -> int: - """ - Apply the previously configured sinks and levels to the program. - - [Rust - `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_apply) - - If no sinks have been setup, will set the log level to info and the target - to standard out. - - This function will install a global tracing subscriber. Any attempts to - modify the logger after the call to `logger_apply` will fail. - """ - raise NotImplementedError - - -def fetch_log_buffer(log_id: str) -> str: - """ - Fetch the in-memory logger buffer contents. - - [Rust - `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_fetch_log_buffer) - - This will only have any contents if the `buffer` sink has been configured to - log to. The contents will be allocated on the heap and will need to be freed - with `pactffi_string_delete`. - - Fetches the logs associated with the provided identifier, or uses the - "global" one if the identifier is not specified (i.e. NULL). - - Returns a NULL pointer if the buffer can't be fetched. This can occur is - there is not sufficient memory to make a copy of the contents or the buffer - contains non-UTF-8 characters. - - # Safety - - This function will fail if the log_id pointer is invalid or does not point - to a NULL terminated string. - """ - raise NotImplementedError - - -def parse_pact_json(json: str) -> Pact: - """ - Parses the provided JSON into a Pact model. - - [Rust - `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_parse_pact_json) - - The returned Pact model must be freed with the `pactffi_pact_model_delete` - function when no longer needed. - - # Error Handling - - This function will return a NULL pointer if passed a NULL pointer or if an - error occurs. - """ - raise NotImplementedError - - -def pact_model_delete(pact: Pact) -> None: - """ - Frees the memory used by the Pact model. - - [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_model_delete) - """ - raise NotImplementedError - - -def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: - """ - Returns an iterator over all the interactions in the Pact. - - [Rust - `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_model_interaction_iterator) - - The iterator will have to be deleted using the - `pactffi_pact_interaction_iter_delete` function. The iterator will contain a - copy of the interactions, so it will not be affected but mutations to the - Pact model and will still function if the Pact model is deleted. - - # Safety This function is safe as long as the Pact pointer is a valid - pointer. - - # Errors On any error, this function will return a NULL pointer. - """ - raise NotImplementedError - - -def pact_spec_version(pact: Pact) -> PactSpecification: - """ - Returns the Pact specification enum that the Pact is for. - - [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_spec_version) - """ - raise NotImplementedError - - -def pact_interaction_delete(interaction: PactInteraction) -> None: - """ - Frees the memory used by the Pact interaction model. - - [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_delete) - """ - raise NotImplementedError - - -def async_message_new() -> AsynchronousMessage: - """ - Get a mutable pointer to a newly-created default message on the heap. - - [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_new) - - # Safety - - This function is safe. - - # Error Handling - - Returns NULL on error. - """ - raise NotImplementedError - - -def async_message_delete(message: AsynchronousMessage) -> None: - """ - Destroy the `AsynchronousMessage` being pointed to. - - [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_delete) - """ - lib.pactffi_async_message_delete(message._ptr) - - -def async_message_get_contents(message: AsynchronousMessage) -> MessageContents | None: - """ - Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. - - [Rust - `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents) - - If the message contents are missing, this function will return `None`. - """ - return MessageContents(lib.pactffi_async_message_get_contents(message._ptr)) - - -def async_message_generate_contents( - message: AsynchronousMessage, -) -> MessageContents | None: - """ - Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. - - This function differs from `async_message_get_contents` in - that it will process the message contents for any generators or matchers - that are present in the message in order to generate the actual message - contents as would be received by the consumer. - - [Rust - `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_generate_contents) - - If the message contents are missing, this function will return `None`. - """ - return MessageContents( - lib.pactffi_async_message_generate_contents(message._ptr), - owned=False, - ) - - -def async_message_get_contents_str(message: AsynchronousMessage) -> str: - """ - Get the message contents of an `AsynchronousMessage` in string form. - - [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_str) - - # Safety - - The returned string must be deleted with `pactffi_string_delete`. - - The returned string can outlive the message. - - # Error Handling - - If the message is NULL, returns NULL. If the body of the message - is missing, then this function also returns NULL. This means there's - no mechanism to differentiate with this function call alone between - a NULL message and a missing message body. - """ - raise NotImplementedError - - -def async_message_set_contents_str( - message: AsynchronousMessage, - contents: str, - content_type: str, -) -> None: - """ - Sets the contents of the message as a string. - - [Rust - `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_contents_str) - - - `message` - the message to set the contents for - - `contents` - pointer to contents to copy from. Must be a valid - NULL-terminated UTF-8 string pointer. - - `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - - # Safety - - The message contents and content type must either be NULL pointers, or point - to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is - undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. - """ - raise NotImplementedError - - -def async_message_get_contents_length(message: AsynchronousMessage) -> int: - """ - Get the length of the contents of a `AsynchronousMessage`. - - [Rust - `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_length) - - # Safety - - This function is safe. - - # Error Handling - - If the message is NULL, returns 0. If the body of the request is missing, - then this function also returns 0. - """ - raise NotImplementedError - - -def async_message_get_contents_bin(message: AsynchronousMessage) -> str: - """ - Get the contents of an `AsynchronousMessage` as bytes. - - [Rust - `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_bin) - - # Safety - - The number of bytes in the buffer will be returned by - `pactffi_async_message_get_contents_length`. It is safe to use the pointer - while the message is not deleted or changed. Using the pointer after the - message is mutated or deleted may lead to undefined behaviour. - - # Error Handling - - If the message is NULL, returns NULL. If the body of the message is missing, - then this function also returns NULL. - """ - raise NotImplementedError - - -def async_message_set_contents_bin( - message: AsynchronousMessage, - contents: str, - len: int, - content_type: str, -) -> None: - """ - Sets the contents of the message as an array of bytes. - - [Rust - `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_contents_bin) - - * `message` - the message to set the contents for - * `contents` - pointer to contents to copy from - * `len` - number of bytes to copy from the contents pointer - * `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - - # Safety - - The contents pointer must be valid for reads of `len` bytes, and it must be - properly aligned and consecutive. Otherwise behaviour is undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. - """ - raise NotImplementedError - - -def async_message_get_description(message: AsynchronousMessage) -> str: - r""" - Get a copy of the description. - - [Rust - `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_description) - - Raises: - RuntimeError: - If the description cannot be retrieved. - """ - ptr = lib.pactffi_async_message_get_description(message._ptr) - if ptr == ffi.NULL: - msg = "Unable to get the description from the message." - raise RuntimeError(msg) - return OwnedString(ptr) - - -def async_message_set_description( - message: AsynchronousMessage, - description: str, -) -> int: - """ - Write the `description` field on the `AsynchronousMessage`. - - [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_description) - - # Safety - - `description` must contain valid UTF-8. Invalid UTF-8 - will be replaced with U+FFFD REPLACEMENT CHARACTER. - - This function will only reallocate if the new string - does not fit in the existing buffer. - - # Error Handling - - Errors will be reported with a non-zero return value. - """ - raise NotImplementedError - - -def async_message_get_provider_state( - message: AsynchronousMessage, - index: int, -) -> ProviderState: - r""" - Get a copy of the provider state at the given index from this message. - - [Rust - `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_provider_state) - - Raises: - RuntimeError: - If the provider state cannot be retrieved. - """ - ptr = lib.pactffi_async_message_get_provider_state(message._ptr, index) - if ptr == ffi.NULL: - msg = "Unable to get the provider state from the message." - raise RuntimeError(msg) - return ProviderState(ptr) - - -def async_message_get_provider_state_iter( - message: AsynchronousMessage, -) -> ProviderStateIterator: - """ - Get an iterator over provider states. - - [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) - - # Safety - - The underlying data must not change during iteration. - """ - return ProviderStateIterator( - lib.pactffi_async_message_get_provider_state_iter(message._ptr) - ) - - -def consumer_get_name(consumer: Consumer) -> str: - r""" - Get a copy of this consumer's name. - - [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_consumer_get_name) - - The copy must be deleted with `pactffi_string_delete`. - - # Usage - - ```c - // Assuming `file_name` and `json_str` are already defined. - - MessagePact *message_pact = pactffi_message_pact_new_from_json(file_name, json_str); - if (message_pact == NULLPTR) { - // handle error. - } - - Consumer *consumer = pactffi_message_pact_get_consumer(message_pact); - if (consumer == NULLPTR) { - // handle error. - } - - char *name = pactffi_consumer_get_name(consumer); - if (name == NULL) { - // handle error. - } - - printf("%s\n", name); - - pactffi_string_delete(name); - ``` - - # Errors - - This function will fail if it is passed a NULL pointer, - or the Rust string contains an embedded NULL byte. - In the case of error, a NULL pointer will be returned. - """ - raise NotImplementedError - - -def pact_get_consumer(pact: Pact) -> Consumer: - """ - Get the consumer from a Pact. - - This returns a copy of the consumer model, and needs to be cleaned up with - `pactffi_pact_consumer_delete` when no longer required. - - [Rust - `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_get_consumer) - - # Errors - - This function will fail if it is passed a NULL pointer. In the case of - error, a NULL pointer will be returned. - """ - raise NotImplementedError - - -def pact_consumer_delete(consumer: Consumer) -> None: - """ - Frees the memory used by the Pact consumer. - - [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_consumer_delete) - """ - raise NotImplementedError - - -def message_contents_delete(contents: MessageContents) -> None: - """ - Delete the message contents instance. - - This should only be called on a message contents that require deletion. - The function creating the message contents should document whether it - requires deletion. - - Deleting a message content which is associated with an interaction - will result in undefined behaviour. - - [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_delete) - """ - lib.pactffi_message_contents_delete(contents._ptr) - - -def message_contents_get_contents_str(contents: MessageContents) -> str | None: - """ - Get the message contents in string form. - - [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_str) - - If the message has no contents or contain invalid UTF-8 characters, this - function will return `None`. - """ - ptr = lib.pactffi_message_contents_get_contents_str(contents._ptr) - if ptr == ffi.NULL: - return None - return OwnedString(ptr) - - -def message_contents_set_contents_str( - contents: MessageContents, - contents_str: str, - content_type: str, -) -> None: - """ - Sets the contents of the message as a string. - - [Rust - `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_set_contents_str) - - * `contents` - the message contents to set the contents for - * `contents_str` - pointer to contents to copy from. Must be a valid - NULL-terminated UTF-8 string pointer. - * `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - - # Safety - - The message contents and content type must either be NULL pointers, or point - to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is - undefined. - - # Error Handling - - If the contents string is a NULL pointer, it will set the message contents - as null. If the content type is a null pointer, or can't be parsed, it will - set the content type as unknown. - """ - raise NotImplementedError - - -def message_contents_get_contents_length(contents: MessageContents) -> int: - """ - Get the length of the message contents. - - [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_length) - - If the message has not contents, this function will return 0. - """ - return lib.pactffi_message_contents_get_contents_length(contents._ptr) - - -def message_contents_get_contents_bin(contents: MessageContents) -> bytes | None: - """ - Get the contents of a message as a pointer to an array of bytes. - - [Rust - `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_bin) - - If the message has no contents, this function will return `None`. - """ - ptr = lib.pactffi_message_contents_get_contents_bin(contents._ptr) - if ptr == ffi.NULL: - return None - return ffi.buffer( - ptr, - lib.pactffi_message_contents_get_contents_length(contents._ptr), - )[:] - - -def message_contents_set_contents_bin( - contents: MessageContents, - contents_bin: str, - len: int, - content_type: str, -) -> None: - """ - Sets the contents of the message as an array of bytes. - - [Rust - `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_set_contents_bin) - - * `message` - the message contents to set the contents for - * `contents_bin` - pointer to contents to copy from - * `len` - number of bytes to copy from the contents pointer - * `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - - # Safety - - The contents pointer must be valid for reads of `len` bytes, and it must be - properly aligned and consecutive. Otherwise behaviour is undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. - """ - raise NotImplementedError - - -def message_contents_get_metadata_iter( - contents: MessageContents, -) -> MessageMetadataIterator: - r""" - Get an iterator over the metadata of a message. - - [Rust - `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) - - # Safety - - This iterator carries a pointer to the message contents, and must not - outlive the message. - - The message metadata also must not be modified during iteration. If it is, - the old iterator must be deleted and a new iterator created. - - Raises: - RuntimeError: - If the metadata iterator cannot be retrieved. - """ - ptr = lib.pactffi_message_contents_get_metadata_iter(contents._ptr) - if ptr == ffi.NULL: - msg = "Unable to get the metadata iterator from the message contents." - raise RuntimeError(msg) - return MessageMetadataIterator(ptr) - - -def message_contents_get_matching_rule_iter( - contents: MessageContents, - category: MatchingRuleCategory, -) -> MatchingRuleCategoryIterator: - r""" - Get an iterator over the matching rules for a category of a message. - - [Rust - `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) - - The returned pointer must be deleted with - `pactffi_matching_rules_iter_delete` when done with it. - - Note that there could be multiple matching rules for the same key, so this - iterator will sequentially return each rule with the same key. - - For sample, given the following rules: - - ``` - "$.a" => Type, - "$.b" => Regex("\\d+"), Number - ``` - - This iterator will return a sequence of 3 values: - - - `("$.a", Type)` - - `("$.b", Regex("\d+"))` - - `("$.b", Number)` - - # Safety - - The iterator contains a copy of the data, so is safe to use when the message - or message contents has been deleted. - - # Error Handling - - On failure, this function will return a NULL pointer. - """ - return MatchingRuleCategoryIterator( - lib.pactffi_message_contents_get_matching_rule_iter(contents._ptr, category) - ) - - -def request_contents_get_matching_rule_iter( - request: HttpRequest, - category: MatchingRuleCategory, -) -> MatchingRuleCategoryIterator: - r""" - Get an iterator over the matching rules for a category of an HTTP request. - - [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) - - The returned pointer must be deleted with - `pactffi_matching_rules_iter_delete` when done with it. - - For sample, given the following rules: - - ``` - "$.a" => Type, - "$.b" => Regex("\d+"), Number - ``` - - This iterator will return a sequence of 3 values: - - - `("$.a", Type)` - - `("$.b", Regex("\d+"))` - - `("$.b", Number)` - - # Safety - - The iterator contains a copy of the data, so is safe to use when the - interaction or request contents has been deleted. - - # Error Handling - - On failure, this function will return a NULL pointer. - """ - raise NotImplementedError - - -def response_contents_get_matching_rule_iter( - response: HttpResponse, - category: MatchingRuleCategory, -) -> MatchingRuleCategoryIterator: - r""" - Get an iterator over the matching rules for a category of an HTTP response. - - [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) - - The returned pointer must be deleted with - `pactffi_matching_rules_iter_delete` when done with it. - - For sample, given the following rules: - - ``` - "$.a" => Type, - "$.b" => Regex("\d+"), Number - ``` - - This iterator will return a sequence of 3 values: - - - `("$.a", Type)` - - `("$.b", Regex("\d+"))` - - `("$.b", Number)` - - # Safety - - The iterator contains a copy of the data, so is safe to use when the - interaction or response contents has been deleted. - - # Error Handling - - On failure, this function will return a NULL pointer. - """ - raise NotImplementedError - - -def message_contents_get_generators_iter( - contents: MessageContents, - category: GeneratorCategory, -) -> GeneratorCategoryIterator: - """ - Get an iterator over the generators for a category of a message. - - [Rust - `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_generators_iter) - - # Safety - - The iterator contains a copy of the data, so is safe to use when the message - or message contents has been deleted. - - Raises: - RuntimeError: - If the generators iterator cannot be retrieved. - """ - ptr = lib.pactffi_message_contents_get_generators_iter(contents._ptr, category) - if ptr == ffi.NULL: - msg = "Unable to get the generators iterator from the message contents." - raise RuntimeError(msg) - return GeneratorCategoryIterator(ptr) - - -def request_contents_get_generators_iter( - request: HttpRequest, - category: GeneratorCategory, -) -> GeneratorCategoryIterator: - """ - Get an iterator over the generators for a category of an HTTP request. - - [Rust - `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_request_contents_get_generators_iter) - - The returned pointer must be deleted with `pactffi_generators_iter_delete` - when done with it. - - # Safety - - The iterator contains a copy of the data, so is safe to use when the - interaction or request contents has been deleted. - - # Error Handling - - On failure, this function will return a NULL pointer. - """ - raise NotImplementedError - - -def response_contents_get_generators_iter( - response: HttpResponse, - category: GeneratorCategory, -) -> GeneratorCategoryIterator: - """ - Get an iterator over the generators for a category of an HTTP response. - - [Rust - `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_contents_get_generators_iter) - - The returned pointer must be deleted with `pactffi_generators_iter_delete` - when done with it. - - # Safety - - The iterator contains a copy of the data, so is safe to use when the - interaction or response contents has been deleted. - - # Error Handling - - On failure, this function will return a NULL pointer. - """ - raise NotImplementedError - - -def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: - """ - Parse a matcher definition string into a MatchingRuleDefinition. - - The MatchingRuleDefinition contains the example value, and matching rules and - any generator. - - [Rust - `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_parse_matcher_definition) - - The following are examples of matching rule definitions: - - * `matching(type,'Name')` - type matcher with string value 'Name' - * `matching(number,100)` - number matcher - * `matching(datetime, 'yyyy-MM-dd','2000-01-01')` - datetime matcher with - format string - - See [Matching Rule definition - expressions](https://docs.rs/pact_models/latest/pact_models/matchingrules/expressions/index.html). - - The returned value needs to be freed up with the - `pactffi_matcher_definition_delete` function. - - # Errors If the expression is invalid, the MatchingRuleDefinition error will - be set. You can check for this value with the - `pactffi_matcher_definition_error` function. - - # Safety - - This function is safe if the expression is a valid NULL terminated string - pointer. - """ - raise NotImplementedError - - -def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: - """ - Returns any error message from parsing a matching definition expression. - - If there is no error, it will return a NULL pointer, otherwise returns the - error message as a NULL-terminated string. The returned string must be freed - using the `pactffi_string_delete` function once done with it. - - [Rust - `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_error) - """ - raise NotImplementedError - - -def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: - """ - Returns the value from parsing a matching definition expression. - - If there was an error, it will return a NULL pointer, otherwise returns the - value as a NULL-terminated string. The returned string must be freed using - the `pactffi_string_delete` function once done with it. - - [Rust - `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_value) - - Note that different expressions values can have types other than a string. - Use `pactffi_matcher_definition_value_type` to get the actual type of the - value. This function will always return the string representation of the - value. - """ - raise NotImplementedError - - -def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: - """ - Frees the memory used by the result of parsing the matching definition expression. - - [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_delete) - """ - raise NotImplementedError - - -def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Generator: - """ - Returns the generator from parsing a matching definition expression. - - If there was an error or there is no associated generator, it will return a - NULL pointer, otherwise returns the generator as a pointer. - - [Rust - `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_generator) - - The generator pointer will be a valid pointer as long as - `pactffi_matcher_definition_delete` has not been called on the definition. - Using the generator pointer after the definition has been deleted will - result in undefined behaviour. - """ - raise NotImplementedError - - -def matcher_definition_value_type( - definition: MatchingRuleDefinitionResult, -) -> ExpressionValueType: - """ - Returns the type of the value from parsing a matching definition expression. - - If there was an error parsing the expression, it will return Unknown. - - [Rust - `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_value_type) - """ - raise NotImplementedError - - -def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: - """ - Free the iterator when you're done using it. - - [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_iter_delete) - """ - raise NotImplementedError - - -def matcher_definition_iter( - definition: MatchingRuleDefinitionResult, -) -> MatchingRuleIterator: - """ - Returns an iterator over the matching rules from the parsed definition. - - The iterator needs to be deleted with the - `pactffi_matching_rule_iter_delete` function once done with it. - - [Rust - `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_iter) - - If there was an error parsing the expression, this function will return a - NULL pointer. - """ - raise NotImplementedError - - -def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: - """ - Get the next matching rule or reference from the iterator. - - As the values returned are owned by the iterator, they do not need to be - deleted but will be cleaned up when the iterator is deleted. - - [Rust - `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_iter_next) - - Will return a NULL pointer when the iterator has advanced past the end of - the list. - - # Safety - - This function is safe. - - # Error Handling - - This function will return a NULL pointer if passed a NULL pointer or if an - error occurs. - """ - raise NotImplementedError - - -def matching_rule_id(rule_result: MatchingRuleResult) -> int: - """ - Return the ID of the matching rule. - - [Rust - `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_id) - - The ID corresponds to the following rules: - - | Rule | ID | - | ---- | -- | - | Equality | 1 | - | Regex | 2 | - | Type | 3 | - | MinType | 4 | - | MaxType | 5 | - | MinMaxType | 6 | - | Timestamp | 7 | - | Time | 8 | - | Date | 9 | - | Include | 10 | - | Number | 11 | - | Integer | 12 | - | Decimal | 13 | - | Null | 14 | - | ContentType | 15 | - | ArrayContains | 16 | - | Values | 17 | - | Boolean | 18 | - | StatusCode | 19 | - | NotEmpty | 20 | - | Semver | 21 | - | EachKey | 22 | - | EachValue | 23 | - - # Safety - - This function is safe as long as the MatchingRuleResult pointer is a valid - pointer and the iterator has not been deleted. - """ - raise NotImplementedError - - -def matching_rule_value(rule_result: MatchingRuleResult) -> str: - """ - Returns the associated value for the matching rule. - - If the matching rule does not have an associated value, will return a NULL - pointer. - - [Rust - `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_value) - - The associated values for the rules are: - - | Rule | ID | VALUE | - | ---- | -- | ----- | - | Equality | 1 | NULL | - | Regex | 2 | Regex value | - | Type | 3 | NULL | - | MinType | 4 | Minimum value | - | MaxType | 5 | Maximum value | - | MinMaxType | 6 | "min:max" | - | Timestamp | 7 | Format string | - | Time | 8 | Format string | - | Date | 9 | Format string | - | Include | 10 | String value | - | Number | 11 | NULL | - | Integer | 12 | NULL | - | Decimal | 13 | NULL | - | Null | 14 | NULL | - | ContentType | 15 | Content type | - | ArrayContains | 16 | NULL | - | Values | 17 | NULL | - | Boolean | 18 | NULL | - | StatusCode | 19 | NULL | - | NotEmpty | 20 | NULL | - | Semver | 21 | NULL | - | EachKey | 22 | NULL | - | EachValue | 23 | NULL | - - Will return a NULL pointer if the matching rule was a reference or does not - have an associated value. - - # Safety - - This function is safe as long as the MatchingRuleResult pointer is a valid - pointer and the iterator it came from has not been deleted. - """ - raise NotImplementedError - - -def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: - """ - Returns the matching rule pointer for the matching rule. - - Will return a NULL pointer if the matching rule result was a reference. - - [Rust - `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_pointer) - - # Safety - - This function is safe as long as the MatchingRuleResult pointer is a valid - pointer and the iterator it came from has not been deleted. - """ - raise NotImplementedError - - -def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: - """ - Return any matching rule reference to a attribute by name. - - This is when the matcher should be configured to match the type of a - structure. I.e., - - [Rust - `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_reference_name) - - ```json - { - "pact:match": "eachValue(matching($'person'))", - "person": { - "name": "Fred", - "age": 100 - } - } - ``` - - Will return a NULL pointer if the matching rule was not a reference. - - # Safety - - This function is safe as long as the MatchingRuleResult pointer is a valid - pointer and the iterator has not been deleted. - """ - raise NotImplementedError - - -def validate_datetime(value: str, format: str) -> None: - """ - Validates the date/time value against the date/time format string. - - [Rust - `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_validate_datetime) - - Raises: - ValueError: - If the value is not a valid date/time for the format string. - - RuntimeError: - For any other error. - """ - ret = lib.pactffi_validate_datetime(value.encode(), format.encode()) - if ret == 0: - return - if ret == 1: - msg = f"Invalid datetime value {value!r}' for format {format!r}" - raise ValueError(msg) - if ret == 2: # noqa: PLR2004 - msg = f"Panic while validating datetime value: {get_error_message()}" - else: - msg = f"Unknown error while validating datetime value: {ret}" - raise RuntimeError(msg) - - -def generator_to_json(generator: Generator) -> str: - """ - Get the JSON form of the generator. - - [Rust - `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_to_json) - - The returned string must be deleted with `pactffi_string_delete`. - - # Safety - - This function will fail if it is passed a NULL pointer, or the owner of the - generator has been deleted. - """ - return OwnedString(lib.pactffi_generator_to_json(generator._ptr)) - - -def generator_generate_string(generator: Generator, context_json: str) -> str: - """ - Generate a string value using the provided generator. - - An optional JSON payload containing any generator context ca be given. The - context value is used for generators like `MockServerURL` (which should - contain details about the running mock server) and `ProviderStateGenerator` - (which should be the values returned from the Provider State callback - function). - - [Rust - `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_generate_string) - - If anything goes wrong, it will return a NULL pointer. - """ - ptr = lib.pactffi_generator_generate_string( - generator._ptr, - context_json.encode("utf-8"), - ) - s = ffi.string(ptr) - if isinstance(s, bytes): - s = s.decode("utf-8") - return s - - -def generator_generate_integer(generator: Generator, context_json: str) -> int: - """ - Generate an integer value using the provided generator. - - An optional JSON payload containing any generator context can be given. The - context value is used for generators like `ProviderStateGenerator` (which - should be the values returned from the Provider State callback function). - - [Rust - `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_generate_integer) - - If anything goes wrong or the generator is not a type that can generate an - integer value, it will return a zero value. - """ - return lib.pactffi_generator_generate_integer( - generator._ptr, - context_json.encode("utf-8"), - ) - - -def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: - """ - Free the iterator when you're done using it. - - [Rust - `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_delete) - """ - lib.pactffi_generators_iter_delete(iter._ptr) - - -def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePair: - """ - Get the next path and generator out of the iterator, if possible. - - [Rust - `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_next) - - The returned pointer must be deleted with - `pactffi_generator_iter_pair_delete`. - - Raises: - StopIteration: - If the iterator has reached the end. - """ - ptr = lib.pactffi_generators_iter_next(iter._ptr) - if ptr == ffi.NULL: - raise StopIteration - return GeneratorKeyValuePair(ptr) - - -def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: - """ - Free a pair of key and value returned from `pactffi_generators_iter_next`. - - [Rust - `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_pair_delete) - """ - lib.pactffi_generators_iter_pair_delete(pair._ptr) - - -def sync_http_new() -> SynchronousHttp: - """ - Get a mutable pointer to a newly-created default interaction on the heap. - - [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_new) - - # Safety - - This function is safe. - - # Error Handling - - Returns NULL on error. - """ - raise NotImplementedError - - -def sync_http_delete(interaction: SynchronousHttp) -> None: - """ - Destroy the `SynchronousHttp` interaction being pointed to. - - [Rust - `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_delete) - """ - lib.pactffi_sync_http_delete(interaction) - - -def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: - """ - Get the request of a `SynchronousHttp` interaction. - - [Rust - `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request) - - # Safety - - The data pointed to by the pointer this function returns will be deleted - when the interaction is deleted. Trying to use if after the interaction is - deleted will result in undefined behaviour. - - # Error Handling - - If the interaction is NULL, returns NULL. - """ - raise NotImplementedError - - -def sync_http_get_request_contents(interaction: SynchronousHttp) -> str | None: - """ - Get the request contents of a `SynchronousHttp` interaction in string form. - - [Rust - `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents) - - Note that this function will return `None` if either the body is missing or - is `null`. - """ - ptr = lib.pactffi_sync_http_get_request_contents(interaction._ptr) - if ptr == ffi.NULL: - return None - return OwnedString(ptr) - - -def sync_http_set_request_contents( - interaction: SynchronousHttp, - contents: str, - content_type: str, -) -> None: - """ - Sets the request contents of the interaction. - - [Rust - `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_request_contents) - - - `interaction` - the interaction to set the request contents for - - `contents` - pointer to contents to copy from. Must be a valid - NULL-terminated UTF-8 string pointer. - - `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - - # Safety - - The request contents and content type must either be NULL pointers, or point - to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is - undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the request contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. - """ - raise NotImplementedError - - -def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: - """ - Get the length of the request contents of a `SynchronousHttp` interaction. - - [Rust - `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) - - This function will return 0 if the body is missing. - """ - return lib.pactffi_sync_http_get_request_contents_length(interaction._ptr) - - -def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes | None: - """ - Get the request contents of a `SynchronousHttp` interaction as bytes. - - [Rust - `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) - - Note that this function will return `None` if either the body is missing or - is `null`. - """ - ptr = lib.pactffi_sync_http_get_request_contents_bin(interaction._ptr) - if ptr == ffi.NULL: - return None - return ffi.buffer( - ptr, - sync_http_get_request_contents_length(interaction), - )[:] - - -def sync_http_set_request_contents_bin( - interaction: SynchronousHttp, - contents: str, - len: int, - content_type: str, -) -> None: - """ - Sets the request contents of the interaction as an array of bytes. - - [Rust - `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) - - - `interaction` - the interaction to set the request contents for - - `contents` - pointer to contents to copy from - - `len` - number of bytes to copy from the contents pointer - - `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - - # Safety - - The contents pointer must be valid for reads of `len` bytes, and it must be - properly aligned and consecutive. Otherwise behaviour is undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the request contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. - """ - raise NotImplementedError - - -def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: - """ - Get the response of a `SynchronousHttp` interaction. - - [Rust - `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response) - - # Safety - - The data pointed to by the pointer this function returns will be deleted - when the interaction is deleted. Trying to use if after the interaction is - deleted will result in undefined behaviour. - - # Error Handling - - If the interaction is NULL, returns NULL. - """ - raise NotImplementedError - - -def sync_http_get_response_contents(interaction: SynchronousHttp) -> str | None: - """ - Get the response contents of a `SynchronousHttp` interaction in string form. - - [Rust - `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents) - - Note that this function will return `None` if either the body is missing or - is `null`. - """ - ptr = lib.pactffi_sync_http_get_response_contents(interaction._ptr) - if ptr == ffi.NULL: - return None - return OwnedString(ptr) - - -def sync_http_set_response_contents( - interaction: SynchronousHttp, - contents: str, - content_type: str, -) -> None: - """ - Sets the response contents of the interaction. - - [Rust - `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_response_contents) - - - `interaction` - the interaction to set the response contents for - - `contents` - pointer to contents to copy from. Must be a valid - NULL-terminated UTF-8 string pointer. - - `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - - # Safety - - The response contents and content type must either be NULL pointers, or - point to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is - undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the response contents as - null. If the content type is a null pointer, or can't be parsed, it will set - the content type as unknown. - """ - raise NotImplementedError - - -def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: - """ - Get the length of the response contents of a `SynchronousHttp` interaction. - - [Rust - `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) - - This function will return 0 if the body is missing. - """ - return lib.pactffi_sync_http_get_response_contents_length(interaction._ptr) - - -def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes | None: - """ - Get the response contents of a `SynchronousHttp` interaction as bytes. - - [Rust - `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) - - Note that this function will return `None` if either the body is missing or - is `null`. - """ - ptr = lib.pactffi_sync_http_get_response_contents_bin(interaction._ptr) - if ptr == ffi.NULL: - return None - return ffi.buffer( - ptr, - sync_http_get_response_contents_length(interaction), - )[:] - - -def sync_http_set_response_contents_bin( - interaction: SynchronousHttp, - contents: str, - len: int, - content_type: str, -) -> None: - """ - Sets the response contents of the `SynchronousHttp` interaction as bytes. - - [Rust - `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) - - - `interaction` - the interaction to set the response contents for - - `contents` - pointer to contents to copy from - - `len` - number of bytes to copy - - `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - - # Safety - - The contents pointer must be valid for reads of `len` bytes, and it must be - properly aligned and consecutive. Otherwise behaviour is undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the response contents as - null. If the content type is a null pointer, or can't be parsed, it will set - the content type as unknown. - """ - raise NotImplementedError - - -def sync_http_get_description(interaction: SynchronousHttp) -> str: - r""" - Get a copy of the description. - - [Rust - `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_description) - - Raises: - RuntimeError: - If the description cannot be retrieved - """ - ptr = lib.pactffi_sync_http_get_description(interaction._ptr) - if ptr == ffi.NULL: - msg = "Failed to get description" - raise RuntimeError(msg) - return OwnedString(ptr) - - -def sync_http_set_description(interaction: SynchronousHttp, description: str) -> int: - """ - Write the `description` field on the `SynchronousHttp`. - - [Rust - `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_description) - - # Safety - - `description` must contain valid UTF-8. Invalid UTF-8 will be replaced with - U+FFFD REPLACEMENT CHARACTER. - - This function will only reallocate if the new string does not fit in the - existing buffer. - - # Error Handling - - Errors will be reported with a non-zero return value. - """ - raise NotImplementedError - - -def sync_http_get_provider_state( - interaction: SynchronousHttp, - index: int, -) -> ProviderState: - r""" - Get a copy of the provider state at the given index from this interaction. - - [Rust - `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_provider_state) - - # Safety - - The returned structure must be deleted with `provider_state_delete`. - - Since it is a copy, the returned structure may safely outlive the - `SynchronousHttp`. - - # Error Handling - - On failure, this function will return a variant other than Success. - - This function may fail if the index requested is out of bounds, or if any of - the Rust strings contain embedded null ('\0') bytes. - """ - raise NotImplementedError - - -def sync_http_get_provider_state_iter( - interaction: SynchronousHttp, -) -> ProviderStateIterator: - """ - Get an iterator over provider states. - - [Rust - `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) - - # Safety - - The underlying data must not change during iteration. - - Raises: - RuntimeError: - If the iterator cannot be retrieved - """ - ptr = lib.pactffi_sync_http_get_provider_state_iter(interaction._ptr) - if ptr == ffi.NULL: - msg = "Failed to get provider state iterator" - raise RuntimeError(msg) - return ProviderStateIterator(ptr) - - -def pact_interaction_as_synchronous_http( - interaction: PactInteraction, -) -> SynchronousHttp: - r""" - Casts this interaction to a `SynchronousHttp` interaction. - - Returns a NULL pointer if the interaction can not be casted to a - `SynchronousHttp` interaction (for instance, it is a message interaction). - The returned pointer must be freed with `pactffi_sync_http_delete` when no - longer required. - - [Rust - `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) - - # Safety This function is safe as long as the interaction pointer is a valid - pointer. - - # Errors On any error, this function will return a NULL pointer. - """ - raise NotImplementedError - - -def pact_interaction_as_asynchronous_message( - interaction: PactInteraction, -) -> AsynchronousMessage: - """ - Casts this interaction to a `AsynchronousMessage` interaction. - - Returns a NULL pointer if the interaction can not be casted to a - `AsynchronousMessage` interaction (for instance, it is a http interaction). - The returned pointer must be freed with `pactffi_async_message_delete` when - no longer required. - - [Rust - `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) - - Note that if the interaction is a V3 `Message`, it will be converted to a V4 - `AsynchronousMessage` before being returned. - - # Safety This function is safe as long as the interaction pointer is a valid - pointer. - - # Errors On any error, this function will return a NULL pointer. - """ - raise NotImplementedError - - -def pact_interaction_as_synchronous_message( - interaction: PactInteraction, -) -> SynchronousMessage: - """ - Casts this interaction to a `SynchronousMessage` interaction. - - Returns a NULL pointer if the interaction can not be casted to a - `SynchronousMessage` interaction (for instance, it is a http interaction). - The returned pointer must be freed with `pactffi_sync_message_delete` when - no longer required. - - [Rust - `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) - - # Safety This function is safe as long as the interaction pointer is a valid - pointer. - - # Errors On any error, this function will return a NULL pointer. - """ - raise NotImplementedError - - -def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> AsynchronousMessage: - """ - Get the next asynchronous message from the iterator. - - [Rust - `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_async_message_iter_next) - - Raises: - StopIteration: - If the iterator has reached the end. - """ - ptr = lib.pactffi_pact_async_message_iter_next(iter._ptr) - if ptr == ffi.NULL: - raise StopIteration - return AsynchronousMessage(ptr, owned=True) - - -def pact_async_message_iter_delete(iter: PactAsyncMessageIterator) -> None: - """ - Free the iterator when you're done using it. - - [Rust - `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_async_message_iter_delete) - """ - lib.pactffi_pact_async_message_iter_delete(iter._ptr) - - -def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMessage: - """ - Get the next synchronous request/response message from the V4 pact. - - [Rust - `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_message_iter_next) - - Raises: - StopIteration: - If the iterator has reached the end. - """ - ptr = lib.pactffi_pact_sync_message_iter_next(iter._ptr) - if ptr == ffi.NULL: - raise StopIteration - return SynchronousMessage(ptr, owned=True) - - -def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: - """ - Free the iterator when you're done using it. - - [Rust - `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) - """ - lib.pactffi_pact_sync_message_iter_delete(iter._ptr) - - -def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: - """ - Get the next synchronous HTTP request/response interaction from the V4 pact. - - [Rust - `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_http_iter_next) - - Raises: - StopIteration: - If the iterator has reached the end. - """ - ptr = lib.pactffi_pact_sync_http_iter_next(iter._ptr) - if ptr == ffi.NULL: - raise StopIteration - return SynchronousHttp(ptr, owned=True) - - -def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: - """ - Free the iterator when you're done using it. - - [Rust - `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) - """ - lib.pactffi_pact_sync_http_iter_delete(iter._ptr) - - -def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction: - """ - Get the next interaction from the pact. - - [Rust - `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_iter_next) - - Raises: - StopIteration: - If the iterator has reached the end. - """ - ptr = lib.pactffi_pact_interaction_iter_next(iter._ptr) - if ptr == ffi.NULL: - raise StopIteration - raise NotImplementedError - return PactInteraction(ptr) - - -def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: - """ - Free the iterator when you're done using it. - - [Rust - `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_iter_delete) - """ - lib.pactffi_pact_interaction_iter_delete(iter._ptr) - - -def matching_rule_to_json(rule: MatchingRule) -> str: - """ - Get the JSON form of the matching rule. - - [Rust - `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_to_json) - - The returned string must be deleted with `pactffi_string_delete`. - - # Safety - - This function will fail if it is passed a NULL pointer, or the iterator that - owns the value of the matching rule has been deleted. - """ - return OwnedString(lib.pactffi_matching_rule_to_json(rule._ptr)) - - -def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: - """ - Free the iterator when you're done using it. - - [Rust - `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_delete) - """ - lib.pactffi_matching_rules_iter_delete(iter._ptr) - - -def matching_rules_iter_next( - iter: MatchingRuleCategoryIterator, -) -> MatchingRuleKeyValuePair: - """ - Get the next path and matching rule out of the iterator, if possible. - - [Rust - `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_next) - - The returned pointer must be deleted with - `pactffi_matching_rules_iter_pair_delete`. - - # Safety - - The underlying data is owned by the `MatchingRuleKeyValuePair`, so is always - safe to use. - - # Error Handling - - If no further data is present, returns NULL. - """ - return MatchingRuleKeyValuePair(lib.pactffi_matching_rules_iter_next(iter._ptr)) - - -def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: - """ - Free a pair of key and value returned from `message_metadata_iter_next`. - - [Rust - `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) - """ - lib.pactffi_matching_rules_iter_pair_delete(pair._ptr) - - -def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: - """ - Get the next value from the iterator. - - [Rust - `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_iter_next) - - # Safety - - The underlying data must not change during iteration. - - Raises: - StopIteration: - If no further data is present, or if an internal error occurs. - """ - provider_state = lib.pactffi_provider_state_iter_next(iter._ptr) - if provider_state == ffi.NULL: - raise StopIteration - return ProviderState(provider_state) - - -def provider_state_iter_delete(iter: ProviderStateIterator) -> None: - """ - Delete the iterator. - - [Rust - `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_iter_delete) - """ - lib.pactffi_provider_state_iter_delete(iter._ptr) - - -def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadataPair: - """ - Get the next key and value out of the iterator, if possible. - - [Rust - `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_iter_next) - - The returned pointer must be deleted with - `pactffi_message_metadata_pair_delete`. - - # Safety - - The underlying data must not change during iteration. This function must - only ever be called from a foreign language. Calling it from a Rust function - that has a Tokio runtime in its call stack can result in a deadlock. - - Raises: - StopIteration: - If no further data is present. - """ - ptr = lib.pactffi_message_metadata_iter_next(iter._ptr) - if ptr == ffi.NULL: - raise StopIteration - return MessageMetadataPair(ptr) - - -def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: - """ - Free the metadata iterator when you're done using it. - - [Rust - `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_iter_delete) - """ - lib.pactffi_message_metadata_iter_delete(iter._ptr) - - -def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: - """ - Free a pair of key and value returned from `message_metadata_iter_next`. - - [Rust - `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_pair_delete) - """ - lib.pactffi_message_metadata_pair_delete(pair._ptr) - - -def provider_get_name(provider: Provider) -> str: - r""" - Get a copy of this provider's name. - - [Rust - `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_get_name) - - The copy must be deleted with `pactffi_string_delete`. - - # Usage - - ```c - // Assuming `file_name` and `json_str` are already defined. - - MessagePact *message_pact = pactffi_message_pact_new_from_json(file_name, json_str); - if (message_pact == NULLPTR) { - // handle error. - } - - Provider *provider = pactffi_message_pact_get_provider(message_pact); - if (provider == NULLPTR) { - // handle error. - } - - char *name = pactffi_provider_get_name(provider); - if (name == NULL) { - // handle error. - } - - printf("%s\n", name); - - pactffi_string_delete(name); - ``` - - # Errors - - This function will fail if it is passed a NULL pointer, or the Rust string - contains an embedded NULL byte. In the case of error, a NULL pointer will be - returned. - """ - raise NotImplementedError - - -def pact_get_provider(pact: Pact) -> Provider: - """ - Get the provider from a Pact. - - This returns a copy of the provider model, and needs to be cleaned up with - `pactffi_pact_provider_delete` when no longer required. - - [Rust - `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_get_provider) - - # Errors - - This function will fail if it is passed a NULL pointer. In the case of - error, a NULL pointer will be returned. - """ - raise NotImplementedError - - -def pact_provider_delete(provider: Provider) -> None: - """ - Frees the memory used by the Pact provider. - - [Rust - `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_provider_delete) - """ - raise NotImplementedError - - -def provider_state_get_name(provider_state: ProviderState) -> str | None: - """ - Get the name of the provider state as a string. - - [Rust - `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_get_name) - - Raises: - RuntimeError: - If the name could not be retrieved. - """ - ptr = lib.pactffi_provider_state_get_name(provider_state._ptr) - if ptr == ffi.NULL: - msg = "Failed to get provider state name." - raise RuntimeError(msg) - return OwnedString(ptr) - - -def provider_state_get_param_iter( - provider_state: ProviderState, -) -> ProviderStateParamIterator: - r""" - Get an iterator over the params of a provider state. - - [Rust - `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_get_param_iter) - - # Safety - - This iterator carries a pointer to the provider state, and must not outlive - the provider state. - - The provider state params also must not be modified during iteration. If it - is, the old iterator must be deleted and a new iterator created. - - Raises: - RuntimeError: - If the iterator could not be created. - """ - ptr = lib.pactffi_provider_state_get_param_iter(provider_state._ptr) - if ptr == ffi.NULL: - msg = "Failed to get provider state param iterator." - raise RuntimeError(msg) - return ProviderStateParamIterator(ptr) - - -def provider_state_param_iter_next( - iter: ProviderStateParamIterator, -) -> ProviderStateParamPair: - """ - Get the next key and value out of the iterator, if possible. - - [Rust - `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_iter_next) - - # Safety - - The underlying data must not be modified during iteration. - - Raises: - StopIteration: - If no further data is present. - """ - provider_state_param = lib.pactffi_provider_state_param_iter_next(iter._ptr) - if provider_state_param == ffi.NULL: - raise StopIteration - return ProviderStateParamPair(provider_state_param) - - -def provider_state_delete(provider_state: ProviderState) -> None: - """ - Free the provider state when you're done using it. - - [Rust - `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_delete) - """ - raise NotImplementedError - - -def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: - """ - Free the provider state param iterator when you're done using it. - - [Rust - `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_iter_delete) - """ - lib.pactffi_provider_state_param_iter_delete(iter._ptr) - - -def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: - """ - Free a pair of key and value. - - [Rust - `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_pair_delete) - """ - lib.pactffi_provider_state_param_pair_delete(pair._ptr) - - -def sync_message_new() -> SynchronousMessage: - """ - Get a mutable pointer to a newly-created default message on the heap. - - [Rust - `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_new) - - # Safety - - This function is safe. - - # Error Handling - - Returns NULL on error. - """ - raise NotImplementedError - - -def sync_message_delete(message: SynchronousMessage) -> None: - """ - Destroy the `Message` being pointed to. - - [Rust - `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_delete) - """ - lib.pactffi_sync_message_delete(message._ptr) - - -def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: - """ - Get the request contents of a `SynchronousMessage` in string form. - - [Rust - `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) - - # Safety - - The returned string must be deleted with `pactffi_string_delete`. - - The returned string can outlive the message. - - # Error Handling - - If the message is NULL, returns NULL. If the body of the request message is - missing, then this function also returns NULL. This means there's no - mechanism to differentiate with this function call alone between a NULL - message and a missing message body. - """ - raise NotImplementedError - - -def sync_message_set_request_contents_str( - message: SynchronousMessage, - contents: str, - content_type: str, -) -> None: - """ - Sets the request contents of the message. - - [Rust - `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) - - - `message` - the message to set the request contents for - - `contents` - pointer to contents to copy from. Must be a valid - NULL-terminated UTF-8 string pointer. - - `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - - # Safety - - The message contents and content type must either be NULL pointers, or point - to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is - undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. - """ - raise NotImplementedError - - -def sync_message_get_request_contents_length(message: SynchronousMessage) -> int: - """ - Get the length of the request contents of a `SynchronousMessage`. - - [Rust - `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) - - # Safety - - This function is safe. - - # Error Handling - - If the message is NULL, returns 0. If the body of the request is missing, - then this function also returns 0. - """ - raise NotImplementedError - - -def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes: - """ - Get the request contents of a `SynchronousMessage` as a bytes. - - [Rust - `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) - - # Safety - - The number of bytes in the buffer will be returned by - `pactffi_sync_message_get_request_contents_length`. It is safe to use the - pointer while the message is not deleted or changed. Using the pointer after - the message is mutated or deleted may lead to undefined behaviour. - - # Error Handling - - If the message is NULL, returns NULL. If the body of the message is missing, - then this function also returns NULL. - """ - raise NotImplementedError - - -def sync_message_set_request_contents_bin( - message: SynchronousMessage, - contents: str, - len: int, - content_type: str, -) -> None: - """ - Sets the request contents of the message as an array of bytes. - - [Rust - `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) - - * `message` - the message to set the request contents for - * `contents` - pointer to contents to copy from - * `len` - number of bytes to copy from the contents pointer - * `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - - # Safety - - The contents pointer must be valid for reads of `len` bytes, and it must be - properly aligned and consecutive. Otherwise behaviour is undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. - """ - raise NotImplementedError - - -def sync_message_get_request_contents(message: SynchronousMessage) -> MessageContents: - """ - Get the request contents of an `SynchronousMessage` as a `MessageContents`. - - [Rust - `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents) - - # Safety - - The data pointed to by the pointer this function returns will be deleted - when the message is deleted. Trying to use if after the message is deleted - will result in undefined behaviour. - - # Error Handling - - If the message is NULL, returns NULL. - """ - raise NotImplementedError - - -def sync_message_generate_request_contents( - message: SynchronousMessage, -) -> MessageContents: - """ - Get the request contents of an `SynchronousMessage` as a `MessageContents`. - - This function differs from `pactffi_sync_message_get_request_contents` in - that it will process the message contents for any generators or matchers - that are present in the message in order to generate the actual message - contents as would be received by the consumer. - - [Rust - `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_generate_request_contents) - - Raises: - RuntimeError: - If the request contents cannot be generated - """ - ptr = lib.pactffi_sync_message_generate_request_contents(message._ptr) - if ptr == ffi.NULL: - msg = "Failed to generate request contents" - raise RuntimeError(msg) - return MessageContents(ptr, owned=False) - - -def sync_message_get_number_responses(message: SynchronousMessage) -> int: - """ - Get the number of response messages in the `SynchronousMessage`. - - [Rust - `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_number_responses) - - If the message is null, this function will return 0. - """ - return lib.pactffi_sync_message_get_number_responses(message._ptr) - - -def sync_message_get_response_contents_str( - message: SynchronousMessage, - index: int, -) -> str: - """ - Get the response contents of a `SynchronousMessage` in string form. - - [Rust - `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) - - # Safety - - The returned string must be deleted with `pactffi_string_delete`. - - The returned string can outlive the message. - - # Error Handling - - If the message is NULL or the index is not valid, returns NULL. - - If the body of the response message is missing, then this function also - returns NULL. This means there's no mechanism to differentiate with this - function call alone between a NULL message and a missing message body. - """ - raise NotImplementedError - - -def sync_message_set_response_contents_str( - message: SynchronousMessage, - index: int, - contents: str, - content_type: str, -) -> None: - """ - Sets the response contents of the message as a string. - - If index is greater - than the number of responses in the message, the responses will be padded - with default values. - - [Rust - `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) - - * `message` - the message to set the response contents for - * `index` - index of the response to set. 0 is the first response. - * `contents` - pointer to contents to copy from. Must be a valid - NULL-terminated UTF-8 string pointer. - * `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - - # Safety - - The message contents and content type must either be NULL pointers, or point - to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is - undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the response contents as - null. If the content type is a null pointer, or can't be parsed, it will set - the content type as unknown. - """ - raise NotImplementedError - - -def sync_message_get_response_contents_length( - message: SynchronousMessage, - index: int, -) -> int: - """ - Get the length of the response contents of a `SynchronousMessage`. - - [Rust - `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) - - # Safety - - This function is safe. - - # Error Handling - - If the message is NULL or the index is not valid, returns 0. If the body of - the request is missing, then this function also returns 0. - """ - raise NotImplementedError - - -def sync_message_get_response_contents_bin( - message: SynchronousMessage, - index: int, -) -> bytes: - """ - Get the response contents of a `SynchronousMessage` as bytes. - - [Rust - `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) - - # Safety - - The number of bytes in the buffer will be returned by - `pactffi_sync_message_get_response_contents_length`. It is safe to use the - pointer while the message is not deleted or changed. Using the pointer after - the message is mutated or deleted may lead to undefined behaviour. - - # Error Handling - - If the message is NULL or the index is not valid, returns NULL. If the body - of the message is missing, then this function also returns NULL. - """ - raise NotImplementedError - - -def sync_message_set_response_contents_bin( - message: SynchronousMessage, - index: int, - contents: str, - len: int, - content_type: str, -) -> None: - """ - Sets the response contents of the message at the given index as bytes. - - If index is greater than the number of responses in the message, the - responses will be padded with default values. - - [Rust - `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) - - * `message` - the message to set the response contents for - * `index` - index of the response to set. 0 is the first response - * `contents` - pointer to contents to copy from - * `len` - number of bytes to copy - * `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - - # Safety - - The contents pointer must be valid for reads of `len` bytes, and it must be - properly aligned and consecutive. Otherwise behaviour is undefined. - - # Error Handling - - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. - """ - raise NotImplementedError - - -def sync_message_get_response_contents( - message: SynchronousMessage, - index: int, -) -> MessageContents: - """ - Get the response contents of an `SynchronousMessage` as a `MessageContents`. - - [Rust - `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents) - - # Safety - - The data pointed to by the pointer this function returns will be deleted - when the message is deleted. Trying to use if after the message is deleted - will result in undefined behaviour. - - # Error Handling - - If the message is NULL or the index is not valid, returns NULL. - """ - raise NotImplementedError - - -def sync_message_generate_response_contents( - message: SynchronousMessage, - index: int, -) -> MessageContents: - """ - Get the response contents of an `SynchronousMessage` as a `MessageContents`. - - This function differs from - `sync_message_get_response_contents` in that it will process - the message contents for any generators or matchers that are present in - the message in order to generate the actual message contents as would be - received by the consumer. - - [Rust - `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_generate_response_contents) - - Raises: - RuntimeError: - If the response contents could not be generated. - """ - ptr = lib.pactffi_sync_message_generate_response_contents(message._ptr, index) - if ptr == ffi.NULL: - msg = "Failed to generate response contents." - raise RuntimeError(msg) - return MessageContents(ptr, owned=False) - - -def sync_message_get_description(message: SynchronousMessage) -> str: - r""" - Get a copy of the description. - - [Rust - `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_description) - - Raises: - RuntimeError: - If the description could not be retrieved - """ - ptr = lib.pactffi_sync_message_get_description(message._ptr) - if ptr == ffi.NULL: - msg = "Failed to get description." - raise RuntimeError(msg) - return OwnedString(ptr) - - -def sync_message_set_description(message: SynchronousMessage, description: str) -> int: - """ - Write the `description` field on the `SynchronousMessage`. - - [Rust - `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_description) - - # Safety - - `description` must contain valid UTF-8. Invalid UTF-8 will be replaced with - U+FFFD REPLACEMENT CHARACTER. - - This function will only reallocate if the new string does not fit in the - existing buffer. - - # Error Handling - - Errors will be reported with a non-zero return value. - """ - raise NotImplementedError - - -def sync_message_get_provider_state( - message: SynchronousMessage, - index: int, -) -> ProviderState: - r""" - Get a copy of the provider state at the given index from this message. - - [Rust - `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_provider_state) - - # Safety - - The returned structure must be deleted with `provider_state_delete`. - - Since it is a copy, the returned structure may safely outlive the - `SynchronousMessage`. - - # Error Handling - - On failure, this function will return a variant other than Success. - - This function may fail if the index requested is out of bounds, or if any of - the Rust strings contain embedded null ('\0') bytes. - """ - raise NotImplementedError - - -def sync_message_get_provider_state_iter( - message: SynchronousMessage, -) -> ProviderStateIterator: - """ - Get an iterator over provider states. - - [Rust - `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) - - # Safety - - The underlying data must not change during iteration. - - Raises: - RuntimeError: - If the iterator could not be created. - """ - ptr = lib.pactffi_sync_message_get_provider_state_iter(message._ptr) - if ptr == ffi.NULL: - msg = "Failed to get provider state iterator." - raise RuntimeError(msg) - return ProviderStateIterator(ptr) - - -def string_delete(string: OwnedString) -> None: - """ - Delete a string previously returned by this FFI. - - [Rust - `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_string_delete) - """ - lib.pactffi_string_delete(string._ptr) - - -def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: - """ - [DEPRECATED] External interface to create a HTTP mock server. - - A pointer to the pact JSON as a NULL-terminated C string is passed in, as - well as the port for the mock server to run on. A value of 0 for the port - will result in a port being allocated by the operating system. The port of - the mock server is returned. - - [Rust - `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server) - - * `pact_str` - Pact JSON - * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:80) - * `tls` - boolean flag to indicate of the mock server should use TLS (using - a self-signed certificate) - - This function is deprecated and replaced with - `pactffi_create_mock_server_for_transport`. - - # Errors - - Errors are returned as negative values. - - | Error | Description | - |-------|-------------| - | -1 | A null pointer was received | - | -2 | The pact JSON could not be parsed | - | -3 | The mock server could not be started | - | -4 | The method panicked | - | -5 | The address is not valid | - | -6 | Could not create the TLS configuration with the self-signed certificate | - """ - warnings.warn( - "This function is deprecated, use create_mock_server_for_transport instead", - DeprecationWarning, - stacklevel=2, - ) - raise NotImplementedError - - -def get_tls_ca_certificate() -> OwnedString: - """ - Fetch the CA Certificate used to generate the self-signed certificate. - - [Rust - `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_get_tls_ca_certificate) - - **NOTE:** The string for the result is allocated on the heap, and will have - to be freed by the caller using pactffi_string_delete. - - # Errors - - An empty string indicates an error reading the pem file. - """ - return OwnedString(lib.pactffi_get_tls_ca_certificate()) - - -def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) -> int: - """ - [DEPRECATED] External interface to create a HTTP mock server. - - A Pact handle is passed in, as well as the port for the mock server to run - on. A value of 0 for the port will result in a port being allocated by the - operating system. The port of the mock server is returned. - - [Rust - `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server_for_pact) - - * `pact` - Handle to a Pact model created with created with - `pactffi_new_pact`. - * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:0). - Must be a valid UTF-8 NULL-terminated string. - * `tls` - boolean flag to indicate of the mock server should use TLS (using - a self-signed certificate) - - This function is deprecated and replaced with - `pactffi_create_mock_server_for_transport`. - - # Errors - - Errors are returned as negative values. - - | Error | Description | - |-------|-------------| - | -1 | An invalid handle was received. Handles should be created with `pactffi_new_pact` | - | -3 | The mock server could not be started | - | -4 | The method panicked | - | -5 | The address is not valid | - | -6 | Could not create the TLS configuration with the self-signed certificate | - """ # noqa: E501 - warnings.warn( - "This function is deprecated, use create_mock_server_for_transport instead", - DeprecationWarning, - stacklevel=2, - ) - raise NotImplementedError - - -def create_mock_server_for_transport( - pact: PactHandle, - addr: str, - port: int, - transport: str, - transport_config: str | None, -) -> PactServerHandle: - """ - Create a mock server for the provided Pact handle and transport. - - [Rust - `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server_for_transport) - - Args: - pact: - Handle to the Pact model. - - addr: - The address to bind to. - - port: - The port number to bind to. A value of zero will result in the - operating system allocating an available port. - - transport: - The transport to use (i.e. http, https, grpc). The underlying Pact - library will interpret this, typically in a case-sensitive way. - - transport_config: - Configuration to be passed to the transport. This must be a valid - JSON string, or `None` if not required. - - Returns: - A handle to the mock server. - - Raises: - RuntimeError: - If the mock server could not be created. The error message will - contain details of the error. - """ - ret: int = lib.pactffi_create_mock_server_for_transport( - pact._ref, - addr.encode("utf-8"), - port, - transport.encode("utf-8"), - (transport_config.encode("utf-8") if transport_config else ffi.NULL), - ) - if ret > 0: - return PactServerHandle(ret) - - if ret == -1: - msg = f"An invalid Pact handle was received: {pact}." - elif ret == -2: # noqa: PLR2004 - msg = "Invalid transport_config JSON." - elif ret == -3: # noqa: PLR2004 - msg = f"Pact mock server could not be started for {pact}." - elif ret == -4: # noqa: PLR2004 - msg = f"Panick during Pact mock server creation for {pact}." - elif ret == -5: # noqa: PLR2004 - msg = f"Address is invalid: {addr}." - else: - msg = f"An unknown error occurred during Pact mock server creation for {pact}." - raise RuntimeError(msg) - - -def mock_server_matched(mock_server_handle: PactServerHandle) -> bool: - """ - External interface to check if a mock server has matched all its requests. - - If all requests have been matched, `true` is returned. `false` is returned - if any request has not been successfully matched, or the method panics. - - [Rust - `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_matched) - """ - return lib.pactffi_mock_server_matched(mock_server_handle._ref) - - -def mock_server_mismatches( - mock_server_handle: PactServerHandle, -) -> list[dict[str, Any]]: - """ - External interface to get all the mismatches from a mock server. - - [Rust - `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_mismatches) - - # Errors - - Raises: - RuntimeError: - If there is no mock server with the provided port number, or the - function panics. - """ - ptr = lib.pactffi_mock_server_mismatches(mock_server_handle._ref) - if ptr == ffi.NULL: - msg = f"No mock server found with port {mock_server_handle}." - raise RuntimeError(msg) - string = ffi.string(ptr) - if isinstance(string, bytes): - string = string.decode("utf-8") - return json.loads(string) - - -def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: - """ - External interface to cleanup a mock server. - - This function will try terminate the mock server with the given port number - and cleanup any memory allocated for it. - - [Rust - `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_cleanup_mock_server) - - Args: - mock_server_handle: - Handle to the mock server to cleanup. - - Raises: - RuntimeError: - If the mock server could not be cleaned up. - """ - success: bool = lib.pactffi_cleanup_mock_server(mock_server_handle._ref) - if not success: - msg = f"Could not cleanup mock server with port {mock_server_handle._ref}" - raise RuntimeError(msg) - - -def write_pact_file( - mock_server_handle: PactServerHandle, - directory: str | Path, - *, - overwrite: bool, -) -> None: - """ - External interface to trigger a mock server to write out its pact file. - - This function should be called if all the consumer tests have passed. The - directory to write the file to is passed as the second parameter. - - [Rust - `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_write_pact_file) - - Args: - mock_server_handle: - Handle to the mock server to write the pact file for. - - directory: - Directory to write the pact file to. - - overwrite: - Whether to overwrite any existing pact files. If this is false, the - pact file will be merged with any existing pact file. - - Raises: - RuntimeError: - If there was an error writing the pact file. - """ - ret: int = lib.pactffi_write_pact_file( - mock_server_handle._ref, - str(directory).encode("utf-8"), - overwrite, - ) - if ret == 0: - return - if ret == 1: - msg = ( - f"The function panicked while writing the Pact for {mock_server_handle} in" - f" {directory}." - ) - elif ret == 2: # noqa: PLR2004 - msg = ( - f"The Pact file for {mock_server_handle} could not be written in" - f" {directory}." - ) - elif ret == 3: # noqa: PLR2004 - msg = f"The Pact for the {mock_server_handle} was not found." - else: - msg = ( - "An unknown error occurred while writing the Pact for" - f" {mock_server_handle} in {directory}." - ) - raise RuntimeError(msg) - - -def mock_server_logs(mock_server_handle: PactServerHandle) -> str: - """ - Fetch the logs for the mock server. - - This needs the memory buffer log sink to be setup before the mock server is - started. - - [Rust - `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_logs) - - Raises: - RuntimeError: - If the logs for the mock server can not be retrieved. - """ - ptr = lib.pactffi_mock_server_logs(mock_server_handle._ref) - if ptr == ffi.NULL: - msg = f"Unable to obtain logs from {mock_server_handle!r}" - raise RuntimeError(msg) - string = ffi.string(ptr) - if isinstance(string, bytes): - string = string.decode("utf-8") - return string - - -def generate_datetime_string(format: str) -> StringResult: - """ - Generates a datetime value from the provided format string. - - This uses the current system date and time NOTE: The memory for the returned - string needs to be freed with the `pactffi_string_delete` function - - [Rust - `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generate_datetime_string) - - # Safety - - If the format string pointer is NULL or has invalid UTF-8 characters, an - error result will be returned. If the format string pointer is not a valid - pointer or is not a NULL-terminated string, this will lead to undefined - behaviour. - """ - raise NotImplementedError - - -def check_regex(regex: str, example: str) -> bool: - """ - Checks that the example string matches the given regex. - - [Rust - `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_check_regex) - - # Safety - - Both the regex and example pointers must be valid pointers to - NULL-terminated strings. Invalid pointers will result in undefined - behaviour. - """ - raise NotImplementedError - - -def generate_regex_value(regex: str) -> StringResult: - """ - Generates an example string based on the provided regex. - - NOTE: The memory for the returned string needs to be freed with the - `pactffi_string_delete` function. - - [Rust - `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generate_regex_value) - - # Safety - - The regex pointer must be a valid pointer to a NULL-terminated string. - Invalid pointers will result in undefined behaviour. - """ - raise NotImplementedError - - -def free_string(s: str) -> None: - """ - [DEPRECATED] Frees the memory allocated to a string by another function. - - [Rust - `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_free_string) - - This function is deprecated. Use `pactffi_string_delete` instead. - - # Safety - - The string pointer can be NULL (which is a no-op), but if it is not a valid - pointer the call will result in undefined behaviour. - """ - warnings.warn( - "This function is deprecated, use string_delete instead", - DeprecationWarning, - stacklevel=2, - ) - raise NotImplementedError - - -def new_pact(consumer_name: str, provider_name: str) -> PactHandle: - """ - Creates a new Pact model and returns a handle to it. - - [Rust - `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_pact) - - Args: - consumer_name: - The name of the consumer for the pact. - - provider_name: - The name of the provider for the pact. - - Returns: - Handle to the new Pact model. - """ - return PactHandle( - lib.pactffi_new_pact( - consumer_name.encode("utf-8"), - provider_name.encode("utf-8"), - ), - ) - - -def pact_handle_to_pointer(pact: PactHandle) -> Pact: - """ - Unwraps a Pact handle to the underlying Pact. - - The Pact model which has been cloned from the Pact handle's inner Pact - model. - - The returned Pact model must be freed with the `pactffi_pact_model_delete` - function when no longer needed. - """ - raise NotImplementedError - - -def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: - """ - Creates a new HTTP Interaction and returns a handle to it. - - Calling this function with the same description as an existing interaction - will result in that interaction being replaced with the new one. - - [Rust - `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_interaction) - - Args: - pact: - Handle to the Pact model. - - description: - The interaction description. It needs to be unique for each Pact. - - Returns: - Handle to the new Interaction. - """ - return InteractionHandle( - lib.pactffi_new_interaction( - pact._ref, - description.encode("utf-8"), - ), - ) - - -def new_message_interaction(pact: PactHandle, description: str) -> InteractionHandle: - """ - Creates a new message interaction and returns a handle to it. - - Calling this function with the same description as an existing interaction - will result in that interaction being replaced with the new one. - - [Rust - `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_message_interaction) - - Args: - pact: - Handle to the Pact model. - - description: - The interaction description. It needs to be unique for each Pact. - - Returns: - Handle to the new Interaction - """ - return InteractionHandle( - lib.pactffi_new_message_interaction( - pact._ref, - description.encode("utf-8"), - ), - ) - - -def new_sync_message_interaction( - pact: PactHandle, - description: str, -) -> InteractionHandle: - """ - Creates a new synchronous message interaction and returns a handle to it. - - Calling this function with the same description as an existing interaction - will result in that interaction being replaced with the new one. - - [Rust - `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_sync_message_interaction) - - Args: - pact: - Handle to the Pact model. - - description: - The interaction description. It needs to be unique for each Pact. - - Returns: - Handle to the new Interaction - """ - return InteractionHandle( - lib.pactffi_new_sync_message_interaction( - pact._ref, - description.encode("utf-8"), - ), - ) - - -def upon_receiving(interaction: InteractionHandle, description: str) -> None: - """ - Sets the description for the Interaction. - - [Rust - `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_upon_receiving) - - This function - - Args: - interaction: - Handle to the Interaction. - - description: - The interaction description. It needs to be unique for each Pact. - - Raises: - NotImplementedError: - This function has intentionally been left unimplemented. - - RuntimeError: - If the interaction description could not be set. - """ - # This function has intentionally been left unimplemented. The rationale is - # to avoid code of the form: - # - # ```python - # .with_request("GET", "/") - # .upon_receiving("some new description") - # ``` - raise NotImplementedError - - success: bool = lib.pactffi_upon_receiving( - interaction._ref, - description.encode("utf-8"), - ) - if not success: - msg = "The interaction description could not be set." - raise RuntimeError(msg) - - -def given(interaction: InteractionHandle, description: str) -> None: - """ - Adds a provider state to the Interaction. - - [Rust - `pactffi_given`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given) - - Args: - interaction: - Handle to the Interaction. - - description: - The provider state description. It needs to be unique. - - Raises: - RuntimeError: - If the provider state could not be specified. - """ - success: bool = lib.pactffi_given(interaction._ref, description.encode("utf-8")) - if not success: - msg = "The provider state could not be specified." - raise RuntimeError(msg) - - -def interaction_test_name(interaction: InteractionHandle, test_name: str) -> None: - """ - Sets the test name annotation for the interaction. - - This allows capturing the name of the test as metadata. This can only be - used with V4 interactions. - - [Rust - `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_interaction_test_name) - - Args: - interaction: - Handle to the Interaction. - - test_name: - The test name to set. - - Raises: - RuntimeError: - If the test name can not be set. - - """ - ret: int = lib.pactffi_interaction_test_name( - interaction._ref, - test_name.encode("utf-8"), - ) - if ret == 0: - return - if ret == 1: - msg = f"Function panicked: {get_error_message()}" - elif ret == 2: # noqa: PLR2004 - msg = f"Invalid handle: {interaction}." - elif ret == 3: # noqa: PLR2004 - msg = f"Mock server for {interaction} has already started." - elif ret == 4: # noqa: PLR2004 - msg = f"Interaction {interaction} is not a V4 interaction." - else: - msg = f"Unknown error setting test name for {interaction}." - raise RuntimeError(msg) - - -def given_with_param( - interaction: InteractionHandle, - description: str, - name: str, - value: str, -) -> None: - """ - Adds a parameter key and value to a provider state to the Interaction. - - If the provider state does not exist, a new one will be created, otherwise - the parameter will be merged into the existing one. The parameter value will - be parsed as JSON. - - [Rust - `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given_with_param) - - Args: - interaction: - Handle to the Interaction. - - description: - The provider state description. - - name: - Parameter name. - - value: - Parameter value as JSON. - - Raises: - RuntimeError: - If the interaction state could not be updated. - """ - success: bool = lib.pactffi_given_with_param( - interaction._ref, - description.encode("utf-8"), - name.encode("utf-8"), - value.encode("utf-8"), - ) - if not success: - msg = "The interaction state could not be updated." - raise RuntimeError(msg) - - -def given_with_params( - interaction: InteractionHandle, - description: str, - params: str, -) -> None: - """ - Adds a provider state to the Interaction. - - If the params is not an JSON object, it will add it as a single parameter - with a `value` key. - - [Rust - `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given_with_params) - - Args: - interaction: - Handle to the Interaction. - - description: - The provider state description. - - params: - Parameter values as a JSON fragment. - - Raises: - RuntimeError: - If the interaction state could not be updated. - """ - ret: int = lib.pactffi_given_with_params( - interaction._ref, - description.encode("utf-8"), - params.encode("utf-8"), - ) - if ret == 0: - return - if ret == 1: - msg = "The interaction state could not be updated." - elif ret == 2: # noqa: PLR2004 - msg = f"Internal error: {get_error_message()}" - elif ret == 3: # noqa: PLR2004 - msg = "Invalid C string." - else: - msg = "Unknown error." - raise RuntimeError(msg) - - -def with_request(interaction: InteractionHandle, method: str, path: str) -> None: - r""" - Configures the request for the Interaction. - - [Rust - `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_request) - - Args: - interaction: - Handle to the Interaction. - - method: - The request HTTP method. - - path: - The request path. - - This may be a simple string in which case it will be used as-is, or - it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) - which allows regex patterns. For examples: - - ```json - { - "value": "/path/to/100", - "pact:matcher:type": "regex", - "regex": "/path/to/\\d+" - } - ``` - - Raises: - RuntimeError: - If the request could not be specified. - """ - success: bool = lib.pactffi_with_request( - interaction._ref, - method.encode("utf-8"), - path.encode("utf-8"), - ) - if not success: - msg = f"The request '{method} {path}' could not be specified for {interaction}." - raise RuntimeError(msg) - - -def with_query_parameter_v2( - interaction: InteractionHandle, - name: str, - index: int, - value: str, -) -> None: - r""" - Configures a query parameter for the Interaction. - - [Rust - `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_query_parameter_v2) - - To setup a query parameter with multiple values, you can either call this - function multiple times with a different index value: - - ```python - with_query_parameter_v2(handle, "version", 0, "2") - with_query_parameter_v2(handle, "version", 0, "3") - ``` - - Or you can call it once with a JSON value that contains multiple values: - - ```python - with_query_parameter_v2( - handle, - "version", - 0, - json.dumps({"value": ["2", "3"]}), - ) - ``` - - The JSON value can also contain a matcher, which will be used to match the - query parameter value. For example, a semver matcher might look like this: - - ```python - with_query_parameter_v2( - handle, - "version", - 0, - json.dumps({ - "value": "1.2.3", - "pact:matcher:type": "regex", - "regex": r"\d+\.\d+\.\d+", - }), - ) - ``` - - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) - - If you want the matching rules to apply to all values (and not just the one - with the given index), make sure to set the value to be an array. - - ```python - with_query_parameter_v2( - handle, - "id", - 0, - json.dumps({ - "value": ["2"], - "pact:matcher:type": "regex", - "regex": r"\d+", - }), - ) - ``` - - For query parameters with no value, two distinct formats are provided: - - 1. Parameters with blank values, as specified by `?foo=&bar=`, require an - empty string: - - ```python - with_query_parameter_v2(handle, "foo", 0, "") - with_query_parameter_v2(handle, "bar", 0, "") - ``` - - 2. Parameters with no associated value, as specified by `?foo&bar`, require - a NULL pointer: - - ```python - with_query_parameter_v2(handle, "foo", 0, None) - with_query_parameter_v2(handle, "bar", 0, None) - ``` - - Args: - interaction: - Handle to the Interaction. - - name: - The query parameter name. - - index: - The index of the value (starts at 0). You can use this to create a - query parameter with multiple values. - - value: - The query parameter value. - - This may be a simple string in which case it will be used as-is, or - it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). - - Raises: - RuntimeError: - If there was an error setting the query parameter. - """ - success: bool = lib.pactffi_with_query_parameter_v2( - interaction._ref, - name.encode("utf-8"), - index, - value.encode("utf-8"), - ) - if not success: - msg = f"Failed to add query parameter {name} to request {interaction}." - raise RuntimeError(msg) - - -def with_specification(pact: PactHandle, version: PactSpecification) -> None: - """ - Sets the specification version for a given Pact model. - - [Rust - `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_specification) - - Args: - pact: - Handle to a Pact model. - - version: - The spec version to use. - - Raises: - RuntimeError: - If the Pact specification could not be set. - """ - success: bool = lib.pactffi_with_specification(pact._ref, version.value) - if not success: - msg = f"Failed to set Pact specification for {pact}" - raise RuntimeError(msg) - - -def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: - """ - Fetches the Pact specification version for the given Pact model. - - [Rust - `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_handle_get_pact_spec_version) - - Args: - handle: - Handle to a Pact model. - - Returns: - The spec version for the Pact model. - """ - return PactSpecification(lib.pactffi_handle_get_pact_spec_version(handle._ref)) - - -def with_pact_metadata( - pact: PactHandle, - namespace: str, - name: str, - value: str, -) -> None: - """ - Sets the additional metadata on the Pact file. - - Common uses are to add the client library details such as the name and - version Returns false if the interaction or Pact can't be modified (i.e. the - mock server for it has already started) - - [Rust - `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_pact_metadata) - - Args: - pact: - Handle to a Pact model - - namespace: - The top level metadat key to set any key values on - - name: - The key to set - - value: - The value to set - - Raises: - RuntimeError: - If the metadata could not be set. - """ - success: bool = lib.pactffi_with_pact_metadata( - pact._ref, - namespace.encode("utf-8"), - name.encode("utf-8"), - value.encode("utf-8"), - ) - if not success: - msg = f"Failed to set Pact metadata for {pact} with {namespace}.{name}={value}" - raise RuntimeError(msg) - - -def with_metadata( - interaction: InteractionHandle, - key: str, - value: str, - part: InteractionPart, -) -> None: - r""" - Adds metadata to the interaction. - - Metadata is only relevant for message interactions to provide additional - information about the message, such as the queue name, message type, tags, - timestamps, etc. - - * `key` - metadata key - * `value` - metadata value, supports JSON structures with matchers and - generators. Passing a `NULL` point will remove the metadata key instead. - * `part` - the part of the interaction to add the metadata to (only - relevant for synchronous message interactions). - - Returns `true` if the metadata was added successfully, `false` otherwise. - - To include matching rules for the value, include the matching rule JSON - format with the value as a single JSON document. I.e. - - ```python with_metadata( - handle, "TagData", json.dumps({ - "value": {"ID": "sjhdjkshsdjh", "weight": 100.5}, - "pact:matcher:type": "type", - }), - ) - ``` - - See - [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) - - # Note - - For HTTP interactions, use [`with_header_v2`][pact.v3.ffi.with_header_v2] - instead. This function will not have any effect on HTTP interactions and - returns `false`. - - For synchronous message interactions, the `part` parameter is required to - specify whether the metadata should be added to the request or response - part. For responses which can have multiple messages, the metadata will be - set on all response messages. This also requires for responses to have been - defined in the interaction. - - The [`with_body`][pact.v3.ffi.with_body] will also contribute to the - metadata of the message (both sync and async) by setting the key - `contentType` with the content type of the message. - - # Safety - - The key and value parameters must be valid pointers to NULL terminated - strings, or `NULL` for the value parameter if the metadata key should be - removed. - - Raises: - RuntimeError: - If the metadata could not be set. - """ - success: bool = lib.pactffi_with_metadata( - interaction._ref, - key.encode("utf-8"), - value.encode("utf-8"), - part.value, - ) - if not success: - msg = f"Failed to set metadata for {interaction} with {key}={value}" - raise RuntimeError(msg) - - -def with_header_v2( - interaction: InteractionHandle, - part: InteractionPart, - name: str, - index: int, - value: str, -) -> None: - r""" - Configures a header for the Interaction. - - [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_header_v2) - - To setup a header with multiple values, you can either call this - function multiple times with a different index value: - - ```python - with_header_v2(handle, part, "Accept-Version", 0, "2") - with_header_v2(handle, part, "Accept-Version", 0, "3") - ``` - - Or you can call it once with a JSON value that contains multiple values: - - ```python - with_header_v2( - handle, - part, - "Accept-Version", - 0, - json.dumps({"value": ["2", "3"]}), - ) - ``` - - The JSON value can also contain a matcher, which will be used to match the - query parameter value. For example, a semver matcher might look like this: - - ```python - with_query_parameter_v2( - handle, - "Accept-Version", - 0, - json.dumps({ - "value": "1.2.3", - "pact:matcher:type": "regex", - "regex": r"\d+\.\d+\.\d+", - }), - ) - ``` - - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) - - Args: - interaction: - Handle to the Interaction. - - part: - The part of the interaction to add the header to (Request or - Response). - - name: - The header name. This is case insensitive. - - index: - The index of the value (starts at 0). You can use this to create a - header with multiple values. - - value: - The header value. - - This may be a simple string in which case it will be used as-is, or - it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). - - Raises: - RuntimeError: - If there was an error setting the header. - """ - success: bool = lib.pactffi_with_header_v2( - interaction._ref, - part.value, - name.encode("utf-8"), - index, - value.encode("utf-8"), - ) - if not success: - msg = f"The header {name!r} could not be specified for {interaction}." - raise RuntimeError(msg) - - -def set_header( - interaction: InteractionHandle, - part: InteractionPart, - name: str, - value: str, -) -> None: - """ - Sets a header for the Interaction. - - Note that this function will overwrite any previously set header values. - Also, this function will not process the value in any way, so matching rules - and generators can not be configured with it. - - [Rust - `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_header) - - If matching rules are required to be set, use `pactffi_with_header_v2`. - - Args: - interaction: - Handle to the Interaction. - - part: - The part of the interaction to add the header to (Request or - Response). - - name: - The header name. This is case insensitive. - - value: - The header value. This is handled as-is, with no processing. - - Raises: - RuntimeError: - If the header could not be set. - """ - success: bool = lib.pactffi_set_header( - interaction._ref, - part.value, - name.encode("utf-8"), - value.encode("utf-8"), - ) - if not success: - msg = f"The header {name!r} could not be set for {interaction}." - raise RuntimeError(msg) - - -def response_status(interaction: InteractionHandle, status: int) -> None: - """ - Configures the response for the Interaction. - - [Rust - `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_status) - - Args: - interaction: - Handle to the Interaction. - - status: - The response status. Defaults to 200. - - Raises: - RuntimeError: - If the response status could not be set. - """ - success: bool = lib.pactffi_response_status(interaction._ref, status) - if not success: - msg = f"The response status {status} could not be set for {interaction}." - raise RuntimeError(msg) - - -def response_status_v2(interaction: InteractionHandle, status: str) -> None: - """ - Configures the response for the Interaction. - - [Rust - `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_status_v2) - - To include matching rules for the status (only statusCode or integer really - makes sense to use), include the matching rule JSON format with the value as - a single JSON document. I.e. - - ```python - response_status_v2( - handle, - json.dumps({ - "pact:generator:type": "RandomInt", - "min": 100, - "max": 399, - "pact:matcher:type": "statusCode", - "status": "nonError", - }), - ) - ``` - - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) - - Args: - interaction: - Handle to the Interaction. - - status: - The response status. Defaults to 200. - - This may be a simple string in which case it will be used as-is, or - it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). - - Raises: - RuntimeError: - If the response status could not be set. - """ - success: bool = lib.pactffi_response_status_v2( - interaction._ref, status.encode("utf-8") - ) - if not success: - msg = f"The response status {status} could not be set for {interaction}." - raise RuntimeError(msg) - - -def with_body( - interaction: InteractionHandle, - part: InteractionPart, - content_type: str | None, - body: str | None, -) -> None: - """ - Adds the body for the interaction. - - [Rust - `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_body) - - Returns false if the interaction or Pact can't be modified (i.e. the mock - server for it has already started) - - If the `content_type` is determined as follows, whichever is first: - - - The `content_type` argument to this function - - The `Content-Type` header for HTTP interaction, or `contentType` metadata - entry for message interactions. - - From automatic detection of the body contents. - - Defaults to `text/plain` as a last resort. - - Furthermore, the `Content-Type` header or `contentType` metadata entry will - be updated with the above determined content type, _unless_ it is already - set. - - This function will overwrite the body contents if they exist, with the - exception of the response part of synchronous message interactions, where a - new response will be appended. - - Args: - interaction: - Handle to the Interaction. - - part: - The part of the interaction to add the body to (Request or - Response). This is ignored for asynchronous message interactions. - - content_type: - The content type of the body, or `None` to use the internal logic. - - body: - The body contents. For JSON payloads, matching rules can be embedded - in the body. See - [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). - - Raises: - RuntimeError: - If the body could not be specified. - """ - success: bool = lib.pactffi_with_body( - interaction._ref, - part.value, - content_type.encode("utf-8") if content_type else ffi.NULL, - body.encode("utf-8") if body else None, - ) - if not success: - msg = f"Unable to set body for {interaction}." - raise RuntimeError(msg) - - -def with_binary_body( - interaction: InteractionHandle, - part: InteractionPart, - content_type: str | None, - body: bytes | None, -) -> None: - """ - Adds the body for the interaction. - - [Rust - `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_binary_body) - - For HTTP and async message interactions, this will overwrite the body. With - asynchronous messages, the part parameter will be ignored. With synchronous - messages, the request contents will be overwritten, while a new response - will be appended to the message. - - Args: - interaction: - Handle to the Interaction. - - part: - The part of the interaction to add the body to (Request or - Response). - - content_type: - The content type of the body. Will be ignored if a content type - header is already set. If `None`, the content type will be set to - `application/octet-stream`. - - body: - The body contents. If `None`, the body will be set to null. - - Raises: - RuntimeError: - If the body could not be modified. - """ - if len(gc.get_referrers(body)) == 0: - warnings.warn( - "Make sure to assign the body to a variable to avoid having the byte array" - " modified.", - UserWarning, - stacklevel=3, - ) - success: bool = lib.pactffi_with_binary_body( - interaction._ref, - part.value, - content_type.encode("utf-8") if content_type else ffi.NULL, - body if body else ffi.NULL, - len(body) if body else 0, - ) - if not success: - msg = f"Unable to set body for {interaction}." - raise RuntimeError(msg) - - -def with_binary_file( - interaction: InteractionHandle, - part: InteractionPart, - content_type: str | None, - body: bytes | None, -) -> None: - """ - Adds a binary file as the body with the expected content type and contents. - - !!! warning - - This function is deprecated. Use - [`with_binary_body`][pact.v3.ffi.with_binary_body] in order to set the - binary body, and use - [`with_matching_rules`][pact.v3.ffi.with_matching_rules] to set the - matching rules to ensure that only the content type is being matched. - - Will use a mime type matcher to match the body. Returns false if the - interaction or Pact can't be modified (i.e. the mock server for it has - already started) - - [Rust - `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_binary_file) - - For HTTP and async message interactions, this will overwrite the body. With - asynchronous messages, the part parameter will be ignored. With synchronous - messages, the request contents will be overwritten, while a new response - will be appended to the message. - - Args: - interaction: - Handle to the Interaction. - - part: - The part of the interaction to add the body to (Request or - Response). - - content_type: - The content type of the body. Will be ignored if a content type - header is already set. - - body: - The body contents. If `None`, the body will be set to null. - - Raises: - RuntimeError: - If the body could not be set. - """ - if len(gc.get_referrers(body)) == 0: - warnings.warn( - "Make sure to assign the body to a variable to avoid having the byte array" - " modified.", - UserWarning, - stacklevel=3, - ) - success: bool = lib.pactffi_with_binary_file( - interaction._ref, - part.value, - content_type.encode("utf-8") if content_type else ffi.NULL, - body if body else ffi.NULL, - len(body) if body else 0, - ) - if not success: - msg = f"Unable to set body for {interaction}." - raise RuntimeError(msg) - - -def with_matching_rules( - interaction: InteractionHandle, - part: InteractionPart, - rules: str, -) -> None: - """ - Add matching rules to the interaction. - - [Rust - `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_matching_rules) - - This function can be called multiple times, in which case the matching - rules will be merged. - - Args: - interaction: - Handle to the Interaction. - - part: - Request or response part (if applicable). - - rules: - JSON string of the matching rules to add to the interaction. - - Raises: - RuntimeError: - If the rules could not be added. - """ - success: bool = lib.pactffi_with_matching_rules( - interaction._ref, - part.value, - rules.encode("utf-8"), - ) - if not success: - msg = f"Unable to set matching rules for {interaction}." - raise RuntimeError(msg) - - -def with_generators( - interaction: InteractionHandle, - part: InteractionPart, - generators: str, -) -> None: - """ - Add generators to the interaction. - - [Rust - `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_generators) - - This function can be called multiple times, in which case the generators - will be combined (provide they don't clash). - - For synchronous messages which allow multiple responses, the generators will - be added to all the responses. - - Args: - interaction: - Handle to the Interaction. - - part: - Request or response part (if applicable). - - generators: - JSON string of the generators to add to the interaction. - - Raises: - RuntimeError: - If the generators could not be added. - """ - success: bool = lib.pactffi_with_generators( - interaction._ref, - part.value, - generators.encode("utf-8"), - ) - if not success: - msg = f"Unable to set generators for {interaction}." - raise RuntimeError(msg) - - -def with_multipart_file_v2( # noqa: PLR0913 - interaction: InteractionHandle, - part: InteractionPart, - content_type: str | None, - file: Path | None, - part_name: str, - boundary: str | None, -) -> None: - """ - Adds a binary file as the body as a MIME multipart. - - Will use a mime type matcher to match the body. Returns an error if the - interaction or Pact can't be modified (i.e. the mock server for it has - already started) or an error occurs. - - [Rust - `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_multipart_file_v2) - - This function can be called multiple times. In that case, each subsequent - call will be appended to the existing multipart body as a new part. - - Args: - interaction: - Handle to the Interaction. - - part: - The part of the interaction to add the body to (Request or - Response). - - content_type: - The content type of the body. - - file: - Path to the file to add. If `None`, the body will be set to null. - - part_name: - Name for the mime part. - - boundary: - Boundary for the multipart separation. If `None`, a random string - will be used. - """ - result = StringResult( - lib.pactffi_with_multipart_file_v2( - interaction._ref, - part.value, - content_type.encode("utf-8") if content_type else ffi.NULL, - str(file).encode("utf-8") if file else ffi.NULL, - part_name.encode("utf-8"), - boundary.encode("utf-8") if boundary else ffi.NULL, - ), - ) - result.raise_exception() - - -def with_multipart_file( - interaction: InteractionHandle, - part: InteractionPart, - content_type: str, - file: str, - part_name: str, -) -> StringResult: - """ - Adds a binary file as the body as a MIME multipart. - - Will use a mime type matcher to match the body. Returns an error if the - interaction or Pact can't be modified (i.e. the mock server for it has - already started) or an error occurs. - - [Rust - `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_multipart_file) - - * `interaction` - Interaction handle to set the body for. - * `part` - Request or response part. - * `content_type` - Expected content type of the file. - * `file` - path to the example file - * `part_name` - name for the mime part - - This function can be called multiple times. In that case, each subsequent - call will be appended to the existing multipart body as a new part. - - # Safety - - The content type, file path and part name must be valid pointers to UTF-8 - encoded NULL-terminated strings. Passing invalid pointers or pointers to - strings that are not NULL terminated will lead to undefined behaviour. - - # Error Handling - - If the file path is a NULL pointer, it will set the body contents as as an - empty mime-part. If the file path does not point to a valid file, or is not - able to be read, it will return an error result. If the content type is a - null pointer, or can't be parsed, it will return an error result. Returns an - error if the interaction or Pact can't be modified (i.e. the mock server for - it has already started), the interaction is not an HTTP interaction or some - other error occurs. - """ - # This function is intentionally left unimplemented. The - # `with_multipart_file_v2` function should be used instead. - raise NotImplementedError - - -def set_key(interaction: InteractionHandle, key: str | None) -> None: - """ - Sets the key attribute for the interaction. - - [Rust - `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_key) - - Args: - interaction: - Interaction handle to modify. - - key: - Key value. This must be a valid UTF-8 null-terminated string, or - `None` to clear the key. - - Raises: - RuntimeError: - If the key could not be set. - """ - success: bool = lib.pactffi_set_key( - interaction._ref, - key.encode("utf-8") if key else ffi.NULL, - ) - if not success: - msg = f"Failed to set key for {interaction}." - raise RuntimeError(msg) - - -def set_pending(interaction: InteractionHandle, *, pending: bool) -> None: - """ - Mark the interaction as pending. - - [Rust - `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_pending) - - Args: - interaction: - Interaction handle to modify. - - pending: - Boolean value to toggle the pending state of the interaction. - - Raises: - RuntimeError: - If the pending status could not be updated. - """ - success: bool = lib.pactffi_set_pending(interaction._ref, pending) - if not success: - msg = f"Failed to update pending status for {interaction}." - raise RuntimeError(msg) - - -def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> None: - """ - Add a comment to the interaction. - - [Rust - `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_comment) - - Args: - interaction: - Interaction handle to set the comments for. - - key: - Key value - - value: - Comment value. This may be any valid JSON value, or a `None` to - clear the comment. Note that a value that deserialize to a JSON null - will result in a comment being added, with the value being the JSON - null. - - Raises: - RuntimeError: - If the comments could not be updated. - """ - success: bool = lib.pactffi_set_comment( - interaction._ref, - key.encode("utf-8"), - value.encode("utf-8") if value else ffi.NULL, - ) - if not success: - msg = f"Failed to set comment for {interaction}." - raise RuntimeError(msg) - - -def add_text_comment(interaction: InteractionHandle, comment: str) -> None: - """ - Add a text comment to the interaction. - - [Rust - `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_add_text_comment) - - Args: - interaction: - Interaction handle to set the comments for. - - comment: - Comment value. This is a regular string value. - - Raises: - RuntimeError: - If the comment could not be added. - """ - success: bool = lib.pactffi_add_text_comment( - interaction._ref, - comment.encode("utf-8"), - ) - if not success: - msg = f"Failed to add text comment for {interaction}." - raise RuntimeError(msg) - - -def pact_handle_get_async_message_iter(pact: PactHandle) -> PactAsyncMessageIterator: - r""" - Get an iterator over all the asynchronous messages of the Pact. - - The returned iterator needs to be freed with - `pactffi_pact_sync_message_iter_delete`. - - [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) - - # Safety - - The iterator contains a copy of the Pact, so it is always safe to use. - - # Error Handling - - On failure, this function will return a NULL pointer. - - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. - """ - return PactAsyncMessageIterator( - lib.pactffi_pact_handle_get_async_message_iter(pact._ref), - ) - - -def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterator: - r""" - Get an iterator over all the synchronous messages of the Pact. - - The returned iterator needs to be freed with - `pactffi_pact_sync_message_iter_delete`. - - [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) - - # Safety - - The iterator contains a copy of the Pact, so it is always safe to use. - - # Error Handling - - On failure, this function will return a NULL pointer. - - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. - """ - return PactSyncMessageIterator( - lib.pactffi_pact_handle_get_sync_message_iter(pact._ref), - ) - - -def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: - r""" - Get an iterator over all the synchronous HTTP request/response interactions. - - The returned iterator needs to be freed with - `pactffi_pact_sync_http_iter_delete`. - - [Rust - `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) - - # Safety - - The iterator contains a copy of the Pact, so it is always safe to use. - - # Error Handling - - On failure, this function will return a NULL pointer. - - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. - """ - return PactSyncHttpIterator(lib.pactffi_pact_handle_get_sync_http_iter(pact._ref)) - - -def pact_handle_write_file( - pact: PactHandle, - directory: Path | str | None, - *, - overwrite: bool, -) -> None: - """ - External interface to write out the pact file. - - [Rust - `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_write_file) - - This function should be called if all the consumer tests have passed. - - Args: - pact: - Handle to a Pact model. - - directory: - The directory to write the file to. If `None`, the current working - directory is used. - - overwrite: - If `True`, the file will be overwritten with the contents of the - current pact. Otherwise, it will be merged with any existing pact - file. - - Raises: - RuntimeError: - If there was an error writing the pact file. - """ - ret: int = lib.pactffi_pact_handle_write_file( - pact._ref, - str(directory).encode("utf-8") if directory else ffi.NULL, - overwrite, - ) - if ret == 0: - return - if ret == 1: - msg = f"The function panicked while writing {pact} to {directory}." - elif ret == 2: # noqa: PLR2004 - msg = f"The pact file was not able to be written for {pact}." - elif ret == 3: # noqa: PLR2004 - msg = f"The pact for {pact} was not found." - else: - msg = f"Unknown error writing {pact} to {directory}." - raise RuntimeError(msg) - - -def free_pact_handle(pact: PactHandle) -> None: - """ - Delete a Pact handle and free the resources used by it. - - [Rust - `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_free_pact_handle) - - Raises: - RuntimeError: - If the handle could not be freed. - """ - ret: int = lib.pactffi_free_pact_handle(pact._ref) - if ret == 0: - return - if ret == 1: - msg = f"{pact} is not valid or does not refer to a valid Pact." - else: - msg = f"There was an unknown error freeing {pact}." - raise RuntimeError(msg) - - -def verify(args: str) -> int: - """ - External interface to verifier a provider. - - [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verify) - - * `args` - the same as the CLI interface, except newline delimited - - # Errors - - Errors are returned as non-zero numeric values. - - | Error | Description | - |-------|-------------| - | 1 | The verification process failed, see output for errors | - | 2 | A null pointer was received | - | 3 | The method panicked | - | 4 | Invalid arguments were provided to the verification process | - - # Safety - - Exported functions are inherently unsafe. Deal. - """ - raise NotImplementedError - - -def verifier_new_for_application() -> VerifierHandle: - """ - Get a Handle to a newly created verifier. - - By default, verification results will not be published. To enable - publishing, use - [`pactffi_verifier_set_publish_options`][pact.v3.ffi.verifier_set_publish_options] - to set the required values and enable it. - - [Rust - `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_new_for_application) - """ - result: cffi.FFI.CData = lib.pactffi_verifier_new_for_application( - b"pact-python", - __version__.encode("utf-8"), - ) - return VerifierHandle(result) - - -def verifier_shutdown(handle: VerifierHandle) -> None: - """ - Shutdown the verifier and release all resources. - - [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_shutdown) - """ - lib.pactffi_verifier_shutdown(handle._ref) - - -def verifier_set_provider_info( # noqa: PLR0913 - handle: VerifierHandle, - name: str | None, - scheme: str | None, - host: str | None, - port: int | None, - path: str | None, -) -> None: - """ - Set the provider details for the Pact verifier. - - [Rust - `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_provider_info) - - Args: - handle: - The verifier handle to update. - - name: - A user-friendly name to describe the provider. - - scheme: - Determine the scheme to use, typically one of `HTTP` or `HTTPS`. - - host: - The host of the provider. This may be either a hostname to resolve, - or an IP address. - - port: - The port of the provider. - - path: - The path of the provider. - - If any value is `None`, the default value as determined by the underlying - FFI library will be used. - """ - lib.pactffi_verifier_set_provider_info( - handle._ref, - name.encode("utf-8") if name else ffi.NULL, - scheme.encode("utf-8") if scheme else ffi.NULL, - host.encode("utf-8") if host else ffi.NULL, - port, - path.encode("utf-8") if path else ffi.NULL, - ) - - -def verifier_add_provider_transport( - handle: VerifierHandle, - protocol: str | None, - port: int, - path: str | None, - scheme: str | None, -) -> None: - """ - Adds a new transport for the given provider. - - [Rust - `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_provider_transport) - - Args: - handle: - The verifier handle to update. - - protocol: - In this context, the kind of - - port: - The port of the provider. - - path: - The path of the provider. - - scheme: - The scheme to use, typically one of `HTTP` or `HTTPS`. - - If any value is `None`, the default value as determined by the underlying - FFI library will be used. - """ - lib.pactffi_verifier_add_provider_transport( - handle._ref, - protocol.encode("utf-8") if protocol else ffi.NULL, - port, - path.encode("utf-8") if path else ffi.NULL, - scheme.encode("utf-8") if scheme else ffi.NULL, - ) - - -def verifier_set_filter_info( - handle: VerifierHandle, - filter_description: str | None, - filter_state: str | None, - *, - filter_no_state: bool, -) -> None: - """ - Set the filters for the Pact verifier. - - [Rust - `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_filter_info) - - Set filters to narrow down the interactions to verify. - - Args: - handle: - The verifier handle to update. - - filter_description: - A regular expression to filter the interactions by description. - - filter_state: - A regular expression to filter the interactions by state. - - filter_no_state: - If `True`, the option to filter by state will be turned on. - """ - lib.pactffi_verifier_set_filter_info( - handle._ref, - filter_description.encode("utf-8") if filter_description else ffi.NULL, - filter_state.encode("utf-8") if filter_state else ffi.NULL, - filter_no_state, - ) - - -def verifier_set_provider_state( - handle: VerifierHandle, - url: str, - *, - teardown: bool, - body: bool, -) -> None: - """ - Set the provider state URL for the Pact verifier. - - [Rust - `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_provider_state) - - Args: - handle: - The verifier handle to update. - - url: - The URL to use for the provider state. - - teardown: - If teardown state change requests should be made after an - interaction is validated. - - body: - If state change request data should be sent in the body or the - query. - """ - lib.pactffi_verifier_set_provider_state( - handle._ref, - url.encode("utf-8"), - teardown, - body, - ) - - -def verifier_set_verification_options( - handle: VerifierHandle, - *, - disable_ssl_verification: bool, - request_timeout: int, -) -> None: - """ - Set the options used by the verifier when calling the provider. - - [Rust - `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_verification_options) - - Args: - handle: - The verifier handle to update. - - disable_ssl_verification: - If SSL verification should be disabled. - - request_timeout: - The timeout for the request in milliseconds. - - Raises: - RuntimeError: - If the options could not be set. - """ - retval: int = lib.pactffi_verifier_set_verification_options( - handle._ref, - disable_ssl_verification, - request_timeout, - ) - if retval != 0: - msg = f"Failed to set verification options for {handle}." - raise RuntimeError(msg) - - -def verifier_set_coloured_output( - handle: VerifierHandle, - *, - enabled: bool, -) -> None: - """ - Enables or disables coloured output using ANSI escape codes. - - [Rust - `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_coloured_output) - - By default, coloured output is enabled. - - Args: - handle: - The verifier handle to update. - - enabled: - A boolean value to enable or disable coloured output. - - Raises: - RuntimeError: - If the coloured output could not be set. - """ - retval: int = lib.pactffi_verifier_set_coloured_output( - handle._ref, - enabled, - ) - if retval != 0: - msg = f"Failed to set coloured output for {handle}." - raise RuntimeError(msg) - - -def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> None: - """ - Enables or disables if no pacts are found to verify results in an error. - - [Rust - `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) - - Args: - handle: - The verifier handle to update. - - enabled: - If `True`, an error will be raised when no pacts are found to verify. - - Raises: - RuntimeError: - If the no pacts is error setting could not be set. - """ - retval: int = lib.pactffi_verifier_set_no_pacts_is_error( - handle._ref, - enabled, - ) - if retval != 0: - msg = f"Failed to set no pacts is error for {handle}." - raise RuntimeError(msg) - - -def verifier_set_publish_options( - handle: VerifierHandle, - provider_version: str, - build_url: str | None, - provider_tags: list[str] | None, - provider_branch: str | None, -) -> None: - """ - Set the options used when publishing verification results to the Broker. - - [Rust - `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_publish_options) - - Args: - handle: - The verifier handle to update. - - provider_version: - Version of the provider to publish. - - build_url: - URL to the build which ran the verification. - - provider_tags: - Collection of tags for the provider. - - provider_branch: - Name of the branch used for verification. - - Raises: - RuntimeError: - If the publish options could not be set. - """ - retval: int = lib.pactffi_verifier_set_publish_options( - handle._ref, - provider_version.encode("utf-8"), - build_url.encode("utf-8") if build_url else ffi.NULL, - [ffi.new("char[]", t.encode("utf-8")) for t in provider_tags or []], - len(provider_tags or []), - provider_branch.encode("utf-8") if provider_branch else ffi.NULL, - ) - if retval != 0: - msg = f"Failed to set publish options for {handle}." - raise RuntimeError(msg) - - -def verifier_set_consumer_filters( - handle: VerifierHandle, - consumer_filters: Collection[str], -) -> None: - """ - Set the consumer filters for the Pact verifier. - - [Rust - `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_consumer_filters) - """ - lib.pactffi_verifier_set_consumer_filters( - handle._ref, - [ffi.new("char[]", f.encode("utf-8")) for f in consumer_filters], - len(consumer_filters), - ) - - -def verifier_add_custom_header( - handle: VerifierHandle, - header_name: str, - header_value: str, -) -> None: - """ - Adds a custom header to be added to the requests made to the provider. - - [Rust - `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_custom_header) - """ - lib.pactffi_verifier_add_custom_header( - handle._ref, - header_name.encode("utf-8"), - header_value.encode("utf-8"), - ) - - -def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: - """ - Adds a Pact file as a source to verify. - - [Rust - `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_file_source) - """ - lib.pactffi_verifier_add_file_source(handle._ref, file.encode("utf-8")) - - -def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> None: - """ - Adds a Pact directory as a source to verify. - - All pacts from the directory that match the provider name will be verified. - - [Rust - `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_directory_source) - - # Safety - - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. - - """ - lib.pactffi_verifier_add_directory_source(handle._ref, directory.encode("utf-8")) - - -def verifier_url_source( - handle: VerifierHandle, - url: str, - username: str | None, - password: str | None, - token: str | None, -) -> None: - """ - Adds a URL as a source to verify. - - [Rust - `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_url_source) - - Args: - handle: - The verifier handle to update. - - url: - The URL to use as a source for the verifier. - - username: - The username to use when fetching pacts from the URL. - - password: - The password to use when fetching pacts from the URL. - - token: - The token to use when fetching pacts from the URL. This will be used - as a bearer token. It is mutually exclusive with the username and - password. - """ - lib.pactffi_verifier_url_source( - handle._ref, - url.encode("utf-8"), - username.encode("utf-8") if username else ffi.NULL, - password.encode("utf-8") if password else ffi.NULL, - token.encode("utf-8") if token else ffi.NULL, - ) - - -def verifier_broker_source( - handle: VerifierHandle, - url: str, - username: str | None, - password: str | None, - token: str | None, -) -> None: - """ - Adds a Pact broker as a source to verify. - - [Rust - `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_broker_source) - - This will fetch all the pact files from the broker that match the provider - name. - - Args: - handle: - The verifier handle to update. - - url: - The URL to use as a source for the verifier. - - username: - The username to use when fetching pacts from the broker. - - password: - The password to use when fetching pacts from the broker. - - token: - The token to use when fetching pacts from the broker. This will be - used as a bearer token. - """ - lib.pactffi_verifier_broker_source( - handle._ref, - url.encode("utf-8"), - username.encode("utf-8") if username else ffi.NULL, - password.encode("utf-8") if password else ffi.NULL, - token.encode("utf-8") if token else ffi.NULL, - ) - - -def verifier_broker_source_with_selectors( # noqa: PLR0913 - handle: VerifierHandle, - url: str, - username: str | None, - password: str | None, - token: str | None, - enable_pending: int, - include_wip_pacts_since: datetime.date | None, - provider_tags: list[str], - provider_branch: str | None, - consumer_version_selectors: list[str], - consumer_version_tags: list[str], -) -> None: - """ - Adds a Pact broker as a source to verify. - - [Rust - `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) - - This will fetch all the pact files from the broker that match the provider - name and the consumer version selectors (See [Consumer Version - Selectors](https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/)). - - If a username and password is given, then basic authentication will be used - when fetching the pact file. If a token is provided, then bearer token - authentication will be used. - - Args: - handle: - The verifier handle to update. - - url: - The URL to use as a source for the verifier. - - username: - The username to use when fetching pacts from the broker. - - password: - The password to use when fetching pacts from the broker. - - token: - The token to use when fetching pacts from the broker. This will be - used as a bearer token. - - enable_pending: - If pending pacts should be included in the verification process. - - include_wip_pacts_since: - The date to use to filter out WIP pacts. - - provider_tags: - The tags to use to filter the provider pacts. - - provider_branch: - The branch to use to filter the provider pacts. - - consumer_version_selectors: - The consumer version selectors to use to filter the consumer pacts. - This must be passed in as a JSON string. - - consumer_version_tags: - The tags to use to filter the consumer pacts. - """ - ret: int = lib.pactffi_verifier_broker_source_with_selectors( - handle._ref, - url.encode("utf-8"), - username.encode("utf-8") if username else ffi.NULL, - password.encode("utf-8") if password else ffi.NULL, - token.encode("utf-8") if token else ffi.NULL, - enable_pending, - ( - include_wip_pacts_since.isoformat().encode("utf-8") - if include_wip_pacts_since - else ffi.NULL - ), - [ffi.new("char[]", t.encode("utf-8")) for t in provider_tags], - len(provider_tags), - provider_branch.encode("utf-8") if provider_branch else ffi.NULL, - [ffi.new("char[]", s.encode("utf-8")) for s in consumer_version_selectors], - len(consumer_version_selectors), - [ffi.new("char[]", t.encode("utf-8")) for t in consumer_version_tags], - len(consumer_version_tags), - ) - if ret == 0: - return - if ret == -1: - msg = "Invalid version selector JSON." - raise ValueError(msg) - msg = "Unknown error adding broker source with selectors." - raise RuntimeError(msg) - - -def verifier_execute(handle: VerifierHandle) -> None: - """ - Runs the verification. - - (https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_execute) - - Raises: - RuntimeError: - If the verifier could not be executed. - """ - success: int = lib.pactffi_verifier_execute(handle._ref) - if success != 0: - msg = f"Failed to execute verifier for {handle}." - raise RuntimeError(msg) - - -def verifier_cli_args() -> str: - """ - External interface to retrieve the CLI options and arguments. - - This available when calling the CLI interface, returning them as a JSON - string. - - [Rust - `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_cli_args) - - The purpose is to then be able to use in other languages which wrap the FFI - library, to implement the same CLI functionality automatically without - manual maintenance of arguments, help descriptions etc. - - # Example structure - - ```json - { - "options": [ - { - "long": "scheme", - "help": "Provider URI scheme (defaults to http)", - "possible_values": [ - "http", - "https" - ], - "default_value": "http" - "multiple": false, - }, - { - "long": "file", - "short": "f", - "help": "Pact file to verify (can be repeated)", - "multiple": true - }, - { - "long": "user", - "help": "Username to use when fetching pacts from URLS", - "multiple": false, - "env": "PACT_BROKER_USERNAME" - } - ], - "flags": [ - { - "long": "disable-ssl-verification", - "help": "Disables validation of SSL certificates", - "multiple": false - } - ] - } - ``` - - # Safety - - Exported functions are inherently unsafe. - """ - raise NotImplementedError - - -def verifier_logs(handle: VerifierHandle) -> OwnedString: - """ - Extracts the logs for the verification run. - - [Rust - `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_logs) - - This needs the memory buffer log sink to be setup before the verification is - executed. The returned string will need to be freed with the `free_string` - function call to avoid leaking memory. - - Raises: - RuntimeError: - If the logs could not be extracted. - """ - ptr = lib.pactffi_verifier_logs(handle._ref) - if ptr == ffi.NULL: - msg = f"Failed to get logs for {handle}." - raise RuntimeError(msg) - return OwnedString(ptr) - - -def verifier_logs_for_provider(provider_name: str) -> OwnedString: - """ - Extracts the logs for the verification run for the provider name. - - [Rust - `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_logs_for_provider) - - This needs the memory buffer log sink to be setup before the verification is - executed. The returned string will need to be freed with the `free_string` - function call to avoid leaking memory. - - Raises: - RuntimeError: - If the logs could not be extracted. - """ - ptr = lib.pactffi_verifier_logs_for_provider(provider_name.encode("utf-8")) - if ptr == ffi.NULL: - msg = f"Failed to get logs for {provider_name}." - raise RuntimeError(msg) - return OwnedString(ptr) - - -def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: - """ - Extracts the standard output for the verification run. - - [Rust - `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_output) - - Args: - handle: - The verifier handle to update. - - strip_ansi: - This parameter controls ANSI escape codes. Setting it to a non-zero - value will cause the ANSI control codes to be stripped from the - output. - - Raises: - RuntimeError: - If the output could not be extracted. - """ - ptr = lib.pactffi_verifier_output(handle._ref, strip_ansi) - if ptr == ffi.NULL: - msg = f"Failed to get output for {handle}." - raise RuntimeError(msg) - return OwnedString(ptr) - - -def verifier_json(handle: VerifierHandle) -> OwnedString: - """ - Extracts the verification result as a JSON document. - - [Rust - `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_json) - - Raises: - RuntimeError: - If the JSON could not be extracted. - """ - ptr = lib.pactffi_verifier_json(handle._ref) - if ptr == ffi.NULL: - msg = f"Failed to get JSON for {handle}." - raise RuntimeError(msg) - return OwnedString(ptr) - - -def using_plugin( - pact: PactHandle, - plugin_name: str, - plugin_version: str | None, -) -> None: - """ - Add a plugin to be used by the test. - - The plugin needs to be installed correctly for this function to work. - - Note that plugins run as separate processes, so will need to be cleaned up - afterwards by calling [`cleanup_plugins`][pact.v3.ffi.cleanup_plugins] - otherwise you will have plugin processes left running. - - [Rust - `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_using_plugin) - - Args: - pact: - Handle to a Pact model. - - plugin_name: - Name of the plugin to use. - - plugin_version: - Version of the plugin to use. If `None`, the latest version will be - used. - - Raises: - RuntimeError: - If the plugin could not be loaded. - """ - ret: int = lib.pactffi_using_plugin( - pact._ref, - plugin_name.encode("utf-8"), - plugin_version.encode("utf-8") if plugin_version else ffi.NULL, - ) - if ret == 0: - return - if ret == 1: - msg = f"A general panic was caught: {get_error_message()}" - elif ret == 2: # noqa: PLR2004 - msg = f"Failed to load the plugin {plugin_name}." - elif ret == 3: # noqa: PLR2004 - msg = f"The Pact handle {pact} is invalid." - else: - msg = f"There was an unknown error loading the plugin {plugin_name}." - raise RuntimeError(msg) - - -def cleanup_plugins(pact: PactHandle) -> None: - """ - Decrement the access count on any plugins that are loaded for the Pact. - - This will shutdown any plugins that are no longer required (access count is - zero). - - [Rust - `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_cleanup_plugins) - """ - lib.pactffi_cleanup_plugins(pact._ref) - - -def interaction_contents( - interaction: InteractionHandle, - part: InteractionPart, - content_type: str, - contents: str, -) -> None: - """ - Setup the interaction part using a plugin. - - The contents is a JSON string that will be passed on to the plugin to - configure the interaction part. Refer to the plugin documentation on the - format of the JSON contents. - - [Rust - `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_interaction_contents) - - Args: - interaction: - Handle to the interaction to configure. - - part: - The part of the interaction to configure (request or response). It - is ignored for messages. - - content_type: - Mime type of the contents. - - contents: - JSON contents that gets passed to the plugin. - - Raises: - RuntimeError: - If the interaction could not be configured - """ - ret: int = lib.pactffi_interaction_contents( - interaction._ref, - part.value, - content_type.encode("utf-8"), - contents.encode("utf-8"), - ) - if ret == 0: - return - if ret == 1: - msg = f"A general panic was caught: {get_error_message()}" - if ret == 2: # noqa: PLR2004 - msg = "The mock server has already been started." - if ret == 3: # noqa: PLR2004 - msg = f"The interaction handle {interaction} is invalid." - if ret == 4: # noqa: PLR2004 - msg = f"The content type {content_type} is not valid." - if ret == 5: # noqa: PLR2004 - msg = "The content is not valid JSON." - if ret == 6: # noqa: PLR2004 - msg = f"The plugin returned an error: {get_error_message()}" - else: - msg = f"There was an unknown error configuring the interaction: {ret}" - raise RuntimeError(msg) - - -def matches_string_value( - matching_rule: MatchingRule, - expected_value: str, - actual_value: str, - cascaded: int, -) -> OwnedString: - """ - Determines if the string value matches the given matching rule. - - If the value matches OK, will return a NULL pointer. If the value does not - match, will return a error message as a NULL terminated string. The error - message pointer will need to be deleted with the `pactffi_string_delete` - function once it is no longer required. - - [Rust - `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_string_value) - - * matching_rule - pointer to a matching rule - * expected_value - value we expect to get as a NULL terminated string - * actual_value - value to match as a NULL terminated string - * cascaded - if the matching rule has been cascaded from a parent. 0 == - false, 1 == true - - # Safety - - The matching rule pointer must be a valid pointer, and the value parameters - must be valid pointers to a NULL terminated strings. - """ - raise NotImplementedError - - -def matches_u64_value( - matching_rule: MatchingRule, - expected_value: int, - actual_value: int, - cascaded: int, -) -> OwnedString: - """ - Determines if the unsigned integer value matches the given matching rule. - - If the value matches OK, will return a NULL pointer. If the value does not - match, will return a error message as a NULL terminated string. The error - message pointer will need to be deleted with the `pactffi_string_delete` - function once it is no longer required. - - [Rust - `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_u64_value) - - * matching_rule - pointer to a matching rule - * expected_value - value we expect to get - * actual_value - value to match - * cascaded - if the matching rule has been cascaded from a parent. 0 == - false, 1 == true - - # Safety - - The matching rule pointer must be a valid pointer. - """ - raise NotImplementedError - - -def matches_i64_value( - matching_rule: MatchingRule, - expected_value: int, - actual_value: int, - cascaded: int, -) -> OwnedString: - """ - Determines if the signed integer value matches the given matching rule. - - If the value matches OK, will return a NULL pointer. If the value does not - match, will return a error message as a NULL terminated string. The error - message pointer will need to be deleted with the `pactffi_string_delete` - function once it is no longer required. - - [Rust - `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_i64_value) - - * matching_rule - pointer to a matching rule - * expected_value - value we expect to get - * actual_value - value to match - * cascaded - if the matching rule has been cascaded from a parent. 0 == - false, 1 == true - - # Safety - - The matching rule pointer must be a valid pointer. - """ - raise NotImplementedError - - -def matches_f64_value( - matching_rule: MatchingRule, - expected_value: float, - actual_value: float, - cascaded: int, -) -> OwnedString: - """ - Determines if the floating point value matches the given matching rule. - - If the value matches OK, will return a NULL pointer. If the value does not - match, will return a error message as a NULL terminated string. The error - message pointer will need to be deleted with the `pactffi_string_delete` - function once it is no longer required. - - [Rust - `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_f64_value) - - * matching_rule - pointer to a matching rule - * expected_value - value we expect to get - * actual_value - value to match - * cascaded - if the matching rule has been cascaded from a parent. 0 == - false, 1 == true - - # Safety - - The matching rule pointer must be a valid pointer. - """ - raise NotImplementedError - - -def matches_bool_value( - matching_rule: MatchingRule, - expected_value: int, - actual_value: int, - cascaded: int, -) -> OwnedString: - """ - Determines if the boolean value matches the given matching rule. - - If the value matches OK, will return a NULL pointer. If the value does not - match, will return a error message as a NULL terminated string. The error - message pointer will need to be deleted with the `pactffi_string_delete` - function once it is no longer required. - - [Rust - `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_bool_value) - - * matching_rule - pointer to a matching rule - * expected_value - value we expect to get, 0 == false and 1 == true - * actual_value - value to match, 0 == false and 1 == true - * cascaded - if the matching rule has been cascaded from a parent. 0 == - false, 1 == true - - # Safety - - The matching rule pointer must be a valid pointer. - """ - raise NotImplementedError - - -def matches_binary_value( # noqa: PLR0913 - matching_rule: MatchingRule, - expected_value: str, - expected_value_len: int, - actual_value: str, - actual_value_len: int, - cascaded: int, -) -> OwnedString: - """ - Determines if the binary value matches the given matching rule. - - If the value matches OK, will return a NULL pointer. If the value does not - match, will return a error message as a NULL terminated string. The error - message pointer will need to be deleted with the `pactffi_string_delete` - function once it is no longer required. - - [Rust - `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_binary_value) - - * matching_rule - pointer to a matching rule - * expected_value - value we expect to get - * expected_value_len - length of the expected value bytes - * actual_value - value to match - * actual_value_len - length of the actual value bytes - * cascaded - if the matching rule has been cascaded from a parent. 0 == - false, 1 == true - - # Safety - - The matching rule, expected value and actual value pointers must be a valid - pointers. expected_value_len and actual_value_len must contain the number of - bytes that the value pointers point to. Passing invalid lengths can lead to - undefined behaviour. - """ - raise NotImplementedError - - -def matches_json_value( - matching_rule: MatchingRule, - expected_value: str, - actual_value: str, - cascaded: int, -) -> OwnedString: - """ - Determines if the JSON value matches the given matching rule. - - If the value matches OK, will return a NULL pointer. If the value does not - match, will return a error message as a NULL terminated string. The error - message pointer will need to be deleted with the `pactffi_string_delete` - function once it is no longer required. - - [Rust - `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_json_value) - - * matching_rule - pointer to a matching rule - * expected_value - value we expect to get as a NULL terminated string - * actual_value - value to match as a NULL terminated string - * cascaded - if the matching rule has been cascaded from a parent. 0 == - false, 1 == true - - # Safety - - The matching rule pointer must be a valid pointer, and the value parameters - must be valid pointers to a NULL terminated strings. - """ - raise NotImplementedError diff --git a/src/pact/v3/interaction/_async_message_interaction.py b/src/pact/v3/interaction/_async_message_interaction.py index 15621650a..2b1bf5c30 100644 --- a/src/pact/v3/interaction/_async_message_interaction.py +++ b/src/pact/v3/interaction/_async_message_interaction.py @@ -4,7 +4,7 @@ from __future__ import annotations -import pact.v3.ffi +import pact_ffi from pact.v3.interaction._base import Interaction @@ -22,7 +22,7 @@ class AsyncMessageInteraction(Interaction): This class is not yet fully implemented and is not yet usable. """ - def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: + def __init__(self, pact_handle: pact_ffi.PactHandle, description: str) -> None: """ Initialise a new Asynchronous Message Interaction. @@ -40,10 +40,10 @@ def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> Non Pact. """ super().__init__(description) - self.__handle = pact.v3.ffi.new_message_interaction(pact_handle, description) + self.__handle = pact_ffi.new_message_interaction(pact_handle, description) @property - def _handle(self) -> pact.v3.ffi.InteractionHandle: + def _handle(self) -> pact_ffi.InteractionHandle: """ Handle for the Interaction. @@ -53,7 +53,7 @@ def _handle(self) -> pact.v3.ffi.InteractionHandle: return self.__handle @property - def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + def _interaction_part(self) -> pact_ffi.InteractionPart: """ Interaction part. @@ -61,7 +61,7 @@ def _interaction_part(self) -> pact.v3.ffi.InteractionPart: of which part is currently being set. As this is an asynchronous message interaction, this will always - return a [`REQUEST`][pact.v3.ffi.InteractionPart.REQUEST], as there the + return a [`REQUEST`][pact_ffi.InteractionPart.REQUEST], as there the consumer of the message does not send any responses. """ - return pact.v3.ffi.InteractionPart.REQUEST + return pact_ffi.InteractionPart.REQUEST diff --git a/src/pact/v3/interaction/_base.py b/src/pact/v3/interaction/_base.py index 28bdedd70..953999f56 100644 --- a/src/pact/v3/interaction/_base.py +++ b/src/pact/v3/interaction/_base.py @@ -15,7 +15,7 @@ import json from typing import TYPE_CHECKING, Any, Literal, overload -import pact.v3.ffi +import pact_ffi from pact.v3.match.matcher import IntegrationJSONEncoder if TYPE_CHECKING: @@ -83,7 +83,7 @@ def __repr__(self) -> str: @property @abc.abstractmethod - def _handle(self) -> pact.v3.ffi.InteractionHandle: + def _handle(self) -> pact_ffi.InteractionHandle: """ Handle for the Interaction. @@ -93,7 +93,7 @@ def _handle(self) -> pact.v3.ffi.InteractionHandle: @property @abc.abstractmethod - def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + def _interaction_part(self) -> pact_ffi.InteractionPart: """ Interaction part. @@ -104,14 +104,14 @@ def _interaction_part(self) -> pact.v3.ffi.InteractionPart: def _parse_interaction_part( self, part: Literal["Request", "Response", None], - ) -> pact.v3.ffi.InteractionPart: + ) -> pact_ffi.InteractionPart: """ Convert the input into an InteractionPart. """ if part == "Request": - return pact.v3.ffi.InteractionPart.REQUEST + return pact_ffi.InteractionPart.REQUEST if part == "Response": - return pact.v3.ffi.InteractionPart.RESPONSE + return pact_ffi.InteractionPart.RESPONSE if part is None: return self._interaction_part msg = f"Invalid part: {part}" @@ -229,18 +229,18 @@ def given( If the combination of arguments is invalid or inconsistent. """ if name is not None and value is not None and parameters is None: - pact.v3.ffi.given_with_param(self._handle, state, name, value) + pact_ffi.given_with_param(self._handle, state, name, value) elif name is None and value is None and parameters is not None: if isinstance(parameters, dict): - pact.v3.ffi.given_with_params( + pact_ffi.given_with_params( self._handle, state, json.dumps(parameters), ) else: - pact.v3.ffi.given_with_params(self._handle, state, parameters) + pact_ffi.given_with_params(self._handle, state, parameters) elif name is None and value is None and parameters is None: - pact.v3.ffi.given(self._handle, state) + pact_ffi.given(self._handle, state) else: msg = "Invalid combination of arguments." raise ValueError(msg) @@ -274,7 +274,7 @@ def with_body( else: body_str = json.dumps(body, cls=IntegrationJSONEncoder) - pact.v3.ffi.with_body( + pact_ffi.with_body( self._handle, self._parse_interaction_part(part), content_type, @@ -308,7 +308,7 @@ def with_binary_body( body: Body of the request. """ - pact.v3.ffi.with_binary_body( + pact_ffi.with_binary_body( self._handle, self._parse_interaction_part(part), content_type, @@ -368,14 +368,14 @@ def with_metadata( """ part = self._parse_interaction_part(__part) for k, v in (__metadata or {}).items(): - pact.v3.ffi.with_metadata( + pact_ffi.with_metadata( self._handle, k, v, part, ) for k, v in kwargs.items(): - pact.v3.ffi.with_metadata( + pact_ffi.with_metadata( self._handle, k, v, @@ -396,7 +396,7 @@ def with_multipart_file( The content type of the body will be set to a MIME multipart message. """ - pact.v3.ffi.with_multipart_file_v2( + pact_ffi.with_multipart_file_v2( self._handle, self._parse_interaction_part(part), content_type, @@ -413,7 +413,7 @@ def set_key(self, key: str | None) -> Self: This is used by V4 interactions to set the key of the interaction, which can subsequently used to reference the interaction. """ - pact.v3.ffi.set_key(self._handle, key) + pact_ffi.set_key(self._handle, key) return self def set_pending(self, *, pending: bool) -> Self: @@ -423,7 +423,7 @@ def set_pending(self, *, pending: bool) -> Self: This is used by V4 interactions to mark the interaction as pending, in which case the provider is not expected to honour the interaction. """ - pact.v3.ffi.set_pending(self._handle, pending=pending) + pact_ffi.set_pending(self._handle, pending=pending) return self def set_comment(self, key: str, value: Any | None) -> Self: # noqa: ANN401 @@ -449,9 +449,9 @@ def set_comment(self, key: str, value: Any | None) -> Self: # noqa: ANN401 particular, the `text` key is used by `add_text_comment`. """ if isinstance(value, str) or value is None: - pact.v3.ffi.set_comment(self._handle, key, value) + pact_ffi.set_comment(self._handle, key, value) else: - pact.v3.ffi.set_comment(self._handle, key, json.dumps(value)) + pact_ffi.set_comment(self._handle, key, json.dumps(value)) return self def add_text_comment(self, comment: str) -> Self: @@ -472,7 +472,7 @@ def add_text_comment(self, comment: str) -> Self: introduced by [`set_comment`][pact.v3.interaction.Interaction.set_comment]. """ - pact.v3.ffi.add_text_comment(self._handle, comment) + pact_ffi.add_text_comment(self._handle, comment) return self def test_name( @@ -488,7 +488,7 @@ def test_name( name: Name of the test. """ - pact.v3.ffi.interaction_test_name(self._handle, name) + pact_ffi.interaction_test_name(self._handle, name) return self def with_plugin_contents( @@ -519,7 +519,7 @@ def with_plugin_contents( if isinstance(contents, dict): contents = json.dumps(contents) - pact.v3.ffi.interaction_contents( + pact_ffi.interaction_contents( self._handle, self._parse_interaction_part(part), content_type, @@ -553,7 +553,7 @@ def with_matching_rules( if isinstance(rules, dict): rules = json.dumps(rules) - pact.v3.ffi.with_matching_rules( + pact_ffi.with_matching_rules( self._handle, self._parse_interaction_part(part), rules, @@ -586,7 +586,7 @@ def with_generators( if isinstance(generators, dict): generators = json.dumps(generators) - pact.v3.ffi.with_generators( + pact_ffi.with_generators( self._handle, self._parse_interaction_part(part), generators, diff --git a/src/pact/v3/interaction/_http_interaction.py b/src/pact/v3/interaction/_http_interaction.py index 38e4aba52..85680614f 100644 --- a/src/pact/v3/interaction/_http_interaction.py +++ b/src/pact/v3/interaction/_http_interaction.py @@ -8,7 +8,7 @@ from collections import defaultdict from typing import TYPE_CHECKING, Any, Literal -import pact.v3.ffi +import pact_ffi from pact.v3.interaction._base import Interaction from pact.v3.match import Matcher from pact.v3.match.matcher import IntegrationJSONEncoder @@ -61,7 +61,7 @@ class HttpInteraction(Interaction): ``` """ - def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: + def __init__(self, pact_handle: pact_ffi.PactHandle, description: str) -> None: """ Initialise a new HTTP Interaction. @@ -71,16 +71,16 @@ def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> Non [`Pact`][pact.v3.Pact] instance. """ super().__init__(description) - self.__handle = pact.v3.ffi.new_interaction(pact_handle, description) - self.__interaction_part = pact.v3.ffi.InteractionPart.REQUEST + self.__handle = pact_ffi.new_interaction(pact_handle, description) + self.__interaction_part = pact_ffi.InteractionPart.REQUEST self._request_indices: dict[ - tuple[pact.v3.ffi.InteractionPart, str], + tuple[pact_ffi.InteractionPart, str], int, ] = defaultdict(int) self._parameter_indices: dict[str, int] = defaultdict(int) @property - def _handle(self) -> pact.v3.ffi.InteractionHandle: + def _handle(self) -> pact_ffi.InteractionHandle: """ Handle for the Interaction. @@ -90,7 +90,7 @@ def _handle(self) -> pact.v3.ffi.InteractionHandle: return self.__handle @property - def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + def _interaction_part(self) -> pact_ffi.InteractionPart: """ Interaction part. @@ -115,7 +115,7 @@ def with_request(self, method: str, path: str | Matcher[Any]) -> Self: path_str = json.dumps(path, cls=IntegrationJSONEncoder) else: path_str = path - pact.v3.ffi.with_request(self._handle, method, path_str) + pact_ffi.with_request(self._handle, method, path_str) return self def with_header( @@ -221,7 +221,7 @@ def with_header( value_str: str = json.dumps(value, cls=IntegrationJSONEncoder) else: value_str = value - pact.v3.ffi.with_header_v2( + pact_ffi.with_header_v2( self._handle, interaction_part, name, @@ -313,7 +313,7 @@ def set_header( [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] method has been called. """ - pact.v3.ffi.set_header( + pact_ffi.set_header( self._handle, self._parse_interaction_part(part), name, @@ -423,7 +423,7 @@ def with_query_parameter( value_str: str = json.dumps(value, cls=IntegrationJSONEncoder) else: value_str = value - pact.v3.ffi.with_query_parameter_v2( + pact_ffi.with_query_parameter_v2( self._handle, name, index, @@ -468,6 +468,6 @@ def will_respond_with(self, status: int) -> Self: status: Status for the response. """ - pact.v3.ffi.response_status(self._handle, status) - self.__interaction_part = pact.v3.ffi.InteractionPart.RESPONSE + pact_ffi.response_status(self._handle, status) + self.__interaction_part = pact_ffi.InteractionPart.RESPONSE return self diff --git a/src/pact/v3/interaction/_sync_message_interaction.py b/src/pact/v3/interaction/_sync_message_interaction.py index 7836fbe41..2946bff6d 100644 --- a/src/pact/v3/interaction/_sync_message_interaction.py +++ b/src/pact/v3/interaction/_sync_message_interaction.py @@ -6,7 +6,7 @@ from typing_extensions import Self -import pact.v3.ffi +import pact_ffi from pact.v3.interaction._base import Interaction @@ -24,7 +24,7 @@ class SyncMessageInteraction(Interaction): This class is not yet fully implemented and is not yet usable. """ - def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> None: + def __init__(self, pact_handle: pact_ffi.PactHandle, description: str) -> None: """ Initialise a new Synchronous Message Interaction. @@ -42,14 +42,14 @@ def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> Non Pact. """ super().__init__(description) - self.__handle = pact.v3.ffi.new_sync_message_interaction( + self.__handle = pact_ffi.new_sync_message_interaction( pact_handle, description, ) - self.__interaction_part = pact.v3.ffi.InteractionPart.REQUEST + self.__interaction_part = pact_ffi.InteractionPart.REQUEST @property - def _handle(self) -> pact.v3.ffi.InteractionHandle: + def _handle(self) -> pact_ffi.InteractionHandle: """ Handle for the Interaction. @@ -59,7 +59,7 @@ def _handle(self) -> pact.v3.ffi.InteractionHandle: return self.__handle @property - def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + def _interaction_part(self) -> pact_ffi.InteractionPart: return self.__interaction_part def will_respond_with(self) -> Self: @@ -92,5 +92,5 @@ def will_respond_with(self) -> Self: The current instance of the interaction. """ - self.__interaction_part = pact.v3.ffi.InteractionPart.RESPONSE + self.__interaction_part = pact_ffi.InteractionPart.RESPONSE return self diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index 6f9917f33..19b3cb854 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -74,7 +74,7 @@ from yarl import URL -import pact.v3.ffi +import pact_ffi from pact.v3._util import find_free_port from pact.v3.error import ( InteractionVerificationError, @@ -144,7 +144,7 @@ def __init__( self._consumer = consumer self._provider = provider self._interactions: set[Interaction] = set() - self._handle: pact.v3.ffi.PactHandle = pact.v3.ffi.new_pact( + self._handle: pact_ffi.PactHandle = pact_ffi.new_pact( consumer, provider, ) @@ -184,15 +184,15 @@ def provider(self) -> str: return self._provider @property - def specification(self) -> pact.v3.ffi.PactSpecification: + def specification(self) -> pact_ffi.PactSpecification: """ Pact specification version. """ - return pact.v3.ffi.handle_get_pact_spec_version(self._handle) + return pact_ffi.handle_get_pact_spec_version(self._handle) def with_specification( self, - version: str | pact.v3.ffi.PactSpecification, + version: str | pact_ffi.PactSpecification, ) -> Self: """ Set the Pact specification version. @@ -203,14 +203,14 @@ def with_specification( Args: version: Pact specification version. The can be either a string or a - [`PactSpecification`][pact.v3.ffi.PactSpecification] instance. + [`PactSpecification`][pact_ffi.PactSpecification] instance. The version string is case insensitive and has an optional `v` prefix. """ if isinstance(version, str): - version = pact.v3.ffi.PactSpecification.from_str(version) - pact.v3.ffi.with_specification(self._handle, version) + version = pact_ffi.PactSpecification.from_str(version) + pact_ffi.with_specification(self._handle, version) return self def using_plugin(self, name: str, version: str | None = None) -> Self: @@ -226,7 +226,7 @@ def using_plugin(self, name: str, version: str | None = None) -> Self: version: Version of the plugin. This is optional and can be `None`. """ - pact.v3.ffi.using_plugin(self._handle, name, version) + pact_ffi.using_plugin(self._handle, name, version) return self def with_metadata( @@ -249,7 +249,7 @@ def with_metadata( Key-value pairs of metadata to add to the Pact. """ for k, v in metadata.items(): - pact.v3.ffi.with_pact_metadata(self._handle, namespace, k, v) + pact_ffi.with_pact_metadata(self._handle, namespace, k, v) return self @overload @@ -363,27 +363,27 @@ def serve( # noqa: PLR0913 def interactions( self, kind: Literal["HTTP"], - ) -> Generator[pact.v3.ffi.SynchronousHttp, None, None]: ... + ) -> Generator[pact_ffi.SynchronousHttp, None, None]: ... @overload def interactions( self, kind: Literal["Sync"], - ) -> Generator[pact.v3.ffi.SynchronousMessage, None, None]: ... + ) -> Generator[pact_ffi.SynchronousMessage, None, None]: ... @overload def interactions( self, kind: Literal["Async"], - ) -> Generator[pact.v3.ffi.AsynchronousMessage, None, None]: ... + ) -> Generator[pact_ffi.AsynchronousMessage, None, None]: ... def interactions( self, kind: Literal["HTTP", "Sync", "Async"] = "HTTP", ) -> ( - Generator[pact.v3.ffi.SynchronousHttp, None, None] - | Generator[pact.v3.ffi.SynchronousMessage, None, None] - | Generator[pact.v3.ffi.AsynchronousMessage, None, None] + Generator[pact_ffi.SynchronousHttp, None, None] + | Generator[pact_ffi.SynchronousMessage, None, None] + | Generator[pact_ffi.AsynchronousMessage, None, None] ): """ Return an iterator over the Pact's interactions. @@ -394,11 +394,11 @@ def interactions( # TODO: Add an iterator for `All` interactions. # https://github.com/pact-foundation/pact-python/issues/451 if kind == "HTTP": - yield from pact.v3.ffi.pact_handle_get_sync_http_iter(self._handle) + yield from pact_ffi.pact_handle_get_sync_http_iter(self._handle) elif kind == "Sync": - yield from pact.v3.ffi.pact_handle_get_sync_message_iter(self._handle) + yield from pact_ffi.pact_handle_get_sync_message_iter(self._handle) elif kind == "Async": - yield from pact.v3.ffi.pact_handle_get_async_message_iter(self._handle) + yield from pact_ffi.pact_handle_get_async_message_iter(self._handle) else: msg = f"Unknown interaction type: {kind}" raise ValueError(msg) @@ -463,10 +463,10 @@ def verify( """ errors: list[InteractionVerificationError] = [] for message in self.interactions(kind): - request: pact.v3.ffi.MessageContents | None = None - if isinstance(message, pact.v3.ffi.SynchronousMessage): + request: pact_ffi.MessageContents | None = None + if isinstance(message, pact_ffi.SynchronousMessage): request = message.request_contents - elif isinstance(message, pact.v3.ffi.AsynchronousMessage): + elif isinstance(message, pact_ffi.AsynchronousMessage): request = message.contents else: msg = f"Unknown message type: {type(message).__name__}" @@ -519,7 +519,7 @@ def write_file( """ if directory is None: directory = Path.cwd() - pact.v3.ffi.pact_handle_write_file( + pact_ffi.pact_handle_write_file( self._handle, directory, overwrite=overwrite, @@ -564,7 +564,7 @@ class PactServer: def __init__( # noqa: PLR0913 self, - pact_handle: pact.v3.ffi.PactHandle, + pact_handle: pact_ffi.PactHandle, host: str = "localhost", port: int | None = None, transport: str = "HTTP", @@ -607,7 +607,7 @@ def __init__( # noqa: PLR0913 self._transport = transport self._transport_config = transport_config self._pact_handle = pact_handle - self._handle: None | pact.v3.ffi.PactServerHandle = None + self._handle: None | pact_ffi.PactServerHandle = None self._raises = raises self._verbose = verbose @@ -658,7 +658,7 @@ def matched(self) -> bool: if not self._handle: msg = "The server is not running." raise RuntimeError(msg) - return pact.v3.ffi.mock_server_matched(self._handle) + return pact_ffi.mock_server_matched(self._handle) @property def mismatches(self) -> list[Mismatch]: @@ -678,7 +678,7 @@ def mismatches(self) -> list[Mismatch]: return list( map( Mismatch.from_dict, - pact.v3.ffi.mock_server_mismatches(self._handle), + pact_ffi.mock_server_mismatches(self._handle), ) ) @@ -700,7 +700,7 @@ def logs(self) -> str | None: raise RuntimeError(msg) try: - return pact.v3.ffi.mock_server_logs(self._handle) + return pact_ffi.mock_server_logs(self._handle) except RuntimeError: return None @@ -733,7 +733,7 @@ def __enter__(self) -> Self: Once the server is running, it is generally no possible to make modifications to the underlying Pact. """ - self._handle = pact.v3.ffi.create_mock_server_for_transport( + self._handle = pact_ffi.create_mock_server_for_transport( self._pact_handle, self._host, self._port, @@ -812,7 +812,7 @@ def write_file( msg = f"{directory} is not a directory" raise ValueError(msg) - pact.v3.ffi.write_pact_file( + pact_ffi.write_pact_file( self._handle, str(directory), overwrite=overwrite, diff --git a/src/pact/v3/verifier.py b/src/pact/v3/verifier.py index 185aac767..9d4fb69b7 100644 --- a/src/pact/v3/verifier.py +++ b/src/pact/v3/verifier.py @@ -84,7 +84,7 @@ from typing_extensions import Self from yarl import URL -import pact.v3.ffi +import pact_ffi from pact.v3._server import MessageProducer, StateCallback from pact.v3._util import apply_args from pact.v3.types import Message, MessageProducerArgs, StateHandlerArgs @@ -165,7 +165,7 @@ def __init__(self, name: str, host: str | None = None) -> None: """ self._name = name self._host = host or "localhost" - self._handle = pact.v3.ffi.verifier_new_for_application() + self._handle = pact_ffi.verifier_new_for_application() # In order to provide a fluent interface, we remember some options which # are set using the same FFI method. In particular, we remember @@ -460,7 +460,7 @@ def filter( "no_state": no_state, }, ) - pact.v3.ffi.verifier_set_filter_info( + pact_ffi.verifier_set_filter_info( self._handle, description, state, @@ -613,7 +613,7 @@ def _state_handler_url( "body": body, }, ) - pact.v3.ffi.verifier_set_provider_state( + pact_ffi.verifier_set_provider_state( self._handle, str(handler), teardown=teardown, @@ -670,7 +670,7 @@ def _handler( ) self._state_handler = StateCallback(_handler) - pact.v3.ffi.verifier_set_provider_state( + pact_ffi.verifier_set_provider_state( self._handle, self._state_handler.url, teardown=teardown, @@ -725,7 +725,7 @@ def _handler( ) self._state_handler = StateCallback(_handler) - pact.v3.ffi.verifier_set_provider_state( + pact_ffi.verifier_set_provider_state( self._handle, self._state_handler.url, teardown=teardown, @@ -739,7 +739,7 @@ def disable_ssl_verification(self) -> Self: Disable SSL verification. """ self._disable_ssl_verification = True - pact.v3.ffi.verifier_set_verification_options( + pact_ffi.verifier_set_verification_options( self._handle, disable_ssl_verification=self._disable_ssl_verification, request_timeout=self._request_timeout, @@ -759,7 +759,7 @@ def set_request_timeout(self, timeout: int) -> Self: raise ValueError(msg) self._request_timeout = timeout - pact.v3.ffi.verifier_set_verification_options( + pact_ffi.verifier_set_verification_options( self._handle, disable_ssl_verification=self._disable_ssl_verification, request_timeout=self._request_timeout, @@ -770,7 +770,7 @@ def set_coloured_output(self, *, enabled: bool = True) -> Self: """ Toggle coloured output. """ - pact.v3.ffi.verifier_set_coloured_output(self._handle, enabled=enabled) + pact_ffi.verifier_set_coloured_output(self._handle, enabled=enabled) return self def set_error_on_empty_pact(self, *, enabled: bool = True) -> Self: @@ -781,7 +781,7 @@ def set_error_on_empty_pact(self, *, enabled: bool = True) -> Self: return an error. If disabled, a Pact file with no interactions will be ignored. """ - pact.v3.ffi.verifier_set_no_pacts_is_error(self._handle, enabled=enabled) + pact_ffi.verifier_set_no_pacts_is_error(self._handle, enabled=enabled) return self def set_publish_options( @@ -807,7 +807,7 @@ def set_publish_options( branch: Name of the branch used for verification. """ - pact.v3.ffi.verifier_set_publish_options( + pact_ffi.verifier_set_publish_options( self._handle, version, url, @@ -824,7 +824,7 @@ def filter_consumers(self, *filters: str) -> Self: filters: Filters to apply to the consumers. """ - pact.v3.ffi.verifier_set_consumer_filters(self._handle, filters) + pact_ffi.verifier_set_consumer_filters(self._handle, filters) return self def add_custom_header(self, name: str, value: str) -> Self: @@ -840,7 +840,7 @@ def add_custom_header(self, name: str, value: str) -> Self: value: The value of the header. """ - pact.v3.ffi.verifier_add_custom_header(self._handle, name, value) + pact_ffi.verifier_add_custom_header(self._handle, name, value) return self def add_custom_headers( @@ -967,10 +967,10 @@ def _add_source_local(self, source: str | Path) -> Self: """ source = Path(source) if source.is_dir(): - pact.v3.ffi.verifier_add_directory_source(self._handle, str(source)) + pact_ffi.verifier_add_directory_source(self._handle, str(source)) return self if source.is_file(): - pact.v3.ffi.verifier_add_file_source(self._handle, str(source)) + pact_ffi.verifier_add_file_source(self._handle, str(source)) return self msg = f"Invalid source: {source}" raise ValueError(msg) @@ -1024,7 +1024,7 @@ def _add_source_remote( msg = "Cannot specify both `token` and `username`/`password`" raise ValueError(msg) - pact.v3.ffi.verifier_url_source( + pact_ffi.verifier_url_source( self._handle, str(url.with_user(None).with_password(None)), username, @@ -1129,7 +1129,7 @@ def broker_source( token, ) - self._broker_source_hook = lambda: pact.v3.ffi.verifier_broker_source( + self._broker_source_hook = lambda: pact_ffi.verifier_broker_source( self._handle, str(url.with_user(None).with_password(None)), username, @@ -1152,7 +1152,7 @@ def verify(self) -> Self: first, *rest = self._transports - pact.v3.ffi.verifier_set_provider_info( + pact_ffi.verifier_set_provider_info( self._handle, self._name, first["scheme"], @@ -1162,7 +1162,7 @@ def verify(self) -> Self: ) for transport in rest: - pact.v3.ffi.verifier_add_provider_transport( + pact_ffi.verifier_add_provider_transport( self._handle, transport["transport"], transport["port"] or 0, @@ -1174,7 +1174,7 @@ def verify(self) -> Self: self._broker_source_hook() with self._message_producer, self._state_handler: - pact.v3.ffi.verifier_execute(self._handle) + pact_ffi.verifier_execute(self._handle) logger.debug("Verifier executed") return self @@ -1184,27 +1184,27 @@ def logs(self) -> str: """ Get the logs. """ - return pact.v3.ffi.verifier_logs(self._handle) + return pact_ffi.verifier_logs(self._handle) @classmethod def logs_for_provider(cls, provider: str) -> str: """ Get the logs for a provider. """ - return pact.v3.ffi.verifier_logs_for_provider(provider) + return pact_ffi.verifier_logs_for_provider(provider) def output(self, *, strip_ansi: bool = False) -> str: """ Get the output. """ - return pact.v3.ffi.verifier_output(self._handle, strip_ansi=strip_ansi) + return pact_ffi.verifier_output(self._handle, strip_ansi=strip_ansi) @property def results(self) -> dict[str, Any]: """ Get the results. """ - return json.loads(pact.v3.ffi.verifier_json(self._handle)) + return json.loads(pact_ffi.verifier_json(self._handle)) class BrokerSelectorBuilder: @@ -1322,7 +1322,7 @@ def build(self) -> Verifier: The Verifier instance with the broker source added. """ self._verifier._broker_source_hook = ( # noqa: SLF001 - lambda: pact.v3.ffi.verifier_broker_source_with_selectors( + lambda: pact_ffi.verifier_broker_source_with_selectors( self._verifier._handle, # noqa: SLF001 self._url, self._username, diff --git a/tests/v3/conftest.py b/tests/v3/conftest.py index 4ee15b215..c6cd0e818 100644 --- a/tests/v3/conftest.py +++ b/tests/v3/conftest.py @@ -7,7 +7,7 @@ import pytest -from pact.v3 import ffi +import pact_ffi @pytest.fixture(scope="session", autouse=True) @@ -15,4 +15,4 @@ def _setup_pact_logging() -> None: """ Set up logging for the pact package. """ - ffi.log_to_stderr("INFO") + pact_ffi.log_to_stderr("INFO") diff --git a/tests/v3/test_ffi.py b/tests/v3/test_ffi.py deleted file mode 100644 index 4886d0ccd..000000000 --- a/tests/v3/test_ffi.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Tests of the FFI module. - -These tests are intended to ensure that the FFI module is working correctly. -They are not intended to test the Pact API itself, as that is handled by the -client library. -""" - -import re - -import pytest - -from pact.v3 import ffi - - -def test_version() -> None: - assert isinstance(ffi.version(), str) - assert len(ffi.version()) > 0 - assert ffi.version().count(".") == 2 - - -def test_string_result_ok() -> None: - result = ffi.StringResult(ffi.lib.pactffi_generate_datetime_string(b"yyyy")) - assert result.is_ok - assert not result.is_failed - assert re.match(r"^\d{4}$", result.text) - assert str(result) == result.text - assert repr(result) == f"" - result.raise_exception() - - -def test_string_result_failed() -> None: - result = ffi.StringResult(ffi.lib.pactffi_generate_datetime_string(b"t")) - assert not result.is_ok - assert result.is_failed - assert result.text.startswith("Error parsing") - with pytest.raises(RuntimeError): - result.raise_exception() - - -def test_datetime_valid() -> None: - ffi.validate_datetime("2023-01-01", "yyyy-MM-dd") - - -def test_datetime_invalid() -> None: - with pytest.raises(ValueError, match=r"Invalid datetime value.*"): - ffi.validate_datetime("01/01/2023", "yyyy-MM-dd") - - -def test_get_error_message() -> None: - # The first bit makes sure that an error is generated. - invalid_utf8 = b"\xc3\x28" - ret: int = ffi.lib.pactffi_validate_datetime(invalid_utf8, invalid_utf8) - assert ret == 2 - assert ffi.get_error_message() == "error parsing value as UTF-8" - - -def test_owned_string() -> None: - string = ffi.get_tls_ca_certificate() - assert isinstance(string, str) - assert len(string) > 0 - assert str(string) == string - assert repr(string).startswith("") - assert string.startswith("-----BEGIN CERTIFICATE-----") - assert string.endswith( - ( - "-----END CERTIFICATE-----\n", - "-----END CERTIFICATE-----\r\n", - ), - ) diff --git a/tests/v3/test_pact.py b/tests/v3/test_pact.py index c40de3188..3f31ae409 100644 --- a/tests/v3/test_pact.py +++ b/tests/v3/test_pact.py @@ -10,7 +10,7 @@ import pytest from pact.v3 import Pact -from pact.v3.ffi import PactSpecification +from pact_ffi import PactSpecification if TYPE_CHECKING: from pathlib import Path From a0d3e3f589d87c89ef7efe3cbdd57f38c50d89ec Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 30 Jul 2025 10:57:42 +1000 Subject: [PATCH 0890/1376] chore(ci): fix core package build Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 163 +++++------------------------------- 1 file changed, 21 insertions(+), 142 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e2304e04..88991c4b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: - STABLE_PYTHON_VERSION: '3.13' + STABLE_PYTHON_VERSION: '39' HATCH_VERBOSE: '1' FORCE_COLOR: '1' CIBW_BUILD_FRONTEND: build @@ -34,17 +34,15 @@ jobs: runs-on: ubuntu-latest needs: - - build-sdist - - build-x86_64 - - build-arm64 + - build steps: - name: Failed run: exit 1 if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') - build-sdist: - name: Build source distribution + build: + name: Build source distribution and wheel runs-on: ubuntu-latest @@ -65,140 +63,22 @@ jobs: - name: Install hatch run: uv tool install hatch - - name: Create source distribution - run: | - hatch build --target sdist + - name: Create source distribution and wheel + run: hatch build - name: Upload sdist uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels-sdist - path: ./dist/*.tar.* + path: ./dist/*.tar* if-no-files-found: error compression-level: 0 - build-x86_64: - name: Build wheels on ${{ matrix.os }} (x86, 64-bit) - - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - archs: x86_64 - - os: macos-13 # macOS 13 is the latest on x86_64 - archs: x86_64 - - os: windows-latest - archs: AMD64 - - steps: - - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - - - name: Cache pip packages - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ~/.cache/pip - key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} - restore-keys: | - ${{ github.workflow }}-pip-${{ runner.os }} - ${{ github.workflow }}-pip - ${{ github.workflow }} - - - name: Filter targets - id: cibw-filter - shell: bash - # Building all wheels on PRs is too slow, so we filter them to target - # the latest stable version of Python. - run: | - if [[ "${{ github.event_name}}" == "pull_request" ]] ; then - echo "build=cp${STABLE_PYTHON_VERSION/./}-*" >> "$GITHUB_OUTPUT" - else - echo "build=*" >> "$GITHUB_OUTPUT" - fi - - - name: Set macOS deployment target - if: startsWith(matrix.os, 'macos-') - run: | - echo "MACOSX_DEPLOYMENT_TARGET=10.12" >> "$GITHUB_ENV" - - - name: Create wheels - uses: pypa/cibuildwheel@faf86a6ed7efa889faf6996aa23820831055001a # v2.23.3 - env: - CIBW_ARCHS: ${{ matrix.archs }} - CIBW_BUILD: ${{ steps.cibw-filter.outputs.build }} - - - name: Upload wheels + - name: Upload wheel uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: wheels-${{ matrix.os }}-${{ matrix.archs }} - path: ./wheelhouse/*.whl - if-no-files-found: error - compression-level: 0 - - build-arm64: - name: Build wheels on ${{ matrix.os }} (arm64) - - # As this requires emulation, it's not worth running on PRs or main - if: >- - github.event_name == 'push' && - startsWith(github.event.ref, 'refs/tags/pact-python/') - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - archs: aarch64 - build: manylinux - - os: ubuntu-latest - archs: aarch64 - build: musllinux - - os: macos-latest - archs: arm64 - build: '' - # TODO: Re-enable once the issues with Windows ARM64 are resolved.exclude: - # See: pypa/cibuildwheel#1942 - # - os: windows-latest - # archs: ARM64 - # build: "" - - steps: - - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - - - name: Cache pip packages - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ~/.cache/pip - key: ${{ github.workflow }}-pip-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} - restore-keys: | - ${{ github.workflow }}-pip-${{ runner.os }} - ${{ github.workflow }}-pip - ${{ github.workflow }} - - - name: Set up QEMU - if: startsWith(matrix.os, 'ubuntu-') - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - with: - platforms: arm64 - - - name: Create wheels - uses: pypa/cibuildwheel@faf86a6ed7efa889faf6996aa23820831055001a # v2.23.3 - env: - CIBW_ARCHS: ${{ matrix.archs }} - CIBW_BUILD: ${{ matrix.build == '' && '*' || format('*{0}*', matrix.build) }} - - - name: Upload wheels - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: wheels-${{ matrix.os }}-${{ matrix.archs }}-${{ matrix.build }} - path: ./wheelhouse/*.whl + name: wheels-whl + path: ./dist/*.whl if-no-files-found: error compression-level: 0 @@ -214,12 +94,11 @@ jobs: url: https://pypi.org/p/pact-python needs: - - build-sdist - - build-x86_64 - - build-arm64 + - build permissions: - contents: read + # Required for creating the release + contents: write # Required for trusted publishing id-token: write @@ -234,12 +113,6 @@ jobs: with: tool: git-cliff,typos - - name: Download wheels and sdist - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - path: wheels - merge-multiple: true - - name: Update changelog run: git cliff --verbose env: @@ -257,11 +130,17 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + - name: Download wheels and sdist + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + path: wheelhouse + merge-multiple: true + - name: Generate release id: release uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 with: - files: wheels/* + files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md draft: false prerelease: false @@ -271,7 +150,7 @@ jobs: uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 with: skip-existing: true - packages-dir: wheels + packages-dir: wheelhouse - name: Create PR for changelog update uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 From 0556696eb7573c063bd6b0977ec9c4801e97e5c9 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 30 Jul 2025 11:34:12 +1000 Subject: [PATCH 0891/1376] docs(ffi): fix old references to pact.v3.ffi Signed-off-by: JP-Ellis --- pact-python-ffi/src/pact_ffi/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index 05a6a963b..e5e761f35 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -2155,7 +2155,7 @@ def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: Args: level_filter: The level of logs to filter to. If a string is given, it must match - one of the [`LevelFilter`][pact.v3.ffi.LevelFilter] values (case + one of the [`LevelFilter`][pact_ffi.LevelFilter] values (case insensitive). Raises: @@ -5854,7 +5854,7 @@ def with_metadata( # Note - For HTTP interactions, use [`with_header_v2`][pact.v3.ffi.with_header_v2] + For HTTP interactions, use [`with_header_v2`][pact_ffi.with_header_v2] instead. This function will not have any effect on HTTP interactions and returns `false`. @@ -5864,7 +5864,7 @@ def with_metadata( set on all response messages. This also requires for responses to have been defined in the interaction. - The [`with_body`][pact.v3.ffi.with_body] will also contribute to the + The [`with_body`][pact_ffi.with_body] will also contribute to the metadata of the message (both sync and async) by setting the key `contentType` with the content type of the message. @@ -6227,9 +6227,9 @@ def with_binary_file( !!! warning This function is deprecated. Use - [`with_binary_body`][pact.v3.ffi.with_binary_body] in order to set the + [`with_binary_body`][pact_ffi.with_binary_body] in order to set the binary body, and use - [`with_matching_rules`][pact.v3.ffi.with_matching_rules] to set the + [`with_matching_rules`][pact_ffi.with_matching_rules] to set the matching rules to ensure that only the content type is being matched. Will use a mime type matcher to match the body. Returns false if the @@ -6754,7 +6754,7 @@ def verifier_new_for_application() -> VerifierHandle: By default, verification results will not be published. To enable publishing, use - [`pactffi_verifier_set_publish_options`][pact.v3.ffi.verifier_set_publish_options] + [`pactffi_verifier_set_publish_options`][pact_ffi.verifier_set_publish_options] to set the required values and enable it. [Rust @@ -7490,7 +7490,7 @@ def using_plugin( The plugin needs to be installed correctly for this function to work. Note that plugins run as separate processes, so will need to be cleaned up - afterwards by calling [`cleanup_plugins`][pact.v3.ffi.cleanup_plugins] + afterwards by calling [`cleanup_plugins`][pact_ffi.cleanup_plugins] otherwise you will have plugin processes left running. [Rust From bb27b2553604cb370fe8adef39cffb72be57a4ba Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 30 Jul 2025 12:19:39 +1000 Subject: [PATCH 0892/1376] chore(cli): cleanup build script Signed-off-by: JP-Ellis --- pact-python-cli/hatch_build.py | 136 ++++++++++++++++++++++----------- pact-python-cli/pyproject.toml | 3 +- 2 files changed, 93 insertions(+), 46 deletions(-) diff --git a/pact-python-cli/hatch_build.py b/pact-python-cli/hatch_build.py index e6f65bbc6..30591e74a 100644 --- a/pact-python-cli/hatch_build.py +++ b/pact-python-cli/hatch_build.py @@ -1,13 +1,7 @@ """ -Hatchling build hook for binary downloads. +Hatchling build hook. -Pact Python is built on top of the Ruby Pact binaries and the Rust Pact library. -This build script downloads the binaries and library for the current platform -and installs them in the `pact` directory under `/bin` and `/lib`. - -The version of the binaries and library can be controlled with the -`PACT_BIN_VERSION` and `PACT_LIB_VERSION` environment variables. If these are -not set, a pinned version will be used instead. +This hook is responsible for downloading and packaging the Pact CLI. """ from __future__ import annotations @@ -21,7 +15,6 @@ import urllib.request import zipfile from pathlib import Path -from typing import Any from hatchling.builders.config import BuilderConfig from hatchling.builders.hooks.plugin.interface import BuildHookInterface @@ -29,27 +22,38 @@ logger = logging.getLogger(__name__) -EXE = ".exe" if os.name == "nt" else "" PKG_DIR = Path(__file__).parent.resolve() / "src" / "pact_cli" -PACT_BIN_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" +PACT_CLI_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" class UnsupportedPlatformError(RuntimeError): - """Raised when the current platform is not supported.""" + """ + Custom error raised when the current platform is not supported. + """ def __init__(self, platform: str) -> None: """ Initialize the exception. Args: - platform: The unsupported platform. + platform: + The unsupported platform. """ self.platform = platform super().__init__(f"Unsupported platform {platform}") class PactCliBuildHook(BuildHookInterface[BuilderConfig]): - """Custom hook to download Pact binaries.""" + """ + Custom hook to download Pact CLI binaries. + + This build hook is invoked by Hatch during the build process. Within + `pyproject.toml`, it takes the special name of `custom` (despite the name + below). + + For more references, see [Build hook + plugins](https://hatch.pypa.io/1.3/plugins/build-hook/reference/). + """ PLUGIN_NAME = "pact-cli" @@ -62,27 +66,60 @@ def __init__(self, *args: object, **kwargs: object) -> None: """ super().__init__(*args, **kwargs) self.tmpdir = Path(tempfile.TemporaryDirectory().name) + self.tmpdir.mkdir(parents=True, exist_ok=True) + + def __del__(self) -> None: + """ + Clean up temporary files. + """ + shutil.rmtree(self.tmpdir, ignore_errors=True) def clean(self, versions: list[str]) -> None: # noqa: ARG002 - """Clean up any files created by the build hook.""" + """ + Code called to clean. + + This is called by `hatch clean` or when the `-c`/`--clean` flag is + passed to `hatch build`. + """ for subdir in ["bin", "lib", "data"]: shutil.rmtree(PKG_DIR / subdir, ignore_errors=True) def initialize( self, version: str, # noqa: ARG002 - build_data: dict[str, Any], + build_data: dict[str, object], ) -> None: - """Hook into Hatchling's build process.""" + """ + Code called immediately before each build. + + The CLI version is inferred from the package metadata. Specifically, the + first three segments of the version string are used. + + Args: + version: + Not used (but required by the parent class). + + build_data: + A dictionary to modify in-place used by Hatch when creating the + final wheel. + + Raises: + UnsupportedPlatformError: + If the CLI cannot be built (presumably due to an + incompatible platform). + """ cli_version = ".".join(self.metadata.version.split(".")[:3]) if not cli_version: - self.app.display_error("Failed to determine Pact CLI version.") + msg = "Failed to determine Pact CLI version." + self.app.display_error(msg) + raise ValueError(msg) try: - self._pact_bin_install(cli_version) + build_data["force_include"] = self._install(cli_version) except UnsupportedPlatformError as err: msg = f"Pact CLI is not available for {err.platform}." - logger.exception(msg, RuntimeWarning, stacklevel=2) + self.app.display_error(msg) + raise build_data["tag"] = self._infer_tag() @@ -94,7 +131,7 @@ def _sys_tag_platform(self) -> str: """ return next(t.platform for t in sys_tags()) - def _pact_bin_install(self, version: str) -> None: + def _install(self, version: str) -> dict[str, str]: """ Install the Pact standalone binaries. @@ -104,10 +141,19 @@ def _pact_bin_install(self, version: str) -> None: Args: version: The Pact CLI version to install. + + Returns: + A mapping of `src` to `dst` to be used by Hatch when creating the + wheel. Each `src` is a full path in the current filesystem, and the + `dst` is the corresponding path within the wheel. """ url = self._pact_bin_url(version) - artifact = self._download(url) - self._pact_bin_extract(artifact) + artefact = self._download(url) + self._extract(artefact) + return { + str(PKG_DIR / "bin"): "pact_cli/bin", + str(PKG_DIR / "lib"): "pact_cli/lib", + } def _pact_bin_url(self, version: str) -> str: """ @@ -124,13 +170,13 @@ def _pact_bin_url(self, version: str) -> str: platform = self._sys_tag_platform() if platform.startswith("macosx"): - os = "osx" + os_name = "osx" ext = "tar.gz" elif "linux" in platform: - os = "linux" + os_name = "linux" ext = "tar.gz" elif platform.startswith("win"): - os = "windows" + os_name = "windows" ext = "zip" else: raise UnsupportedPlatformError(platform) @@ -144,14 +190,14 @@ def _pact_bin_url(self, version: str) -> str: else: raise UnsupportedPlatformError(platform) - return PACT_BIN_URL.format( + return PACT_CLI_URL.format( version=version, - os=os, + os=os_name, machine=machine, ext=ext, ) - def _pact_bin_extract(self, artifact: Path) -> None: + def _extract(self, artefact: Path) -> None: """ Extract the Pact binaries. @@ -159,14 +205,15 @@ def _pact_bin_extract(self, artifact: Path) -> None: to be present, which is included in the `lib` directory. Args: - artifact: The path to the downloaded artifact. + artefact: + The path to the downloaded artefact. """ - if str(artifact).endswith(".zip"): - with zipfile.ZipFile(artifact) as f: + if str(artefact).endswith(".zip"): + with zipfile.ZipFile(artefact) as f: f.extractall(self.tmpdir) # noqa: S202 - if str(artifact).endswith(".tar.gz"): - with tarfile.open(artifact) as f: + if str(artefact).endswith(".tar.gz"): + with tarfile.open(artefact) as f: f.extractall(self.tmpdir) # noqa: S202 for d in ["bin", "lib"]: @@ -181,23 +228,24 @@ def _download(self, url: str) -> Path: """ Download the target URL. - This will download the target URL to the `pact/data` directory. If the - download artifact is already present, its path will be returned. + This will download the target URL to the `src/pact_cli/data` directory. + If the download artefact is already present, the existing artefact's + path will be returned without downloading it again. Args: - url: The URL to download + url: + The URL to download Return: - The path to the downloaded artifact. + The path to the downloaded artefact. """ filename = url.split("/")[-1] - artifact = PKG_DIR / "data" / filename - artifact.parent.mkdir(parents=True, exist_ok=True) - - if not artifact.exists(): - urllib.request.urlretrieve(url, artifact) # noqa: S310 + artefact = PKG_DIR / "data" / filename + artefact.parent.mkdir(parents=True, exist_ok=True) - return artifact + if not artefact.exists(): + urllib.request.urlretrieve(url, artefact) # noqa: S310 + return artefact def _infer_tag(self) -> str: """ diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 3e517ed39..e85c12af5 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -92,8 +92,7 @@ requires = ["hatch-vcs", "hatchling", "packaging"] version-file = "src/pact_cli/__version__.py" [tool.hatch.build.targets.wheel] - artifacts = ["src/pact_cli/bin", "src/pact_cli/lib"] - packages = ["src/pact_cli"] + packages = ["src/pact_cli"] [tool.hatch.build.targets.wheel.hooks.custom] patch = "hatch_build.py" From 4fe3cacd1257ac358e86075a50729efb962326ed Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 30 Jul 2025 11:57:16 +1000 Subject: [PATCH 0893/1376] chore(ffi): cleanup build script Signed-off-by: JP-Ellis --- pact-python-ffi/hatch_build.py | 161 +++++++++++++++++++++++---------- 1 file changed, 115 insertions(+), 46 deletions(-) diff --git a/pact-python-ffi/hatch_build.py b/pact-python-ffi/hatch_build.py index 294584c94..ead8dc4c2 100644 --- a/pact-python-ffi/hatch_build.py +++ b/pact-python-ffi/hatch_build.py @@ -1,13 +1,8 @@ """ -Hatchling build hook for binary downloads. +Hatchling build hook. -Pact Python is built on top of the Ruby Pact binaries and the Rust Pact library. -This build script downloads the binaries and library for the current platform -and installs them in the `pact` directory under `/bin` and `/lib`. - -The version of the binaries and library can be controlled with the -`PACT_BIN_VERSION` and `PACT_LIB_VERSION` environment variables. If these are -not set, a pinned version will be used instead. +This hook is responsible for download the Pact FFI library and building the +CFFI bindings for it. """ from __future__ import annotations @@ -15,6 +10,7 @@ import gzip import os import shutil +import subprocess import sys import tempfile import urllib.request @@ -30,25 +26,37 @@ class UnsupportedPlatformError(RuntimeError): - """Raised when the current platform is not supported.""" + """ + Custom error raised when the current platform is not supported. + """ def __init__(self, platform: str) -> None: """ Initialize the exception. Args: - platform: The unsupported platform. + platform: + The unsupported platform. """ self.platform = platform super().__init__(f"Unsupported platform {platform}") class PactBuildHook(BuildHookInterface[Any]): - """Custom hook to download Pact binaries.""" + """ + Custom hook to download Pact binaries. + + This build hook is invoked by Hatch during the build process. Within + `pyproject.toml`, is takes the special name of `custom` (despite the name + below). + + For more references, see [Build hook + plugins](https://hatch.pypa.io/1.3/plugins/build-hook/reference/). + """ PLUGIN_NAME = "pact-ffi" - def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 + def __init__(self, *args: object, **kwargs: object) -> None: """ Initialize the build hook. @@ -59,21 +67,53 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 self.tmpdir = Path(tempfile.TemporaryDirectory().name) self.tmpdir.mkdir(parents=True, exist_ok=True) + def __del__(self) -> None: + """ + Clean up temporary files. + """ + shutil.rmtree(self.tmpdir, ignore_errors=True) + def clean(self, versions: list[str]) -> None: # noqa: ARG002 - """Clean up any files created by the build hook.""" - for ffi in (PKG_DIR / "v3").glob("__init__.*"): + """ + Code called to clean. + + This is called by `hatch clean` or when the `-c`/`--clean` flag is + passed to `hatch build`. + """ + # Cleanup the Python extension + for ffi in PKG_DIR.glob("ffi.*"): if ffi.suffix in (".so", ".dylib", ".dll", ".a", ".pyd"): ffi.unlink() + # Cleanup the Pact FFI library + for lib in PKG_DIR.glob("*pact_ffi.*"): + lib.unlink() def initialize( self, version: str, # noqa: ARG002 - build_data: dict[str, Any], + build_data: dict[str, object], ) -> None: - """Hook into Hatchling's build process.""" + """ + Code called immediately before each build. + + Args: + version: + Not used (but required by the parent class). + + build_data: + A dictionary to modify in-place used by Hatch when creating the + final wheel. + + Raises: + UnsupportedPlatformError: + If the C extension cannot be built (presumably due to an + incompatible platform). + """ ffi_version = ".".join(self.metadata.version.split(".")[:3]) if not ffi_version: - self.app.display_error("Failed to determine Pact FFI version.") + msg = "Failed to determine Pact FFI version." + self.app.display_error(msg) + raise ValueError(msg) try: build_data["force_include"] = self._install(ffi_version) @@ -81,7 +121,7 @@ def initialize( msg = f"Pact FFI library is not available for {err.platform}" self.app.display_error(msg) - self.app.display_debug(f"Wheel artifacts: {build_data['force_include']}") + self.app.display_debug(f"Wheel artefacts: {build_data['force_include']}") build_data["tag"] = self._infer_tag() def _sys_tag_platform(self) -> str: @@ -100,7 +140,13 @@ def _install(self, version: str) -> dict[str, str]: build the CFFI bindings for it. Args: - version: The Pact version to install. + version: + The Pact version to install. + + Returns: + A mapping of `src` to `dst` to be used by Hatch when creating the + wheel. Each `src` is a full path in the current filesystem, and the + `dst` is the corresponding path within the wheel. """ # Download the Pact library binary and header file lib_url = self._lib_url(version) @@ -119,9 +165,10 @@ def _install(self, version: str) -> dict[str, str]: # Copy into the package directory, using the ABI3 marking for broad # compatibility. # NOTE: Windows does _not_ use the version infixation - extension_name, _, suffix = extension.name.split(".") + name = extension.name.split(".")[0] + suffix = extension.suffix infix = ".abi3" if os.name != "nt" else "" - extension_dest = f"{extension_name}{infix}.{suffix}" + extension_dest = f"{name}{infix}{suffix}" shutil.copy(extension, PKG_DIR / extension_dest) if pact_lib_dir := os.getenv("PACT_LIB_DIR"): @@ -142,7 +189,8 @@ def _lib_url(self, version: str) -> str: # noqa: C901, PLR0912 Generate the download URL for the Pact library. Args: - version: The upstream Pact version. + version: + The upstream Pact version. Returns: The URL to download the Pact library from. @@ -215,16 +263,17 @@ def _lib_url(self, version: str) -> str: # noqa: C901, PLR0912 ext=ext, ) - def _extract_lib(self, artifact: Path) -> Path: + def _extract_lib(self, artefact: Path) -> Path: """ Extract the Pact library. Args: - artifact: The URL to download the Pact binaries from. + artefact: + The URL to download the Pact binaries from. """ - target = PKG_DIR / (artifact.name.split("-")[0] + artifact.suffixes[-2]) + target = PKG_DIR / (artefact.name.split("-")[0] + artefact.suffixes[-2]) with ( - gzip.open(artifact, "rb") as f_in, + gzip.open(artefact, "rb") as f_in, target.open("wb") as f_out, ): shutil.copyfileobj(f_in, f_out) @@ -275,10 +324,15 @@ def _compile(self, lib: Path, header: Path) -> Path: ) linker_args: list[str] = [] - if os.name == "posix": + if sys.platform == "darwin": + # On macOS, we pad the headers so that install_name_tool can + # subsequently adjust them. + linker_args.append("-Wl,-headerpad_max_install_names") + elif sys.platform == "linux": + # On Linux, we set the RPATH linker_args.append(f"-Wl,-rpath,{lib.parent}") - elif os.name == "nt": - # Windows has no equivalent to rpath, instead, the end-user must + elif sys.platform == "win32": + # Windows has no equivalent to RPATH, instead, the end-user must # ensure that the PATH environment variable is updated to include # the directory containing the Pact library. self.app.display_warning( @@ -294,6 +348,17 @@ def _compile(self, lib: Path, header: Path) -> Path: extra_link_args=linker_args, ) extension = Path(ffibuilder.compile(verbose=True, tmpdir=str(self.tmpdir))) + + if sys.platform == "darwin": + self.app.display_debug(f"Updating install names for {extension}") + subprocess.check_call([ # noqa: S603, S607 + "install_name_tool", + "-change", + "libpact_ffi.dylib", + str(lib), + str(extension), + ]) + self.app.display_debug(f"Compiled CFFI bindings to {extension}") return extension @@ -301,38 +366,42 @@ def _download(self, url: str) -> Path: """ Download the target URL. - This will download the target URL to the `pact/data` directory. If the - download artifact is already present, its path will be returned. - - If `extract` is True, the downloaded artifact will be extracted and the - path to the extract file will be returned instead. + This will download the target URL to the `src/pact_ffi/data` directory. + If the download artefact is already present, the existing artefact's + path will be returned without downloading it again. Args: - url: The URL to download - extract: Whether to extract the downloaded artifact. + url: + The URL to download Return: - The path to the downloaded artifact. + The path to the downloaded artefact. """ filename = url.split("/")[-1] - artifact = PKG_DIR / "data" / filename - artifact.parent.mkdir(parents=True, exist_ok=True) + artefact = PKG_DIR / "data" / filename + artefact.parent.mkdir(parents=True, exist_ok=True) - if not artifact.exists(): - self.app.display_debug(f"Downloading {url} to {artifact}") - urllib.request.urlretrieve(url, artifact) # noqa: S310 + if not artefact.exists(): + self.app.display_debug(f"Downloading {url} to {artefact}") + urllib.request.urlretrieve(url, artefact) # noqa: S310 else: - self.app.display_debug(f"Using cached artifact {artifact}") + self.app.display_debug(f"Using cached artefact {artefact}") - return artifact + return artefact def _infer_tag(self) -> str: """ Infer the tag for the current build. The bindings are built to target ABI3, which is compatible with multiple - Python versions. As a result, we generate `py3-abi3-{platform}` tags for - the wheels. + Python versions. As a result, we generate `py{version}-abi3-{platform}` + tags for the wheels. + + While the ABI3 interface was introduced in Python 3.2, we target the + earliest supported version of Python in the Python wrapper. + + Return: + The tag for the current build. """ python_version = f"{sys.version_info.major}{sys.version_info.minor}" From aae65809720a4177a096eea87247d764695c5a30 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 30 Jul 2025 12:00:51 +1000 Subject: [PATCH 0894/1376] chore: ignore extensions Signed-off-by: JP-Ellis --- pact-python-ffi/.gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pact-python-ffi/.gitignore b/pact-python-ffi/.gitignore index 3f3a7ec73..f3039de16 100644 --- a/pact-python-ffi/.gitignore +++ b/pact-python-ffi/.gitignore @@ -1,2 +1,6 @@ src/pact_ffi/data src/pact_ffi/__version__.py +src/pact_ffi/*.pyd +src/pact_ffi/*.so +src/pact_ffi/*.dylib +src/pact_ffi/*.a From 2c08912bfb4faf0ba2c3947c2d40e392e0eace18 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 30 Jul 2025 12:41:56 +1000 Subject: [PATCH 0895/1376] chore(cli): fix flakey test Signed-off-by: JP-Ellis --- pact-python-cli/tests/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pact-python-cli/tests/test_init.py b/pact-python-cli/tests/test_init.py index 32999c2ce..8f131fc5b 100644 --- a/pact-python-cli/tests/test_init.py +++ b/pact-python-cli/tests/test_init.py @@ -130,7 +130,7 @@ def test_exec_wrapper_mock_service() -> None: stderr=subprocess.PIPE, text=True, ) - time.sleep(1) + time.sleep(2) process.terminate() process.wait() From 04ea22636ec44f20da82f5b508df89a0a730e6f2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 04:10:13 +0000 Subject: [PATCH 0896/1376] chore(deps): update pypa/cibuildwheel action to v3.1.2 (#1075) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index ff1d3ee58..c5ec52e41 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -98,7 +98,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@95d2f3a92fbf80abe066b09418bbf128a8923df2 # v3.0.1 + uses: pypa/cibuildwheel@9e4e50bd76b3190f55304387e333f6234823ea9b # v3.1.2 with: package-dir: pact-python-cli env: diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 691ec3eaa..f43ed5d10 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -98,7 +98,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@95d2f3a92fbf80abe066b09418bbf128a8923df2 # v3.0.1 + uses: pypa/cibuildwheel@9e4e50bd76b3190f55304387e333f6234823ea9b # v3.1.2 with: package-dir: pact-python-ffi env: From 2975cd06b0bde8ead519dc8ee07f0bb58266f9c2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:37:07 +1000 Subject: [PATCH 0897/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.1.3 (#1138) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4f7cf126..e98bc422f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.1.2 + rev: v2.1.3 hooks: - id: biome-check From af4acdbcc5bfb358174224c4763a1e890871a089 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 04:42:58 +0000 Subject: [PATCH 0898/1376] fix(deps): update ruff to v0.12.7 (#1128) Co-authored-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- src/pact/v3/_server.py | 8 ++++---- tests/v3/compatibility_suite/util/provider.py | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e98bc422f..8ce792f03 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.4 + rev: v0.12.7 hooks: - id: ruff-check # Exclude python files in pact/** and tests/**, except for the diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index e85c12af5..be42c69e1 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.9" devel = [ "pact-python-cli[devel-test]", "pact-python-cli[devel-types]", - "ruff==0.12.4", + "ruff==0.12.7", ] devel-test = ["pytest-cov~=6.0", "pytest-mock~=3.0", "pytest~=8.0"] devel-types = ["mypy==1.17.0"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 68eb045a5..da669298d 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -47,7 +47,7 @@ dependencies = ["cffi~=1.0"] devel = [ "pact-python-ffi[devel-test]", "pact-python-ffi[devel-types]", - "ruff==0.12.4", + "ruff==0.12.7", ] devel-test = ["pytest-cov~=6.0", "pytest-mock~=3.0", "pytest~=8.0"] devel-types = ["mypy==1.17.0"] diff --git a/pyproject.toml b/pyproject.toml index ee3f92b73..b728a67e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ dependencies = [ "pact-python[devel-docs]", "pact-python[devel-test]", "pact-python[devel-types]", - "ruff==0.12.4", + "ruff==0.12.7", ] devel-docs = [ "mkdocs-literate-nav~=0.6", diff --git a/src/pact/v3/_server.py b/src/pact/v3/_server.py index 19a1e6172..32c520fb5 100644 --- a/src/pact/v3/_server.py +++ b/src/pact/v3/_server.py @@ -241,7 +241,7 @@ def version_string(self) -> str: """ return f"Pact Python Message Relay/{__version__}" - def do_POST(self) -> None: # noqa: N802 + def do_POST(self) -> None: """ Handle a POST request. @@ -288,7 +288,7 @@ def do_POST(self) -> None: # noqa: N802 self.end_headers() self.wfile.write(contents) - def do_GET(self) -> None: # noqa: N802 + def do_GET(self) -> None: """ Handle a GET request. @@ -429,7 +429,7 @@ def version_string(self) -> str: """ return f"Pact Python State Callback/{__version__}" - def do_POST(self) -> None: # noqa: N802 + def do_POST(self) -> None: """ Handle a POST request. @@ -464,7 +464,7 @@ def do_POST(self) -> None: # noqa: N802 self.send_response(200, "OK") self.end_headers() - def do_GET(self) -> None: # noqa: N802 + def do_GET(self) -> None: """ Handle a GET request. diff --git a/tests/v3/compatibility_suite/util/provider.py b/tests/v3/compatibility_suite/util/provider.py index 97a17b42c..fd0c01e6d 100644 --- a/tests/v3/compatibility_suite/util/provider.py +++ b/tests/v3/compatibility_suite/util/provider.py @@ -279,7 +279,7 @@ def _record_request(self) -> None: self.server.requests.append(request) self.rfile = BytesIO(body) - def do_POST(self) -> None: # noqa: N802 + def do_POST(self) -> None: """ Handle a POST request. """ @@ -298,7 +298,7 @@ def do_POST(self) -> None: # noqa: N802 ) self.send_error(404, "Not Found") - def do_GET(self) -> None: # noqa: N802 + def do_GET(self) -> None: """ Handle a GET request. """ From 25458008fb5d1347e4cd430e3413a925d841eda3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 30 Jul 2025 15:31:29 +1000 Subject: [PATCH 0899/1376] feat!: prepare for v3 release Prepare for the release of Pact Python v3. This moves the contents of `pact.*` into `pact.v2` for backwards compatibility; and `pact.v3.*` is moved into `pact.*`. Some minor changes to some of the test fixtures have been incorporated while fixing the tests. BREAKING CHANGE: This prepares for version 3. Pact Python v2 will be still accessible under `pact.v2` and all imports should be appropriate renamed. Everyone is encouraged to migrate to Pact Python v3. Signed-off-by: JP-Ellis --- .gitmodules | 2 +- .pre-commit-config.yaml | 19 +- examples/README.md | 4 +- examples/conftest.py | 4 +- ...cker-compose.yml => container-compose.yml} | 15 +- examples/{tests => plugins}/__init__.py | 0 examples/{v3 => }/plugins/proto/__init__.py | 0 examples/{v3 => }/plugins/proto/person.proto | 0 examples/{v3 => }/plugins/proto/person_pb2.py | 0 .../{v3 => }/plugins/proto/person_pb2.pyi | 0 .../{v3 => }/plugins/proto/person_pb2_grpc.py | 0 .../{v3 => }/plugins/protobuf/__init__.py | 0 .../plugins/protobuf/test_consumer.py | 9 +- .../plugins/protobuf/test_provider.py | 14 +- examples/{tests/v3 => v2}/__init__.py | 0 examples/{ => v2}/src/__init__.py | 0 examples/{ => v2}/src/consumer.py | 2 +- examples/{ => v2}/src/fastapi.py | 0 examples/{ => v2}/src/flask.py | 0 examples/{ => v2}/src/message.py | 0 examples/{ => v2}/src/message_producer.py | 0 examples/{v3 => v2/tests}/__init__.py | 0 examples/{ => v2}/tests/test_00_consumer.py | 10 +- .../tests/test_01_provider_fastapi.py | 16 +- .../{ => v2}/tests/test_01_provider_flask.py | 18 +- .../tests/test_02_message_consumer.py | 8 +- .../tests/test_03_message_provider.py | 2 +- .../{v3/plugins => v2/tests/v3}/__init__.py | 0 .../{ => v2}/tests/v3/test_00_consumer.py | 9 +- .../tests/v3/test_01_fastapi_provider.py | 66 +- .../tests/v3/test_02_message_consumer.py | 9 +- .../tests/v3/test_03_message_provider.py | 6 +- pyproject.toml | 50 +- src/pact/__init__.py | 102 +- src/pact/{v3 => }/_server.py | 4 +- src/pact/{v3 => }/_util.py | 0 src/pact/{v3 => }/error.py | 0 src/pact/{v3 => }/generate/__init__.py | 6 +- src/pact/{v3 => }/generate/generator.py | 2 +- src/pact/{v3 => }/interaction/__init__.py | 10 +- .../interaction/_async_message_interaction.py | 2 +- src/pact/{v3 => }/interaction/_base.py | 4 +- .../{v3 => }/interaction/_http_interaction.py | 6 +- .../interaction/_sync_message_interaction.py | 2 +- src/pact/{v3 => }/match/__init__.py | 16 +- src/pact/{v3 => }/match/matcher.py | 4 +- src/pact/pact.py | 1133 ++++++++----- src/pact/{v3 => }/py.typed | 0 src/pact/{v3 => }/types.py | 0 src/pact/{v3 => }/types.pyi | 0 src/pact/v2/__init__.py | 43 + src/pact/{ => v2}/broker.py | 7 - src/pact/{ => v2}/cli/__init__.py | 0 src/pact/{ => v2}/cli/verify.py | 2 +- src/pact/{ => v2}/constants.py | 0 src/pact/{ => v2}/consumer.py | 11 +- src/pact/{ => v2}/http_proxy.py | 9 +- src/pact/{ => v2}/matchers.py | 47 +- src/pact/{ => v2}/message_consumer.py | 11 +- src/pact/{ => v2}/message_pact.py | 9 +- src/pact/{ => v2}/message_provider.py | 7 - src/pact/v2/pact.py | 461 ++++++ src/pact/{ => v2}/provider.py | 9 - src/pact/v2/verifier.py | 144 ++ src/pact/{ => v2}/verify_wrapper.py | 44 +- src/pact/v3/__init__.py | 83 - src/pact/v3/pact.py | 819 --------- src/pact/v3/verifier.py | 1352 --------------- src/pact/verifier.py | 1465 +++++++++++++++-- tests/.ruff.toml | 15 +- tests/__init__.py | 3 + tests/{v3 => }/assets/pacts/basic.json | 0 .../{v3 => }/compatibility_suite/__init__.py | 0 .../{v3 => }/compatibility_suite/conftest.py | 2 +- tests/{v3 => }/compatibility_suite/definition | 0 .../compatibility_suite/test_v1_consumer.py | 6 +- .../compatibility_suite/test_v1_provider.py | 6 +- .../compatibility_suite/test_v2_consumer.py | 6 +- .../compatibility_suite/test_v2_provider.py | 6 +- .../compatibility_suite/test_v3_consumer.py | 6 +- .../test_v3_http_matching.py | 17 +- .../test_v3_message_consumer.py | 22 +- .../test_v3_message_producer.py | 35 +- .../compatibility_suite/test_v3_provider.py | 6 +- .../compatibility_suite/test_v4_consumer.py | 6 +- .../test_v4_message_consumer.py | 6 +- .../test_v4_message_provider.py | 2 +- .../compatibility_suite/test_v4_provider.py | 6 +- .../compatibility_suite/util/__init__.py | 2 +- .../compatibility_suite/util/consumer.py | 24 +- .../util/interaction_definition.py | 10 +- .../compatibility_suite/util/pact-broker.yml | 0 .../compatibility_suite/util/provider.py | 91 +- tests/conftest.py | 38 +- tests/pacts/.gitignore | 2 + tests/{v3 => }/test_async_interaction.py | 2 +- tests/{v3 => }/test_error.py | 4 +- tests/{v3 => }/test_http_interaction.py | 12 +- tests/{v3 => }/test_match.py | 2 +- tests/test_pact.py | 784 ++------- tests/{v3 => }/test_server.py | 2 +- tests/{v3 => }/test_sync_interaction.py | 2 +- tests/{v3 => }/test_util.py | 2 +- tests/test_verifier.py | 427 ++--- tests/{cli => v2}/__init__.py | 0 tests/v2/cli/__init__.py | 0 tests/{ => v2}/cli/test_verify.py | 52 +- tests/{ => v2}/test_broker.py | 6 +- tests/{ => v2}/test_constants.py | 24 +- tests/{ => v2}/test_consumer.py | 6 +- tests/{ => v2}/test_http_proxy.py | 2 +- tests/{ => v2}/test_matchers.py | 2 +- tests/{ => v2}/test_message_consumer.py | 6 +- tests/{ => v2}/test_message_pact.py | 8 +- tests/{ => v2}/test_message_provider.py | 22 +- tests/v2/test_pact.py | 663 ++++++++ tests/v2/test_verifier.py | 267 +++ tests/{ => v2}/test_verify_wrapper.py | 40 +- tests/v3/__init__.py | 3 - tests/v3/conftest.py | 18 - tests/v3/test_pact.py | 131 -- tests/v3/test_verifier.py | 166 -- 122 files changed, 4426 insertions(+), 4621 deletions(-) rename examples/{docker-compose.yml => container-compose.yml} (56%) rename examples/{tests => plugins}/__init__.py (100%) rename examples/{v3 => }/plugins/proto/__init__.py (100%) rename examples/{v3 => }/plugins/proto/person.proto (100%) rename examples/{v3 => }/plugins/proto/person_pb2.py (100%) rename examples/{v3 => }/plugins/proto/person_pb2.pyi (100%) rename examples/{v3 => }/plugins/proto/person_pb2_grpc.py (100%) rename examples/{v3 => }/plugins/protobuf/__init__.py (100%) rename examples/{v3 => }/plugins/protobuf/test_consumer.py (96%) rename examples/{v3 => }/plugins/protobuf/test_provider.py (97%) rename examples/{tests/v3 => v2}/__init__.py (100%) rename examples/{ => v2}/src/__init__.py (100%) rename examples/{ => v2}/src/consumer.py (98%) rename examples/{ => v2}/src/fastapi.py (100%) rename examples/{ => v2}/src/flask.py (100%) rename examples/{ => v2}/src/message.py (100%) rename examples/{ => v2}/src/message_producer.py (100%) rename examples/{v3 => v2/tests}/__init__.py (100%) rename examples/{ => v2}/tests/test_00_consumer.py (96%) rename examples/{ => v2}/tests/test_01_provider_fastapi.py (94%) rename examples/{ => v2}/tests/test_01_provider_flask.py (94%) rename examples/{ => v2}/tests/test_02_message_consumer.py (95%) rename examples/{ => v2}/tests/test_03_message_provider.py (98%) rename examples/{v3/plugins => v2/tests/v3}/__init__.py (100%) rename examples/{ => v2}/tests/v3/test_00_consumer.py (96%) rename examples/{ => v2}/tests/v3/test_01_fastapi_provider.py (87%) rename examples/{ => v2}/tests/v3/test_02_message_consumer.py (95%) rename examples/{ => v2}/tests/v3/test_03_message_provider.py (92%) rename src/pact/{v3 => }/_server.py (99%) rename src/pact/{v3 => }/_util.py (100%) rename src/pact/{v3 => }/error.py (100%) rename src/pact/{v3 => }/generate/__init__.py (98%) rename src/pact/{v3 => }/generate/generator.py (99%) rename src/pact/{v3 => }/interaction/__init__.py (88%) rename src/pact/{v3 => }/interaction/_async_message_interaction.py (97%) rename src/pact/{v3 => }/interaction/_base.py (99%) rename src/pact/{v3 => }/interaction/_http_interaction.py (99%) rename src/pact/{v3 => }/interaction/_sync_message_interaction.py (98%) rename src/pact/{v3 => }/match/__init__.py (98%) rename src/pact/{v3 => }/match/matcher.py (98%) rename src/pact/{v3 => }/py.typed (100%) rename src/pact/{v3 => }/types.py (100%) rename src/pact/{v3 => }/types.pyi (100%) create mode 100644 src/pact/v2/__init__.py rename src/pact/{ => v2}/broker.py (94%) rename src/pact/{ => v2}/cli/__init__.py (100%) rename src/pact/{ => v2}/cli/verify.py (99%) rename src/pact/{ => v2}/constants.py (100%) rename src/pact/{ => v2}/consumer.py (95%) rename src/pact/{ => v2}/http_proxy.py (84%) rename src/pact/{ => v2}/matchers.py (91%) rename src/pact/{ => v2}/message_consumer.py (94%) rename src/pact/{ => v2}/message_pact.py (96%) rename src/pact/{ => v2}/message_provider.py (95%) create mode 100644 src/pact/v2/pact.py rename src/pact/{ => v2}/provider.py (61%) create mode 100644 src/pact/v2/verifier.py rename src/pact/{ => v2}/verify_wrapper.py (83%) delete mode 100644 src/pact/v3/__init__.py delete mode 100644 src/pact/v3/pact.py delete mode 100644 src/pact/v3/verifier.py rename tests/{v3 => }/assets/pacts/basic.json (100%) rename tests/{v3 => }/compatibility_suite/__init__.py (100%) rename tests/{v3 => }/compatibility_suite/conftest.py (98%) rename tests/{v3 => }/compatibility_suite/definition (100%) rename tests/{v3 => }/compatibility_suite/test_v1_consumer.py (98%) rename tests/{v3 => }/compatibility_suite/test_v1_provider.py (98%) rename tests/{v3 => }/compatibility_suite/test_v2_consumer.py (97%) rename tests/{v3 => }/compatibility_suite/test_v2_provider.py (95%) rename tests/{v3 => }/compatibility_suite/test_v3_consumer.py (97%) rename tests/{v3 => }/compatibility_suite/test_v3_http_matching.py (94%) rename tests/{v3 => }/compatibility_suite/test_v3_message_consumer.py (97%) rename tests/{v3 => }/compatibility_suite/test_v3_message_producer.py (93%) rename tests/{v3 => }/compatibility_suite/test_v3_provider.py (95%) rename tests/{v3 => }/compatibility_suite/test_v4_consumer.py (96%) rename tests/{v3 => }/compatibility_suite/test_v4_message_consumer.py (95%) rename tests/{v3 => }/compatibility_suite/test_v4_message_provider.py (97%) rename tests/{v3 => }/compatibility_suite/test_v4_provider.py (95%) rename tests/{v3 => }/compatibility_suite/util/__init__.py (99%) rename tests/{v3 => }/compatibility_suite/util/consumer.py (97%) rename tests/{v3 => }/compatibility_suite/util/interaction_definition.py (99%) rename tests/{v3 => }/compatibility_suite/util/pact-broker.yml (100%) rename tests/{v3 => }/compatibility_suite/util/provider.py (95%) create mode 100644 tests/pacts/.gitignore rename tests/{v3 => }/test_async_interaction.py (96%) rename tests/{v3 => }/test_error.py (99%) rename tests/{v3 => }/test_http_interaction.py (98%) rename tests/{v3 => }/test_match.py (99%) rename tests/{v3 => }/test_server.py (98%) rename tests/{v3 => }/test_sync_interaction.py (96%) rename tests/{v3 => }/test_util.py (99%) rename tests/{cli => v2}/__init__.py (100%) create mode 100644 tests/v2/cli/__init__.py rename tests/{ => v2}/cli/test_verify.py (90%) rename tests/{ => v2}/test_broker.py (98%) rename tests/{ => v2}/test_constants.py (56%) rename tests/{ => v2}/test_consumer.py (95%) rename tests/{ => v2}/test_http_proxy.py (98%) rename tests/{ => v2}/test_matchers.py (99%) rename tests/{ => v2}/test_message_consumer.py (93%) rename tests/{ => v2}/test_message_pact.py (98%) rename tests/{ => v2}/test_message_provider.py (87%) create mode 100644 tests/v2/test_pact.py create mode 100644 tests/v2/test_verifier.py rename tests/{ => v2}/test_verify_wrapper.py (91%) delete mode 100644 tests/v3/__init__.py delete mode 100644 tests/v3/conftest.py delete mode 100644 tests/v3/test_pact.py delete mode 100644 tests/v3/test_verifier.py diff --git a/.gitmodules b/.gitmodules index ebf9afa34..6a51a5997 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "compatibility-suite"] - path = tests/v3/compatibility_suite/definition + path = tests/compatibility_suite/definition url = ../pact-compatibility-suite.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ce792f03..198f2e169 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,14 +49,18 @@ repos: rev: v0.12.7 hooks: - id: ruff-check - # Exclude python files in pact/** and tests/**, except for the - # files in src/pact/v3/** and tests/v3/**. - exclude: ^(src/pact|tests)/(?!v3/).*\.py$ + exclude: | + (?x)^( + (src/pact|tests|examples)/v2/.*\\.pyi? + )$ args: - --fix - --exit-non-zero-on-fix - id: ruff-format - exclude: ^(pact|tests)/(?!v3/).*\.py$ + exclude: | + (?x)^( + (src/pact|tests|examples)/v2/.*\\.pyi? + )$ - repo: https://github.com/crate-ci/committed rev: v1.1.7 @@ -69,8 +73,8 @@ repos: - id: markdownlint exclude: | (?x)^( - .github/PULL_REQUEST_TEMPLATE\.md - | CHANGELOG.md + .github/PULL_REQUEST_TEMPLATE\.md | + CHANGELOG.md ) - repo: https://github.com/crate-ci/typos @@ -90,8 +94,7 @@ repos: - python exclude: | (?x)^( - .*/hatch_build.py | - (src|tests|examples)/(?!v3/).*\.py + (src/pact|tests|examples)/v2/.*\\.pyi? )$ stages: - pre-push diff --git a/examples/README.md b/examples/README.md index 0034bdafa..b6727c20c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -78,7 +78,7 @@ In this way, Pact is consumer-driven and can ensure that the provider is compati ### Consumer -The consumer in this example is a simple Python script that makes a HTTP GET request to a server. It is defined in [`src/consumer.py`][examples.src.consumer]. The tests for the consumer are defined in [`tests/test_00_consumer.py`][examples.tests.test_00_consumer]. Each interaction is defined using the format mentioned above. Programmatically, this looks like: +The consumer in this example is a simple Python script that makes a HTTP GET request to a server. It is defined in [`src/consumer.py`][examples.v2.src.consumer]. The tests for the consumer are defined in [`tests/test_00_consumer.py`][examples.tests.test_00_consumer]. Each interaction is defined using the format mentioned above. Programmatically, this looks like: ```py expected: dict[str, Any] = { @@ -97,7 +97,7 @@ expected: dict[str, Any] = { ### Provider -This example showcases two different providers; one written in Flask and one written in FastAPI. Both are simple Python web servers that respond to a HTTP GET request. The Flask provider is defined in [`src/flask.py`][examples.src.flask] and the FastAPI provider is defined in [`src/fastapi.py`][examples.src.fastapi]. The tests for the providers are defined in [`tests/test_01_provider_flask.py`][examples.tests.test_01_provider_flask] and [`tests/test_01_provider_fastapi.py`][examples.tests.test_01_provider_fastapi]. +This example showcases two different providers; one written in Flask and one written in FastAPI. Both are simple Python web servers that respond to a HTTP GET request. The Flask provider is defined in [`src/flask.py`][examples.v2.src.flask] and the FastAPI provider is defined in [`src/fastapi.py`][examples.v2.src.fastapi]. The tests for the providers are defined in [`tests/test_01_provider_flask.py`][examples.tests.test_01_provider_flask] and [`tests/test_01_provider_fastapi.py`][examples.tests.test_01_provider_fastapi]. Unlike the consumer side, the provider side is responsible for responding to the interactions defined by the consumers. In this regard, the provider testing is rather simple: diff --git a/examples/conftest.py b/examples/conftest.py index 513fee6b5..2c6b2f2c5 100644 --- a/examples/conftest.py +++ b/examples/conftest.py @@ -61,7 +61,7 @@ def broker(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: with DockerCompose( EXAMPLE_DIR, - compose_file_name=["docker-compose.yml"], + compose_file_name=["container-compose.yml"], pull=True, wait=False, ) as _: @@ -69,7 +69,7 @@ def broker(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: @pytest.fixture(scope="session") -def pact_dir() -> Path: +def pacts_path() -> Path: """Fixture for the Pact directory.""" return EXAMPLE_DIR / "pacts" diff --git a/examples/docker-compose.yml b/examples/container-compose.yml similarity index 56% rename from examples/docker-compose.yml rename to examples/container-compose.yml index d249cf434..15aad7288 100644 --- a/examples/docker-compose.yml +++ b/examples/container-compose.yml @@ -2,20 +2,8 @@ version: '3.9' services: - postgres: - image: postgres - healthcheck: - test: psql postgres -U postgres --command 'SELECT 1' - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: postgres - broker: image: pactfoundation/pact-broker:latest-multi - depends_on: - postgres: - condition: service_healthy ports: - 9292:9292 restart: always @@ -25,8 +13,7 @@ services: PACT_BROKER_BASIC_AUTH_USERNAME: pactbroker PACT_BROKER_BASIC_AUTH_PASSWORD: pactbroker # Database - PACT_BROKER_DATABASE_URL: postgres://postgres:postgres@postgres/postgres - # PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite # Pending pact-foundation/pact-broker-docker#148 + PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite healthcheck: test: diff --git a/examples/tests/__init__.py b/examples/plugins/__init__.py similarity index 100% rename from examples/tests/__init__.py rename to examples/plugins/__init__.py diff --git a/examples/v3/plugins/proto/__init__.py b/examples/plugins/proto/__init__.py similarity index 100% rename from examples/v3/plugins/proto/__init__.py rename to examples/plugins/proto/__init__.py diff --git a/examples/v3/plugins/proto/person.proto b/examples/plugins/proto/person.proto similarity index 100% rename from examples/v3/plugins/proto/person.proto rename to examples/plugins/proto/person.proto diff --git a/examples/v3/plugins/proto/person_pb2.py b/examples/plugins/proto/person_pb2.py similarity index 100% rename from examples/v3/plugins/proto/person_pb2.py rename to examples/plugins/proto/person_pb2.py diff --git a/examples/v3/plugins/proto/person_pb2.pyi b/examples/plugins/proto/person_pb2.pyi similarity index 100% rename from examples/v3/plugins/proto/person_pb2.pyi rename to examples/plugins/proto/person_pb2.pyi diff --git a/examples/v3/plugins/proto/person_pb2_grpc.py b/examples/plugins/proto/person_pb2_grpc.py similarity index 100% rename from examples/v3/plugins/proto/person_pb2_grpc.py rename to examples/plugins/proto/person_pb2_grpc.py diff --git a/examples/v3/plugins/protobuf/__init__.py b/examples/plugins/protobuf/__init__.py similarity index 100% rename from examples/v3/plugins/protobuf/__init__.py rename to examples/plugins/protobuf/__init__.py diff --git a/examples/v3/plugins/protobuf/test_consumer.py b/examples/plugins/protobuf/test_consumer.py similarity index 96% rename from examples/v3/plugins/protobuf/test_consumer.py rename to examples/plugins/protobuf/test_consumer.py index 79620fddb..08559d6be 100644 --- a/examples/v3/plugins/protobuf/test_consumer.py +++ b/examples/plugins/protobuf/test_consumer.py @@ -14,23 +14,23 @@ from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING import pytest import requests -from pact.v3 import Pact +from pact import Pact from ..proto.person_pb2 import Person from . import address_book if TYPE_CHECKING: from collections.abc import Generator + from pathlib import Path @pytest.fixture -def pact() -> Generator[Pact, None, None]: +def pact(pacts_path: Path) -> Generator[Pact, None, None]: """ Set up the Pact fixture with protobuf plugin. @@ -45,14 +45,13 @@ def pact() -> Generator[Pact, None, None]: Yields: The configured Pact instance for protobuf consumer tests. """ - pact_dir = Path(__file__).parents[3] / "pacts" pact = ( Pact("protobuf_consumer", "protobuf_provider") .with_specification("V4") .using_plugin("protobuf") ) yield pact - pact.write_file(pact_dir) + pact.write_file(pacts_path) def test_get_person_by_id(pact: Pact) -> None: diff --git a/examples/v3/plugins/protobuf/test_provider.py b/examples/plugins/protobuf/test_provider.py similarity index 97% rename from examples/v3/plugins/protobuf/test_provider.py rename to examples/plugins/protobuf/test_provider.py index d11487ea0..e0bc5d5ee 100644 --- a/examples/v3/plugins/protobuf/test_provider.py +++ b/examples/plugins/protobuf/test_provider.py @@ -21,23 +21,23 @@ import contextlib import time -from pathlib import Path from threading import Thread from typing import TYPE_CHECKING, Any, Literal import pytest import uvicorn -from yarl import URL - from fastapi import FastAPI, HTTPException from fastapi.responses import Response -from pact.v3 import Verifier +from yarl import URL + +from pact import Verifier from ..proto.person_pb2 import AddressBook from . import address_book if TYPE_CHECKING: from collections.abc import Generator + from pathlib import Path PROVIDER_URL = URL("http://localhost:8001") @@ -144,7 +144,7 @@ def server() -> Generator[str, None, None]: yield url -def test_provider(server: str) -> None: +def test_provider(server: str, pacts_path: Path) -> None: """ Test the protobuf provider against the consumer contract. @@ -160,9 +160,7 @@ def test_provider(server: str) -> None: 3. Sets up state handlers to prepare test data 4. Verifies all interactions match the contract """ - pact_file = ( - Path(__file__).parents[3] / "pacts" / "protobuf_consumer-protobuf_provider.json" - ) + pact_file = pacts_path / "protobuf_consumer-protobuf_provider.json" verifier = ( Verifier("protobuf_provider") diff --git a/examples/tests/v3/__init__.py b/examples/v2/__init__.py similarity index 100% rename from examples/tests/v3/__init__.py rename to examples/v2/__init__.py diff --git a/examples/src/__init__.py b/examples/v2/src/__init__.py similarity index 100% rename from examples/src/__init__.py rename to examples/v2/src/__init__.py diff --git a/examples/src/consumer.py b/examples/v2/src/consumer.py similarity index 98% rename from examples/src/consumer.py rename to examples/v2/src/consumer.py index 13e097ce2..966a80955 100644 --- a/examples/src/consumer.py +++ b/examples/v2/src/consumer.py @@ -9,7 +9,7 @@ The consumer is the application which makes requests to another service (the provider) and receives a response to process. In this example, we have a simple -[`User`][examples.src.consumer.User] class and the consumer fetches a user's +[`User`][examples.v2.src.consumer.User] class and the consumer fetches a user's information from a HTTP endpoint. This also showcases how Pact tests differ from merely testing adherence to an diff --git a/examples/src/fastapi.py b/examples/v2/src/fastapi.py similarity index 100% rename from examples/src/fastapi.py rename to examples/v2/src/fastapi.py diff --git a/examples/src/flask.py b/examples/v2/src/flask.py similarity index 100% rename from examples/src/flask.py rename to examples/v2/src/flask.py diff --git a/examples/src/message.py b/examples/v2/src/message.py similarity index 100% rename from examples/src/message.py rename to examples/v2/src/message.py diff --git a/examples/src/message_producer.py b/examples/v2/src/message_producer.py similarity index 100% rename from examples/src/message_producer.py rename to examples/v2/src/message_producer.py diff --git a/examples/v3/__init__.py b/examples/v2/tests/__init__.py similarity index 100% rename from examples/v3/__init__.py rename to examples/v2/tests/__init__.py diff --git a/examples/tests/test_00_consumer.py b/examples/v2/tests/test_00_consumer.py similarity index 96% rename from examples/tests/test_00_consumer.py rename to examples/v2/tests/test_00_consumer.py index 5fed1e449..ffe964c9b 100644 --- a/examples/tests/test_00_consumer.py +++ b/examples/v2/tests/test_00_consumer.py @@ -23,14 +23,14 @@ import requests from yarl import URL -from examples.src.consumer import User, UserConsumer -from pact import Consumer, Format, Like, Provider +from examples.v2.src.consumer import User, UserConsumer +from pact.v2 import Consumer, Format, Like, Provider if TYPE_CHECKING: from collections.abc import Generator from pathlib import Path - from pact.pact import Pact + from pact.v2.pact import Pact logger = logging.getLogger(__name__) @@ -56,7 +56,7 @@ def user_consumer() -> UserConsumer: @pytest.fixture(scope="module") -def pact(broker: URL, pact_dir: Path) -> Generator[Pact, Any, None]: +def pact(broker: URL, pacts_path: Path) -> Generator[Pact, Any, None]: """ Set up Pact. @@ -78,7 +78,7 @@ def pact(broker: URL, pact_dir: Path) -> Generator[Pact, Any, None]: consumer = Consumer("UserConsumer") pact = consumer.has_pact_with( Provider("UserProvider"), - pact_dir=pact_dir, + pact_dir=pacts_path, publish_to_broker=True, # Mock service configuration host_name=MOCK_URL.host, diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/v2/tests/test_01_provider_fastapi.py similarity index 94% rename from examples/tests/test_01_provider_fastapi.py rename to examples/v2/tests/test_01_provider_fastapi.py index 7855d342b..b997cc6ed 100644 --- a/examples/tests/test_01_provider_fastapi.py +++ b/examples/v2/tests/test_01_provider_fastapi.py @@ -35,9 +35,9 @@ from pydantic import BaseModel from yarl import URL -import examples.src.fastapi -from examples.src.fastapi import User, app -from pact import Verifier # type: ignore[import-untyped] +import examples.v2.src.fastapi +from examples.v2.src.fastapi import User, app +from pact.v2 import Verifier # type: ignore[import-untyped] if TYPE_CHECKING: from collections.abc import Generator @@ -109,8 +109,8 @@ def verifier() -> Generator[Verifier, Any, None]: def mock_user_123_doesnt_exist() -> None: """Mock the database for the user 123 doesn't exist state.""" - examples.src.fastapi.FAKE_DB = MagicMock() - examples.src.fastapi.FAKE_DB.get.return_value = None + examples.v2.src.fastapi.FAKE_DB = MagicMock() + examples.v2.src.fastapi.FAKE_DB.get.return_value = None def mock_user_123_exists() -> None: @@ -137,7 +137,7 @@ def mock_user_123_exists() -> None: hobbies=["hiking", "swimming"], admin=False, ) - examples.src.fastapi.FAKE_DB = mock_db + examples.v2.src.fastapi.FAKE_DB = mock_db def mock_post_request_to_create_user() -> None: @@ -156,7 +156,7 @@ def local_getitem(key: int) -> User: mock_db.__len__.return_value = 124 mock_db.__setitem__.side_effect = local_setitem mock_db.__getitem__.side_effect = local_getitem - examples.src.fastapi.FAKE_DB = mock_db + examples.v2.src.fastapi.FAKE_DB = mock_db def mock_delete_request_to_delete_user() -> None: @@ -193,7 +193,7 @@ def local_contains(key: int) -> bool: mock_db = MagicMock() mock_db.__delitem__.side_effect = local_delitem mock_db.__contains__.side_effect = local_contains - examples.src.fastapi.FAKE_DB = mock_db + examples.v2.src.fastapi.FAKE_DB = mock_db def test_against_broker(broker: URL, verifier: Verifier) -> None: diff --git a/examples/tests/test_01_provider_flask.py b/examples/v2/tests/test_01_provider_flask.py similarity index 94% rename from examples/tests/test_01_provider_flask.py rename to examples/v2/tests/test_01_provider_flask.py index 49e719f28..d42fb0997 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/v2/tests/test_01_provider_flask.py @@ -33,10 +33,10 @@ import pytest from yarl import URL -import examples.src.flask -from examples.src.flask import User, app +import examples.v2.src.flask +from examples.v2.src.flask import User, app from flask import request -from pact import Verifier # type: ignore[import-untyped] +from pact.v2 import Verifier # type: ignore[import-untyped] if TYPE_CHECKING: from collections.abc import Generator @@ -103,8 +103,8 @@ def verifier() -> Generator[Verifier, Any, None]: def mock_user_123_doesnt_exist() -> None: """Mock the database for the user 123 doesn't exist state.""" - examples.src.flask.FAKE_DB = MagicMock() - examples.src.flask.FAKE_DB.get.return_value = None + examples.v2.src.flask.FAKE_DB = MagicMock() + examples.v2.src.flask.FAKE_DB.get.return_value = None def mock_user_123_exists() -> None: @@ -121,8 +121,8 @@ def mock_user_123_exists() -> None: and removing fields) without fear of breaking the interactions with the consumers. """ - examples.src.flask.FAKE_DB = MagicMock() - examples.src.flask.FAKE_DB.get.return_value = User( + examples.v2.src.flask.FAKE_DB = MagicMock() + examples.v2.src.flask.FAKE_DB.get.return_value = User( id=123, name="Verna Hampton", email="verna@example.com", @@ -149,7 +149,7 @@ def local_getitem(key: int) -> User: mock_db.__len__.return_value = 124 mock_db.__setitem__.side_effect = local_setitem mock_db.__getitem__.side_effect = local_getitem - examples.src.flask.FAKE_DB = mock_db + examples.v2.src.flask.FAKE_DB = mock_db def mock_delete_request_to_delete_user() -> None: @@ -187,7 +187,7 @@ def local_contains(key: int) -> bool: mock_db.__delitem__.side_effect = local_delitem mock_db.__contains__.side_effect = local_contains mock_db.is_mocked = True - examples.src.flask.FAKE_DB = mock_db + examples.v2.src.flask.FAKE_DB = mock_db def test_against_broker(broker: URL, verifier: Verifier) -> None: diff --git a/examples/tests/test_02_message_consumer.py b/examples/v2/tests/test_02_message_consumer.py similarity index 95% rename from examples/tests/test_02_message_consumer.py rename to examples/v2/tests/test_02_message_consumer.py index 1498196c7..cd79253a7 100644 --- a/examples/tests/test_02_message_consumer.py +++ b/examples/v2/tests/test_02_message_consumer.py @@ -36,8 +36,8 @@ import pytest -from examples.src.message import Handler -from pact import MessageConsumer, MessagePact, Provider +from examples.v2.src.message import Handler +from pact.v2 import MessageConsumer, MessagePact, Provider if TYPE_CHECKING: from collections.abc import Generator @@ -49,7 +49,7 @@ @pytest.fixture(scope="module") -def pact(broker: URL, pact_dir: Path) -> Generator[MessagePact, Any, None]: +def pact(broker: URL, pacts_path: Path) -> Generator[MessagePact, Any, None]: """ Set up Message Pact Consumer. @@ -75,7 +75,7 @@ def pact(broker: URL, pact_dir: Path) -> Generator[MessagePact, Any, None]: consumer = MessageConsumer("MessageConsumer") pact = consumer.has_pact_with( Provider("MessageProvider"), - pact_dir=pact_dir, + pact_dir=pacts_path, publish_to_broker=True, # Broker configuration broker_base_url=str(broker), diff --git a/examples/tests/test_03_message_provider.py b/examples/v2/tests/test_03_message_provider.py similarity index 98% rename from examples/tests/test_03_message_provider.py rename to examples/v2/tests/test_03_message_provider.py index c657d5d2c..644b0ae5c 100644 --- a/examples/tests/test_03_message_provider.py +++ b/examples/v2/tests/test_03_message_provider.py @@ -29,7 +29,7 @@ from typing import TYPE_CHECKING from flask import Flask -from pact import MessageProvider +from pact.v2 import MessageProvider if TYPE_CHECKING: from yarl import URL diff --git a/examples/v3/plugins/__init__.py b/examples/v2/tests/v3/__init__.py similarity index 100% rename from examples/v3/plugins/__init__.py rename to examples/v2/tests/v3/__init__.py diff --git a/examples/tests/v3/test_00_consumer.py b/examples/v2/tests/v3/test_00_consumer.py similarity index 96% rename from examples/tests/v3/test_00_consumer.py rename to examples/v2/tests/v3/test_00_consumer.py index 7eeda0acd..06583f772 100644 --- a/examples/tests/v3/test_00_consumer.py +++ b/examples/v2/tests/v3/test_00_consumer.py @@ -26,12 +26,12 @@ import pytest import requests -from examples.src.consumer import UserConsumer -from pact.v3 import Pact, match +from examples.v2.src.consumer import UserConsumer +from pact import Pact, match @pytest.fixture -def pact() -> Generator[Pact, None, None]: +def pact(pacts_path: Path) -> Generator[Pact, None, None]: """ Set up the Pact fixture. @@ -47,10 +47,9 @@ def pact() -> Generator[Pact, None, None]: Yields: The Pact instance for the consumer tests. """ - pact_dir = Path(Path(__file__).parent.parent.parent / "pacts") pact = Pact("v3_http_consumer", "v3_http_provider") yield pact.with_specification("V4") - pact.write_file(pact_dir) + pact.write_file(pacts_path) def test_get_existing_user(pact: Pact) -> None: diff --git a/examples/tests/v3/test_01_fastapi_provider.py b/examples/v2/tests/v3/test_01_fastapi_provider.py similarity index 87% rename from examples/tests/v3/test_01_fastapi_provider.py rename to examples/v2/tests/v3/test_01_fastapi_provider.py index e8f4fc59e..d105b722a 100644 --- a/examples/tests/v3/test_01_fastapi_provider.py +++ b/examples/v2/tests/v3/test_01_fastapi_provider.py @@ -37,9 +37,9 @@ import uvicorn from yarl import URL -import examples.src.fastapi -from examples.src.fastapi import User -from pact.v3 import Verifier +import examples.v2.src.fastapi +from examples.v2.src.fastapi import User +from pact import Verifier if TYPE_CHECKING: from collections.abc import Generator @@ -86,12 +86,12 @@ def run_in_thread(self) -> Generator[str, None, None]: @pytest.fixture(scope="session") def server() -> Generator[str, None, None]: - server = Server(uvicorn.Config("examples.src.fastapi:app", host="localhost")) + server = Server(uvicorn.Config("examples.v2.src.fastapi:app", host="localhost")) with server.run_in_thread() as url: yield url -def test_provider(server: str) -> None: +def test_provider(server: str, pacts_path: Path) -> None: """ Test the FastAPI provider with Pact. @@ -138,7 +138,7 @@ def test_provider(server: str) -> None: verifier = ( Verifier("v3_http_provider") .add_transport(url=server) - .add_source("examples/pacts/v3_http_consumer-v3_http_provider.json") + .add_source(f"{pacts_path}/v3_http_consumer-v3_http_provider.json") .state_handler(provider_state_handler, teardown=True) ) verifier.verify() @@ -214,7 +214,7 @@ def mock_user_doesnt_exist() -> None: """ mock_db = MagicMock() mock_db.get.return_value = None - examples.src.fastapi.FAKE_DB = mock_db + examples.v2.src.fastapi.FAKE_DB = mock_db def mock_user_exists() -> None: @@ -241,7 +241,7 @@ def mock_user_exists() -> None: hobbies=["hiking", "swimming"], admin=False, ) - examples.src.fastapi.FAKE_DB = mock_db + examples.v2.src.fastapi.FAKE_DB = mock_db def mock_post_request_to_create_user() -> None: @@ -272,7 +272,7 @@ def local_getitem(key: int) -> User: mock_db.__len__.return_value = 124 mock_db.__setitem__.side_effect = local_setitem mock_db.__getitem__.side_effect = local_getitem - examples.src.fastapi.FAKE_DB = mock_db + examples.v2.src.fastapi.FAKE_DB = mock_db def mock_delete_request_to_delete_user() -> None: @@ -313,7 +313,7 @@ def local_contains(key: int) -> bool: mock_db = MagicMock() mock_db.__delitem__.side_effect = local_delitem mock_db.__contains__.side_effect = local_contains - examples.src.fastapi.FAKE_DB = mock_db + examples.v2.src.fastapi.FAKE_DB = mock_db def verify_user_doesnt_exist_mock() -> None: @@ -327,17 +327,17 @@ def verify_user_doesnt_exist_mock() -> None: if TYPE_CHECKING: # During setup, the `FAKE_DB` is replaced with a MagicMock object. # We need to inform the type checker that this has happened. - examples.src.fastapi.FAKE_DB = MagicMock() + examples.v2.src.fastapi.FAKE_DB = MagicMock() - assert len(examples.src.fastapi.FAKE_DB.mock_calls) == 1 + assert len(examples.v2.src.fastapi.FAKE_DB.mock_calls) == 1 - examples.src.fastapi.FAKE_DB.get.assert_called_once() - args, kwargs = examples.src.fastapi.FAKE_DB.get.call_args + examples.v2.src.fastapi.FAKE_DB.get.assert_called_once() + args, kwargs = examples.v2.src.fastapi.FAKE_DB.get.call_args assert len(args) == 1 assert isinstance(args[0], int) assert kwargs == {} - examples.src.fastapi.FAKE_DB.reset_mock() + examples.v2.src.fastapi.FAKE_DB.reset_mock() def verify_user_exists_mock() -> None: @@ -349,53 +349,53 @@ def verify_user_exists_mock() -> None: the integer argument `1`. It then resets the mock for future tests. """ if TYPE_CHECKING: - examples.src.fastapi.FAKE_DB = MagicMock() + examples.v2.src.fastapi.FAKE_DB = MagicMock() - assert len(examples.src.fastapi.FAKE_DB.mock_calls) == 1 + assert len(examples.v2.src.fastapi.FAKE_DB.mock_calls) == 1 - examples.src.fastapi.FAKE_DB.get.assert_called_once() - args, kwargs = examples.src.fastapi.FAKE_DB.get.call_args + examples.v2.src.fastapi.FAKE_DB.get.assert_called_once() + args, kwargs = examples.v2.src.fastapi.FAKE_DB.get.call_args assert len(args) == 1 assert isinstance(args[0], int) assert kwargs == {} - examples.src.fastapi.FAKE_DB.reset_mock() + examples.v2.src.fastapi.FAKE_DB.reset_mock() def verify_mock_post_request_to_create_user() -> None: if TYPE_CHECKING: - examples.src.fastapi.FAKE_DB = MagicMock() + examples.v2.src.fastapi.FAKE_DB = MagicMock() - assert len(examples.src.fastapi.FAKE_DB.mock_calls) == 3 + assert len(examples.v2.src.fastapi.FAKE_DB.mock_calls) == 3 - examples.src.fastapi.FAKE_DB.__getitem__.assert_called_once() - args, kwargs = examples.src.fastapi.FAKE_DB.__getitem__.call_args + examples.v2.src.fastapi.FAKE_DB.__getitem__.assert_called_once() + args, kwargs = examples.v2.src.fastapi.FAKE_DB.__getitem__.call_args assert len(args) == 1 assert isinstance(args[0], int) assert kwargs == {} - examples.src.fastapi.FAKE_DB.__len__.assert_called_once() - args, kwargs = examples.src.fastapi.FAKE_DB.__len__.call_args + examples.v2.src.fastapi.FAKE_DB.__len__.assert_called_once() + args, kwargs = examples.v2.src.fastapi.FAKE_DB.__len__.call_args assert len(args) == 0 assert kwargs == {} - examples.src.fastapi.FAKE_DB.reset_mock() + examples.v2.src.fastapi.FAKE_DB.reset_mock() def verify_mock_delete_request_to_delete_user() -> None: if TYPE_CHECKING: - examples.src.fastapi.FAKE_DB = MagicMock() + examples.v2.src.fastapi.FAKE_DB = MagicMock() - assert len(examples.src.fastapi.FAKE_DB.mock_calls) == 2 + assert len(examples.v2.src.fastapi.FAKE_DB.mock_calls) == 2 - examples.src.fastapi.FAKE_DB.__delitem__.assert_called_once() - args, kwargs = examples.src.fastapi.FAKE_DB.__delitem__.call_args + examples.v2.src.fastapi.FAKE_DB.__delitem__.assert_called_once() + args, kwargs = examples.v2.src.fastapi.FAKE_DB.__delitem__.call_args assert len(args) == 1 assert isinstance(args[0], int) assert kwargs == {} - examples.src.fastapi.FAKE_DB.__contains__.assert_called_once() - args, kwargs = examples.src.fastapi.FAKE_DB.__contains__.call_args + examples.v2.src.fastapi.FAKE_DB.__contains__.assert_called_once() + args, kwargs = examples.v2.src.fastapi.FAKE_DB.__contains__.call_args assert len(args) == 1 assert isinstance(args[0], int) assert kwargs == {} diff --git a/examples/tests/v3/test_02_message_consumer.py b/examples/v2/tests/v3/test_02_message_consumer.py similarity index 95% rename from examples/tests/v3/test_02_message_consumer.py rename to examples/v2/tests/v3/test_02_message_consumer.py index a3d8b9628..6e5bd1dcf 100644 --- a/examples/tests/v3/test_02_message_consumer.py +++ b/examples/v2/tests/v3/test_02_message_consumer.py @@ -18,8 +18,8 @@ import pytest -from examples.src.message import Handler -from pact.v3.pact import Pact +from examples.v2.src.message import Handler +from pact.pact import Pact if TYPE_CHECKING: from collections.abc import Callable, Generator @@ -29,7 +29,7 @@ @pytest.fixture(scope="module") -def pact() -> Generator[Pact, None, None]: +def pact(pacts_path: Path) -> Generator[Pact, None, None]: """ Set up Message Pact Consumer. @@ -63,11 +63,10 @@ def pact() -> Generator[Pact, None, None]: ``` """ - pact_dir = Path(Path(__file__).parent.parent.parent / "pacts") pact = Pact("v3_message_consumer", "v3_message_provider") log.info("Creating Message Pact with V3 specification") yield pact.with_specification("V3") - pact.write_file(pact_dir, overwrite=True) + pact.write_file(pacts_path, overwrite=True) @pytest.fixture diff --git a/examples/tests/v3/test_03_message_provider.py b/examples/v2/tests/v3/test_03_message_provider.py similarity index 92% rename from examples/tests/v3/test_03_message_provider.py rename to examples/v2/tests/v3/test_03_message_provider.py index a6462947c..3d0abcb4d 100644 --- a/examples/tests/v3/test_03_message_provider.py +++ b/examples/v2/tests/v3/test_03_message_provider.py @@ -12,9 +12,9 @@ from typing import Any from unittest.mock import MagicMock -from examples.src.message_producer import FileSystemMessageProducer -from pact.v3 import Verifier -from pact.v3.types import Message +from examples.v2.src.message_producer import FileSystemMessageProducer +from pact import Verifier +from pact.types import Message PACT_DIR = (Path(__file__).parent.parent.parent / "pacts").resolve() diff --git a/pyproject.toml b/pyproject.toml index b728a67e1..9b9d4fc0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ dependencies = [ "Repository" = "https://github.com/pact-foundation/pact-python" [project.scripts] - pact-verifier = "pact.cli.verify:main" + pact-verifier = "pact.v2.cli.verify:main" [project.optional-dependencies] # Linting and formatting tools use a more narrow specification to ensure @@ -218,7 +218,7 @@ requires = ["hatch-vcs", "hatchling"] "--cov=pact", # Xdist options "--dist=worksteal", - "--numprocesses=logical", + "--numprocesses=2", # Rerun options "--rerun-except=assert", "--reruns=3", @@ -228,9 +228,6 @@ requires = ["hatch-vcs", "hatchling"] "ignore::DeprecationWarning:examples", "ignore::DeprecationWarning:pact", "ignore::DeprecationWarning:tests", - "ignore::PendingDeprecationWarning:examples", - "ignore::PendingDeprecationWarning:pact", - "ignore::PendingDeprecationWarning:tests", ] pythonpath = "." @@ -270,42 +267,9 @@ requires = ["hatch-vcs", "hatchling"] ################################################################################ [tool.ruff] -# TODO: Remove the explicity extend-exclude once astral-sh/ruff#6262 is fixed. +# TODO: Don't check v2 files # https://github.com/pact-foundation/pact-python/issues/458 -extend-exclude = [ - # "src/pact/*.py", - # "src/pact/cli/*.py", - # "src/tests/*.py", - # "src/tests/cli/*.py", - "src/pact/__init__.py", - "src/pact/__version__.py", - "src/pact/broker.py", - "src/pact/cli/*.py", - "src/pact/consumer.py", - "src/pact/http_proxy.py", - "src/pact/matchers.py", - "src/pact/message_consumer.py", - "src/pact/message_pact.py", - "src/pact/message_provider.py", - "src/pact/pact.py", - "src/pact/provider.py", - "src/pact/verifier.py", - "src/pact/verify_wrapper.py", - "tests/__init__.py", - "tests/cli/*.py", - "tests/conftest.py", - "tests/test_broker.py", - "tests/test_constants.py", - "tests/test_consumer.py", - "tests/test_http_proxy.py", - "tests/test_matchers.py", - "tests/test_message_consumer.py", - "tests/test_message_pact.py", - "tests/test_message_provider.py", - "tests/test_pact.py", - "tests/test_verifier.py", - "tests/test_verify_wrapper.py", -] +extend-exclude = ["src/pact/v2/*", "tests/v2/*", "examples/v2/*"] [tool.ruff.lint] select = ["ALL"] @@ -342,7 +306,9 @@ extend-exclude = [ ## Mypy Configuration ################################################################################ [tool.mypy] -exclude = '^(src/pact|tests|examples|examples/tests)/(?!v3).+\.py$' +exclude = """(?x)^( + (src/pact|tests|examples)/v2/.*\\.pyi? +)$""" ################################################################################ ## CI Build Wheel @@ -355,7 +321,7 @@ exclude = '^(src/pact|tests|examples|examples/tests)/(?!v3).+\.py$' [tool.typos] [tool.typos.default] - extend-ignore-re = ["(?Rm)^.*(#|//| +1 August 2025 +: With the release of Pact Python `v3`, some hyperlinks have been removed from this blog post as they are no longer relevant. + diff --git a/docs/blog/posts/2024/07-26 asynchronous message support.md b/docs/blog/posts/2024/07-26 asynchronous message support.md index f3ad93665..bdb982735 100644 --- a/docs/blog/posts/2024/07-26 asynchronous message support.md +++ b/docs/blog/posts/2024/07-26 asynchronous message support.md @@ -7,7 +7,7 @@ date: # Asynchronous Message Support -We are excited to announce that support for verifying asynchronous message interactions has been added in the recent [release of Pact Python version 2.2.1](https://github.com/pact-foundation/pact-python/releases/tag/v2.2.1). To explore this feature, use the [`pact.v3`][pact.v3] module. A huge shoutout goes to [Val Kolovos](https://github.com/valkolovos) who contributed this feature across two very large PRs ([#714](https://github.com/pact-foundation/pact-python/pull/714) and [#725](https://github.com/pact-foundation/pact-python/pull/725)). This represents a significant step forward in the capabilities of Pact Python and on the road to full support for the Pact specification. +We are excited to announce that support for verifying asynchronous message interactions has been added in the recent [release of Pact Python version 2.2.1](https://github.com/pact-foundation/pact-python/releases/tag/v2.2.1). To explore this feature, use the [`pact.v3`][pact] module. A huge shoutout goes to [Val Kolovos](https://github.com/valkolovos) who contributed this feature across two very large PRs ([#714](https://github.com/pact-foundation/pact-python/pull/714) and [#725](https://github.com/pact-foundation/pact-python/pull/725)). This represents a significant step forward in the capabilities of Pact Python and on the road to full support for the Pact specification. Asynchronous messages play a crucial role in building resilient and scalable systems. They allow services to communicate with each other without blocking, which can be particularly useful when the sender and receiver are not always available at the same time. However, verifying these interactions is challenging due to the wide variety of messaging systems and protocols. @@ -71,7 +71,7 @@ Here’s an example of a Pact test for this consumer: ```python import json -from pact.v3 import Pact +from pact import Pact from my_consumer import process_message @@ -112,7 +112,7 @@ For context of asynchronous messages, the provider is the service that sends the As the underlying protocol is abstracted away, Pact uses a local HTTP server to receive the messages that the provider sends. The provider test for the above consumer might look something like this: ```python -from pact.v3 import Verifier +from pact import Verifier class Provider: """ @@ -183,3 +183,10 @@ At present, it is the responsibility of the end user to set up the provider endp ``` Some queueuing systems allow for metadata to be attached to messages and may be required as part of the Pact. If that is the case, the metadata generated by the provider can be passed through the `Pact-Message-Metadata` header as a base-64 encoded string of the underlying JSON object. + +--- + + +1 August 2025 +: With the release of Pact Python `v3` and the splitting of the CLI and FFI into standalone packages, some hyperlinks and code snippets have been updated to point to the new locations. The _text_ has been kept unchanged to preserve the original context and intent of the post. + diff --git a/docs/blog/posts/2024/12-30 functional arguments.md b/docs/blog/posts/2024/12-30 functional arguments.md index 3949e24cc..2a1925740 100644 --- a/docs/blog/posts/2024/12-30 functional arguments.md +++ b/docs/blog/posts/2024/12-30 functional arguments.md @@ -85,7 +85,7 @@ The new `state_handler` method replaces the `set_state` method and simplifies th ???+ example ```python - from pact.v3 import Verifier + from pact import Verifier def provider_state_callback( name: str, # (1) @@ -123,7 +123,7 @@ The new `state_handler` method replaces the `set_state` method and simplifies th 3. The `parameters` parameter is a dictionary of additional parameters that the provider state requires. For example, instead of `"a user with ID 123 exists"`, the provider state might be `"a user with the given ID exists"` and the specific ID would be passed in the `parameters` dictionary. Note that `parameters` is always present, but may be `None` if no parameters are specified by the consumer. -The function arguments must include the relevant keys from the [`StateHandlerArgs`][pact.v3.types.StateHandlerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments. +The function arguments must include the relevant keys from the [`StateHandlerArgs`][pact.types.StateHandlerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments. This snippet showcases a way to set up the provider state with a function that is fully parameterized. The `state_handler` method also handles the following scenarios: @@ -133,7 +133,7 @@ This snippet showcases a way to set up the provider state with a function that i ??? example ```python - from pact.v3 import Verifier + from pact import Verifier def provider_state_callback( name: str, @@ -153,7 +153,7 @@ This snippet showcases a way to set up the provider state with a function that i ??? example ```python - from pact.v3 import Verifier + from pact import Verifier def user_state_callback( action: Literal["setup", "teardown"], @@ -184,7 +184,7 @@ This snippet showcases a way to set up the provider state with a function that i ??? example ```python - from pact.v3 import Verifier + from pact import Verifier def user_state_callback( parameters: dict[str, Any] | None, @@ -217,8 +217,8 @@ With the update to 2.3.0, the `Verifier` class has a new `message_handler` metho ???+ example ```python - from pact.v3 import Verifier - from pact.v3.types import Message + from pact import Verifier + from pact.types import Message def message_producer_callback( name: str, # (1) @@ -247,7 +247,7 @@ With the update to 2.3.0, the `Verifier` class has a new `message_handler` metho 1. The `name` parameter is the name of the message. For example, `"request to delete a user"`. If you instead use a mapping of message names to functions, this parameter is not passed to the function. 2. The `params` parameter is a dictionary of additional parameters that the message requires. For example, one could specify the user ID to delete in the parameters instead of the message. Note that `params` is always present, but may be `None` if no parameters are specified by the consumer. -The function arguments must include the relevant keys from the [`MessageProducerArgs`][pact.v3.types.MessageProducerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments. +The function arguments must include the relevant keys from the [`MessageProducerArgs`][pact.types.MessageProducerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments. The output of the callback function should be an instance of the `Message` type. This is a simple [TypedDict][typing.TypedDict] that represents the message that the consumer expects and can be specified as a simple dictionary, or with typing hints through the `Message` constructor: @@ -255,7 +255,7 @@ The output of the callback function should be an instance of the `Message` type. === "With typing hints" ```python - from pact.v3.types import Message + from pact.types import Message def message_producer_callback( name: str, @@ -296,8 +296,8 @@ In much the same way as the `state_handler` method, the `message_handler` method ???+ example ```python - from pact.v3 import Verifier - from pact.v3.types import Message + from pact import Verifier + from pact.types import Message def delete_user_message(metadata: dict[str, Any] | None) -> Message: ... @@ -323,5 +323,10 @@ In much the same way as the `state_handler` method, the `message_handler` method 28 March 2025 : This blog post was updated on 28 March 2025 to reflect changes to the way functional arguments are handled. Instead of requiring positional arguments, Pact Python now inspects the function signature in order to determine whether to pass the arguments as positional or keyword arguments. It will fallback to passing the arguments through variadic arguments (`*args` and `**kwargs`) if present. This was done specific to allow for functions with optional arguments. - For this added flexibility, the function signatures must have parameters that align with the [`StateHandlerArgs`][pact.v3.types.StateHandlerArgs] and [`MessageProducerArgs`][pact.v3.types.MessageProducerArgs] typed dictionaries. This allows Pact Python to match a `parameters=...` argument with the `parameters` key in the dictionary. Using an alternative name (e.g., `params`) will not work. + For this added flexibility, the function signatures must have parameters that align with the [`StateHandlerArgs`][pact.types.StateHandlerArgs] and [`MessageProducerArgs`][pact.types.MessageProducerArgs] typed dictionaries. This allows Pact Python to match a `parameters=...` argument with the `parameters` key in the dictionary. Using an alternative name (e.g., `params`) will not work. + +1 August 2025 +: With the release of Pact Python `v3` and the splitting of the CLI and FFI into standalone packages, some hyperlinks and code snippets have been updated to point to the new locations. The _text_ has been kept unchanged to preserve the original context and intent of the post. + + From 4c4e35c4c5c86400df3e689276968b9e078859d9 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 1 Aug 2025 18:36:39 +1000 Subject: [PATCH 0909/1376] docs: fix v3 references Signed-off-by: JP-Ellis --- src/pact/error.py | 4 +- src/pact/generate/__init__.py | 78 +++++++------ src/pact/interaction/__init__.py | 2 +- .../interaction/_async_message_interaction.py | 4 +- src/pact/interaction/_base.py | 12 +- src/pact/interaction/_http_interaction.py | 32 +++--- .../interaction/_sync_message_interaction.py | 10 +- src/pact/match/__init__.py | 104 +++++++++++------- src/pact/pact.py | 12 +- src/pact/types.py | 2 +- src/pact/verifier.py | 10 +- tests/test_server.py | 2 +- tests/test_util.py | 2 +- tests/test_verifier.py | 2 +- 14 files changed, 158 insertions(+), 118 deletions(-) diff --git a/src/pact/error.py b/src/pact/error.py index 5db830b39..59ad49cd8 100644 --- a/src/pact/error.py +++ b/src/pact/error.py @@ -63,7 +63,7 @@ class PactVerificationError(PactError): Exception raised due to errors in the verification of a Pact. This is raised when performing manual verification of the Pact through the - [`verify`][pact.v3.Pact.verify] method: + [`verify`][pact.Pact.verify] method: ```python pact = Pact("consumer", "provider") @@ -77,7 +77,7 @@ class PactVerificationError(PactError): All of the errors that occurred during the verification of all of the interactions are stored in the `errors` attribute. - This is different from the [`MismatchesError`][pact.v3.MismatchesError] + This is different from the [`MismatchesError`][pact.error.MismatchesError] which is raised when there are mismatches detected by the mock server. """ diff --git a/src/pact/generate/__init__.py b/src/pact/generate/__init__.py index 26a612c9b..ee8721c60 100644 --- a/src/pact/generate/__init__.py +++ b/src/pact/generate/__init__.py @@ -72,7 +72,7 @@ def __import__( # noqa: N807 warn users when they import functions directly from this module. This is done to avoid shadowing built-in types and functions. """ - if name == "pact.v3.generate" and len(set(fromlist) - {"Matcher"}) > 0: + if name == "pact.generate" and len(set(fromlist) - {"Matcher"}) > 0: warnings.warn( "Avoid `from pact.generate import `. " "Prefer importing `generate` and use `generate.`", @@ -95,6 +95,7 @@ def int( Args: min: The minimum value for the integer. + max: The maximum value for the integer. """ @@ -112,7 +113,7 @@ def integer( max: builtins.int | None = None, ) -> Generator: """ - Alias for [`generate.int`][pact.v3.generate.int]. + Alias for [`generate.int`][pact.generate.int]. """ return int(min=min, max=max) @@ -137,7 +138,7 @@ def float(precision: builtins.int | None = None) -> Generator: def decimal(precision: builtins.int | None = None) -> Generator: """ - Alias for [`generate.float`][pact.v3.generate.float]. + Alias for [`generate.float`][pact.generate.float]. """ return float(precision=precision) @@ -158,7 +159,7 @@ def hex(digits: builtins.int | None = None) -> Generator: def hexadecimal(digits: builtins.int | None = None) -> Generator: """ - Alias for [`generate.hex`][pact.v3.generate.hex]. + Alias for [`generate.hex`][pact.generate.hex]. """ return hex(digits=digits) @@ -179,7 +180,7 @@ def str(size: builtins.int | None = None) -> Generator: def string(size: builtins.int | None = None) -> Generator: """ - Alias for [`generate.str`][pact.v3.generate.str]. + Alias for [`generate.str`][pact.generate.str]. """ return str(size=size) @@ -227,18 +228,21 @@ def date( """ Create a date generator. + !!! info + + Pact internally uses the Java's + [`SimpleDateFormat`](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html). + To ensure compatibility with the rest of the Python ecosystem, this + function accepts Python's [`strftime`](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior) + format and performs the conversion to Java's format internally. + + Args: format: - Expected format of the date. This uses Python's [`strftime` - format](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) - - Pact internally uses the [Java - SimpleDateFormat](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html) - and the conversion from Python's `strftime` format to Java's - `SimpleDateFormat` format is done in - [`strftime_to_simple_date_format`][pact.v3.util.strftime_to_simple_date_format]. + Expected format of the date. If not provided, an ISO 8601 date format is used: `%Y-%m-%d`. + disable_conversion: If True, the conversion from Python's `strftime` format to Java's `SimpleDateFormat` format will be disabled, and the format must be @@ -258,23 +262,26 @@ def time( """ Create a time generator. + !!! info + + Pact internally uses the Java's + [`SimpleDateFormat`](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html). + To ensure compatibility with the rest of the Python ecosystem, this + function accepts Python's + [`strftime`](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior) + format and performs the conversion to Java's format internally. + Args: format: - Expected format of the time. This uses Python's [`strftime` - format](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) - - Pact internally uses the [Java - SimpleDateFormat](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html) - and the conversion from Python's `strftime` format to Java's - `SimpleDateFormat` format is done in - [`strftime_to_simple_date_format`][pact.v3.util.strftime_to_simple_date_format]. + Expected format of the time. If not provided, an ISO 8601 time format will be used: `%H:%M:%S`. + disable_conversion: If True, the conversion from Python's `strftime` format to Java's `SimpleDateFormat` format will be disabled, and the format must be - in Java's `SimpleDateFormat` format. As a result, the value must - be a string as Python cannot format the time in the target format. + in Java's `SimpleDateFormat` format. As a result, the value must be + a string as Python cannot format the time in the target format. """ if not disable_conversion: format = strftime_to_simple_date_format(format or "%H:%M:%S") @@ -287,21 +294,24 @@ def datetime( disable_conversion: builtins.bool = False, ) -> Generator: """ - Create a date-time generator. + Create a datetime generator. + + !!! info + + Pact internally uses the Java's + [`SimpleDateFormat`](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html). + To ensure compatibility with the rest of the Python ecosystem, this + function accepts Python's + [`strftime`](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior) + format and performs the conversion to Java's format internally. Args: format: - Expected format of the timestamp. This uses Python's [`strftime` - format](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) - - Pact internally uses the [Java - SimpleDateFormat](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html) - and the conversion from Python's `strftime` format to Java's - `SimpleDateFormat` format is done in - [`strftime_to_simple_date_format`][pact.v3.util.strftime_to_simple_date_format]. + Expected format of the timestamp. If not provided, an ISO 8601 timestamp format will be used: `%Y-%m-%dT%H:%M:%S%z`. + disable_conversion: If True, the conversion from Python's `strftime` format to Java's `SimpleDateFormat` format will be disabled, and the format must be @@ -318,7 +328,7 @@ def timestamp( disable_conversion: builtins.bool = False, ) -> Generator: """ - Alias for [`generate.datetime`][pact.v3.generate.datetime]. + Alias for [`generate.datetime`][pact.generate.datetime]. """ return datetime(format=format, disable_conversion=disable_conversion) @@ -332,7 +342,7 @@ def bool() -> Generator: def boolean() -> Generator: """ - Alias for [`generate.bool`][pact.v3.generate.bool]. + Alias for [`generate.bool`][pact.generate.bool]. """ return bool() diff --git a/src/pact/interaction/__init__.py b/src/pact/interaction/__init__.py index 455bed490..5d93fa664 100644 --- a/src/pact/interaction/__init__.py +++ b/src/pact/interaction/__init__.py @@ -2,7 +2,7 @@ Interaction module. This module defines the classes that are used to define individual interactions -within a [`Pact`][pact.v3.pact.Pact] between a consumer and a provider. These +within a [`Pact`][pact.pact.Pact] between a consumer and a provider. These interactions can be of different types, such as HTTP requests, synchronous messages, or asynchronous messages. diff --git a/src/pact/interaction/_async_message_interaction.py b/src/pact/interaction/_async_message_interaction.py index 5795cc8fe..ac6048e45 100644 --- a/src/pact/interaction/_async_message_interaction.py +++ b/src/pact/interaction/_async_message_interaction.py @@ -28,8 +28,8 @@ def __init__(self, pact_handle: pact_ffi.PactHandle, description: str) -> None: This function should not be called directly. Instead, an AsyncMessageInteraction should be created using the - [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a - [`Pact`][pact.v3.Pact] instance using the `"Async"` interaction type. + [`upon_receiving(...)`][pact.Pact.upon_receiving] method of a + [`Pact`][pact.Pact] instance using the `"Async"` interaction type. Args: pact_handle: diff --git a/src/pact/interaction/_base.py b/src/pact/interaction/_base.py index 9245f7371..d1a150da6 100644 --- a/src/pact/interaction/_base.py +++ b/src/pact/interaction/_base.py @@ -6,7 +6,7 @@ and provides the functionality to verify that the interactions are satisfied. For the roles of consumer and provider, see the documentation for the -`pact.v3.service` module. +`pact.service` module. """ from __future__ import annotations @@ -37,9 +37,9 @@ class Interaction(abc.ABC): provider. The concrete subclasses define the type of interaction, and include: - - [`HttpInteraction`][pact.v3.interaction.HttpInteraction] - - [`AsyncMessageInteraction`][pact.v3.interaction.AsyncMessageInteraction] - - [`SyncMessageInteraction`][pact.v3.interaction.SyncMessageInteraction] + - [`HttpInteraction`][pact.interaction.HttpInteraction] + - [`AsyncMessageInteraction`][pact.interaction.AsyncMessageInteraction] + - [`SyncMessageInteraction`][pact.interaction.SyncMessageInteraction] # Interaction Part @@ -293,7 +293,7 @@ def with_binary_body( Note that for HTTP interactions, this function will overwrite the body if it has been set using - [`with_body(...)`][pact.v3.interaction.Interaction.with_body]. + [`with_body(...)`][pact.interaction.Interaction.with_body]. Args: part: @@ -470,7 +470,7 @@ def add_text_comment(self, comment: str) -> Self: Internally, the comments are appended to an array under the `text` comment key. Care should be taken to ensure that conflicts are not introduced by - [`set_comment`][pact.v3.interaction.Interaction.set_comment]. + [`set_comment`][pact.interaction.Interaction.set_comment]. """ pact_ffi.add_text_comment(self._handle, comment) return self diff --git a/src/pact/interaction/_http_interaction.py b/src/pact/interaction/_http_interaction.py index 2a7e4441d..e0b47828c 100644 --- a/src/pact/interaction/_http_interaction.py +++ b/src/pact/interaction/_http_interaction.py @@ -35,7 +35,7 @@ class HttpInteraction(Interaction): response, this class provides a common interface for both. The functions intelligently determine whether the element should be added to the request or the response based on whether - [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] + [`will_respond_with(...)`][pact.interaction.HttpInteraction.will_respond_with] has been called. For example, the following two interactions are equivalent: @@ -67,8 +67,8 @@ def __init__(self, pact_handle: pact_ffi.PactHandle, description: str) -> None: This class should not be instantiated directly. Instead, an `HttpInteraction` should be created using the - [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a - [`Pact`][pact.v3.Pact] instance. + [`upon_receiving(...)`][pact.Pact.upon_receiving] method of a + [`Pact`][pact.Pact] instance. """ super().__init__(description) self.__handle = pact_ffi.new_interaction(pact_handle, description) @@ -195,7 +195,7 @@ def with_header( If the value of the header is expected to be a JSON object and clashes with the above syntax, then it is recommended to make use of the - [`set_header(...)`][pact.v3.interaction.HttpInteraction.set_header] + [`set_header(...)`][pact.interaction.HttpInteraction.set_header] method instead. Args: @@ -210,7 +210,7 @@ def with_header( response. If `None`, then the function intelligently determines whether the header should be added to the request or the response, based on whether the - [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] + [`will_respond_with(...)`][pact.interaction.HttpInteraction.will_respond_with] method has been called. """ interaction_part = self._parse_interaction_part(part) @@ -247,9 +247,9 @@ def with_headers( dictionary view. - Make multiple calls to - [`with_header(...)`][pact.v3.interaction.HttpInteraction.with_header] + [`with_header(...)`][pact.interaction.HttpInteraction.with_header] or - [`with_headers(...)`][pact.v3.interaction.HttpInteraction.with_headers]. + [`with_headers(...)`][pact.interaction.HttpInteraction.with_headers]. - Specify the multiple values in a JSON object of the form: @@ -263,7 +263,7 @@ def with_headers( ``` See - [`with_header(...)`][pact.v3.interaction.HttpInteraction.with_header] + [`with_header(...)`][pact.interaction.HttpInteraction.with_header] for more information. Args: @@ -275,7 +275,7 @@ def with_headers( response. If `None`, then the function intelligently determines whether the header should be added to the request or the response, based on whether the - [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] + [`will_respond_with(...)`][pact.interaction.HttpInteraction.will_respond_with] method has been called. """ if isinstance(headers, dict): @@ -294,7 +294,7 @@ def set_header( Add a header to the request. Unlike - [`with_header(...)`][pact.v3.interaction.HttpInteraction.with_header], + [`with_header(...)`][pact.interaction.HttpInteraction.with_header], this function does no additional processing of the header value. This is useful for headers that contain a JSON object. @@ -310,7 +310,7 @@ def set_header( response. If `None`, then the function intelligently determines whether the header should be added to the request or the response, based on whether the - [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] + [`will_respond_with(...)`][pact.interaction.HttpInteraction.will_respond_with] method has been called. """ pact_ffi.set_header( @@ -331,10 +331,10 @@ def set_headers( This function intelligently determines whether the header should be added to the request or the response, based on whether the - [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] + [`will_respond_with(...)`][pact.interaction.HttpInteraction.will_respond_with] method has been called. - See [`set_header(...)`][pact.v3.interaction.HttpInteraction.set_header] for + See [`set_header(...)`][pact.interaction.HttpInteraction.set_header] for more information. Args: @@ -346,7 +346,7 @@ def set_headers( response. If `None`, then the function intelligently determines whether the header should be added to the request or the response, based on whether the - [`will_respond_with(...)`][pact.v3.interaction.HttpInteraction.will_respond_with] + [`will_respond_with(...)`][pact.interaction.HttpInteraction.will_respond_with] method has been called. """ if isinstance(headers, dict): @@ -439,7 +439,7 @@ def with_query_parameters( Add multiple query parameters to the request. See - [`with_query_parameter(...)`][pact.v3.interaction.HttpInteraction.with_query_parameter] + [`with_query_parameter(...)`][pact.interaction.HttpInteraction.with_query_parameter] for more information. Args: @@ -458,7 +458,7 @@ def will_respond_with(self, status: int) -> Self: Ideally, this function is called once all of the request information has been set. This allows functions such as - [`with_header(...)`][pact.v3.interaction.HttpInteraction.with_header] + [`with_header(...)`][pact.interaction.HttpInteraction.with_header] to intelligently determine whether this is a request or response header. Alternatively, the `part` argument can be used to explicitly specify diff --git a/src/pact/interaction/_sync_message_interaction.py b/src/pact/interaction/_sync_message_interaction.py index cd9b9047e..40ac22373 100644 --- a/src/pact/interaction/_sync_message_interaction.py +++ b/src/pact/interaction/_sync_message_interaction.py @@ -15,7 +15,7 @@ class SyncMessageInteraction(Interaction): A synchronous message interaction. This class defines a synchronous message interaction between a consumer and - a provider. As with [`HttpInteraction`][pact.v3.pact.HttpInteraction], it + a provider. As with [`HttpInteraction`][pact.pact.HttpInteraction], it defines a specific request that the consumer makes to the provider, and the response that the provider should return. @@ -30,8 +30,8 @@ def __init__(self, pact_handle: pact_ffi.PactHandle, description: str) -> None: This function should not be called directly. Instead, an AsyncMessageInteraction should be created using the - [`upon_receiving(...)`][pact.v3.Pact.upon_receiving] method of a - [`Pact`][pact.v3.Pact] instance using the `"Sync"` interaction type. + [`upon_receiving(...)`][pact.Pact.upon_receiving] method of a + [`Pact`][pact.Pact] instance using the `"Sync"` interaction type. Args: pact_handle: @@ -68,8 +68,8 @@ def will_respond_with(self) -> Self: This method is a convenience method to separate the request and response parts of the interaction. This function is analogous to the - [`will_respond_with()`][pact.v3.pact.HttpInteraction.will_respond_with] - method of the [`HttpInteraction`][pact.v3.pact.HttpInteraction] class, + [`will_respond_with()`][pact.pact.HttpInteraction.will_respond_with] + method of the [`HttpInteraction`][pact.pact.HttpInteraction] class, albeit more generic for synchronous message interactions. For example, the following two snippets are diff --git a/src/pact/match/__init__.py b/src/pact/match/__init__.py index 4971c0481..92ab241d0 100644 --- a/src/pact/match/__init__.py +++ b/src/pact/match/__init__.py @@ -126,11 +126,11 @@ def __import__( # noqa: N807 """ Override to warn when importing functions directly from this module. - This function is used to override the built-in `__import__` function to - warn users when they import functions directly from this module. This is - done to avoid shadowing built-in types and functions. + This function is used to override the built-in `__import__` function to warn + users when they import functions directly from this module. This is done to + avoid shadowing built-in types and functions. """ - if name == "pact.v3.match" and len(set(fromlist) - {"Matcher"}) > 0: + if name == "pact.match" and len(set(fromlist) - {"Matcher"}) > 0: warnings.warn( "Avoid `from pact.match import `. " "Prefer importing `match` and use `match.`", @@ -155,8 +155,10 @@ def int( Args: value: Default value to use when generating a consumer test. + min: If provided, the minimum value of the integer to generate. + max: If provided, the maximum value of the integer to generate. """ @@ -179,7 +181,7 @@ def integer( max: builtins.int | None = None, ) -> Matcher[builtins.int]: """ - Alias for [`match.int`][pact.v3.match.int]. + Alias for [`match.int`][pact.match.int]. """ return int(value, min=min, max=max) @@ -199,6 +201,7 @@ def float( Args: value: Default value to use when generating a consumer test. + precision: The number of decimal precision to generate. """ @@ -220,7 +223,7 @@ def decimal( precision: builtins.int | None = None, ) -> Matcher[_NumberT]: """ - Alias for [`match.float`][pact.v3.match.float]. + Alias for [`match.float`][pact.match.float]. """ return float(value, precision=precision) @@ -263,19 +266,22 @@ def number( """ Match a general number. - This matcher is a generalization of the [`integer`][pact.v3.match.integer] - and [`decimal`][pact.v3.match.decimal] matchers. It can be used to match any + This matcher is a generalization of the [`integer`][pact.match.integer] + and [`decimal`][pact.match.decimal] matchers. It can be used to match any number, whether it is an integer or a float. Args: value: Default value to use when generating a consumer test. + min: The minimum value of the number to generate. Only used when value is an integer. Defaults to None. + max: The maximum value of the number to generate. Only used when value is an integer. Defaults to None. + precision: The number of decimal digits to generate. Only used when value is a float. Defaults to None. @@ -343,8 +349,10 @@ def str( Args: value: Default value to use when generating a consumer test. + size: The size of the string to generate during a consumer test. + generator: Alternative generator to use when generating a consumer test. If set, the `size` argument is ignored. @@ -380,7 +388,7 @@ def string( generator: Generator | None = None, ) -> Matcher[builtins.str]: """ - Alias for [`match.str`][pact.v3.match.str]. + Alias for [`match.str`][pact.match.str]. """ return str(value, size=size, generator=generator) @@ -397,6 +405,7 @@ def regex( Args: value: Default value to use when generating a consumer test. + regex: The regular expression to match against. """ @@ -434,7 +443,7 @@ def uuid( """ Match a UUID value. - This matcher internally combines the [`regex`][pact.v3.match.regex] matcher + This matcher internally combines the [`regex`][pact.match.regex] matcher with a UUID regex pattern. See [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) for details about the UUID format. @@ -446,6 +455,7 @@ def uuid( Args: value: Default value to use when generating a consumer test. + format: Enforce a specific UUID format. The following formats are supported: @@ -491,7 +501,7 @@ def bool(value: builtins.bool | Unset = UNSET, /) -> Matcher[builtins.bool]: def boolean(value: builtins.bool | Unset = UNSET, /) -> Matcher[builtins.bool]: """ - Alias for [`match.bool`][pact.v3.match.bool]. + Alias for [`match.bool`][pact.match.bool]. """ return bool(value) @@ -509,20 +519,24 @@ def date( A date value is a string that represents a date in a specific format. It does _not_ have any time information. + !!! info + + Pact internally uses the Java's + [`SimpleDateFormat`](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html). + To ensure compatibility with the rest of the Python ecosystem, this + function accepts Python's + [`strftime`](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior) + format and performs the conversion to Java's format internally. + Args: value: Default value to use when generating a consumer test. - format: - Expected format of the date. This uses Python's [`strftime` - format](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) - Pact internally uses the [Java - SimpleDateFormat](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html) - and the conversion from Python's `strftime` format to Java's - `SimpleDateFormat` format is done in - [`strftime_to_simple_date_format`][pact.v3.util.strftime_to_simple_date_format]. + format: + Expected format of the date. If not provided, an ISO 8601 date format will be used: `%Y-%m-%d`. + disable_conversion: If True, the conversion from Python's `strftime` format to Java's `SimpleDateFormat` format will be disabled, and the format must be @@ -577,20 +591,23 @@ def time( A time value is a string that represents a time in a specific format. It does _not_ have any date information. + !!! info + + Pact internally uses the Java's + [`SimpleDateFormat`](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html). + To ensure compatibility with the rest of the Python ecosystem, this + function accepts Python's [`strftime`](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior) + format and performs the conversion to Java's format internally. + Args: value: Default value to use when generating a consumer test. - format: - Expected format of the time. This uses Python's [`strftime` - format](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) - Pact internally uses the [Java - SimpleDateFormat](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html) - and the conversion from Python's `strftime` format to Java's - `SimpleDateFormat` format is done in - [`strftime_to_simple_date_format`][pact.v3.util.strftime_to_simple_date_format]. + format: + Expected format of the time. If not provided, an ISO 8601 time format will be used: `%H:%M:%S`. + disable_conversion: If True, the conversion from Python's `strftime` format to Java's `SimpleDateFormat` format will be disabled, and the format must be @@ -643,21 +660,26 @@ def datetime( A timestamp value is a string that represents a date and time in a specific format. + !!! info + + Pact internally uses the Java's + [`SimpleDateFormat`](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html). + To ensure compatibility with the rest of the Python ecosystem, this + function accepts Python's + [`strftime`](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior) + format and performs the conversion to Java's format internally. + Args: value: Default value to use when generating a consumer test. + format: - Expected format of the timestamp. This uses Python's [`strftime` + Expected format of the timestamp. format](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) - Pact internally uses the [Java - SimpleDateFormat](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html) - and the conversion from Python's `strftime` format to Java's - `SimpleDateFormat` format is done in - [`strftime_to_simple_date_format`][pact.v3.util.strftime_to_simple_date_format]. - If not provided, an ISO 8601 timestamp format will be used: `%Y-%m-%dT%H:%M:%S%z`. + disable_conversion: If True, the conversion from Python's `strftime` format to Java's `SimpleDateFormat` format will be disabled, and the format must be @@ -705,7 +727,7 @@ def timestamp( disable_conversion: builtins.bool = False, ) -> Matcher[builtins.str]: """ - Alias for [`match.datetime`][pact.v3.match.datetime]. + Alias for [`match.datetime`][pact.match.datetime]. """ return datetime(value, format, disable_conversion=disable_conversion) @@ -719,7 +741,7 @@ def none() -> Matcher[None]: def null() -> Matcher[None]: """ - Alias for [`match.none`][pact.v3.match.none]. + Alias for [`match.none`][pact.match.none]. """ return none() @@ -739,10 +761,13 @@ def type( value: A value to match against. This can be a primitive value, or a more complex object or array. + min: The minimum number of items that must match the value. + max: The maximum number of items that must match the value. + generator: The generator to use when generating the value. """ @@ -763,7 +788,7 @@ def like( generator: Generator | None = None, ) -> Matcher[_T]: """ - Alias for [`match.type`][pact.v3.match.type]. + Alias for [`match.type`][pact.match.type]. """ return type(value, min=min, max=max, generator=generator) @@ -783,9 +808,11 @@ def each_like( Args: value: The value to match against. + min: The minimum number of items that must match the value. The minimum value is always 1, even if min is set to 0. + max: The maximum number of items that must match the value. """ @@ -809,6 +836,7 @@ def includes( Args: value: The value to match against. + generator: The generator to use when generating the value. """ @@ -845,6 +873,7 @@ def each_key_matches( Args: value: The value to match against. + rules: The matching rules to match against each key. """ @@ -865,6 +894,7 @@ def each_value_matches( Args: value: The value to match against. + rules: The matching rules to match against each value. """ diff --git a/src/pact/pact.py b/src/pact/pact.py index da19bef24..bbd058165 100644 --- a/src/pact/pact.py +++ b/src/pact/pact.py @@ -11,7 +11,7 @@ ## Usage -The main class in this module is the [`Pact`][pact.v3.Pact] class. This class +The main class in this module is the [`Pact`][pact.Pact] class. This class defines the Pact between the consumer and the provider. It is responsible for defining the interactions between the two parties. @@ -115,8 +115,8 @@ class Pact: to the Pact. Each interaction between the consumer and the provider is defined through - the [`upon_receiving`][pact.v3.pact.Pact.upon_receiving] method, which - returns a sub-class of [`Interaction`][pact.v3.interaction.Interaction]. + the [`upon_receiving`][pact.pact.Pact.upon_receiving] method, which + returns a sub-class of [`Interaction`][pact.interaction.Interaction]. """ def __init__( @@ -347,7 +347,7 @@ def serve( # noqa: PLR0913 independently of `raises`. Returns: - A [`PactServer`][pact.v3.pact.PactServer] instance. + A [`PactServer`][pact.pact.PactServer] instance. """ return PactServer( self._handle, @@ -536,7 +536,7 @@ class PactServer: stopping the mock server when the block is exited. Note that the server should not be started directly, but rather through the - [`serve(...)`][pact.v3.Pact.serve] method of a [`Pact`][pact.v3.Pact]: + [`serve(...)`][pact.Pact.serve] method of a [`Pact`][pact.Pact]: ```python pact = Pact("consumer", "provider") @@ -546,7 +546,7 @@ class PactServer: ``` The URL for the server can be accessed through its - [`url`][pact.v3.pact.PactServer.url] attribute, which will be required in + [`url`][pact.pact.PactServer.url] attribute, which will be required in order to point the consumer client to the mock server: ```python diff --git a/src/pact/types.py b/src/pact/types.py index a0fd4eff3..1f316fdc2 100644 --- a/src/pact/types.py +++ b/src/pact/types.py @@ -120,7 +120,7 @@ class StateHandlerArgs(TypedDict, total=False): This argument is only used if the state handler is expected to perform both setup and teardown actions (i.e., if `teardown=True` is used when calling - [`Verifier.state_handler][pact.v3.verifier.Verifier.state_handler]`). + [`Verifier.state_handler][pact.verifier.Verifier.state_handler]`). """ parameters: dict[str, Any] | None diff --git a/src/pact/verifier.py b/src/pact/verifier.py index d1f1b0712..c9c84a8dd 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -335,12 +335,12 @@ def message_handler( 1. A fully fledged function that will be called for all messages. This is the most powerful option as it allows for full control over the message generation. The function's signature must be compatible with - the [`MessageProducerArgs`][pact.v3.types.MessageProducerArgs] type. + the [`MessageProducerArgs`][pact.types.MessageProducerArgs] type. 2. A dictionary mapping message names to either (a) producer functions, - (b) [`Message`][pact.v3.types.Message] dictionaries, or (c) raw + (b) [`Message`][pact.types.Message] dictionaries, or (c) raw bytes. If using a producer function, it must be compatible with the - [`MessageProducerArgs`][pact.v3.types.MessageProducerArgs] type. + [`MessageProducerArgs`][pact.types.MessageProducerArgs] type. ## Implementation @@ -529,7 +529,7 @@ def state_handler( in Python. The function signature must be compatible with the - [`StateHandlerArgs`][pact.v3.types.StateHandlerArgs]. If the function + [`StateHandlerArgs`][pact.types.StateHandlerArgs]. If the function has additional arguments, these must either have default values, or be filled by using the [`partial`][functools.partial] function. @@ -1344,7 +1344,7 @@ def __del__(self) -> None: Destructor for the Broker Selector. This destructor will raise a warning if the instance is dropped without - having the [`build()`][pact.v3.verifier.BrokerSelectorBuilder.build] + having the [`build()`][pact.verifier.BrokerSelectorBuilder.build] method called. """ if not self._built: diff --git a/tests/test_server.py b/tests/test_server.py index 9f349c4a4..dab2fb5ae 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,5 +1,5 @@ """ -Tests for `pact.v3._server` module. +Tests for `pact._server` module. """ import json diff --git a/tests/test_util.py b/tests/test_util.py index 4f307a173..612daa2ff 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,5 +1,5 @@ """ -Tests of pact.v3.util functions. +Tests of pact._util functions. """ from __future__ import annotations diff --git a/tests/test_verifier.py b/tests/test_verifier.py index 968e76151..18580eb42 100644 --- a/tests/test_verifier.py +++ b/tests/test_verifier.py @@ -1,5 +1,5 @@ """ -Unit tests for the pact.v3.verifier module. +Unit tests for the pact.verifier module. These tests perform only very basic checks to ensure that the FFI module is working correctly. They are not intended to test the Verifier API much, as From 8db3ae9277c038e0c89b0314f8f257b059e41e24 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 1 Aug 2025 21:34:15 +1000 Subject: [PATCH 0910/1376] docs: v3 review Miscellaneous review of the docs while I'm looking at them. Signed-off-by: JP-Ellis --- README.md | 40 ++-- docs/SUMMARY.md | 4 +- docs/scripts/markdown.py | 17 ++ docs/scripts/python.py | 26 ++- mkdocs.yml | 1 + pact-python-cli/README.md | 83 +++++++ pact-python-cli/docs/SUMMARY.md | 5 + pact-python-ffi/README.md | 85 +++++++- pact-python-ffi/docs/SUMMARY.md | 5 + pyproject.toml | 1 + src/pact/__init__.py | 168 ++++++++------ src/pact/_server.py | 45 +++- src/pact/_util.py | 8 +- src/pact/error.py | 4 +- src/pact/generate/__init__.py | 78 ++++++- .../interaction/_async_message_interaction.py | 2 +- src/pact/interaction/_base.py | 67 +++--- src/pact/interaction/_http_interaction.py | 206 ++++++------------ .../interaction/_sync_message_interaction.py | 6 +- src/pact/match/__init__.py | 64 +++++- src/pact/match/matcher.py | 23 +- src/pact/pact.py | 31 ++- src/pact/verifier.py | 90 ++++++-- 23 files changed, 749 insertions(+), 310 deletions(-) create mode 100644 pact-python-cli/docs/SUMMARY.md create mode 100644 pact-python-ffi/docs/SUMMARY.md diff --git a/README.md b/README.md index 5ba0a2a63..921f0aa19 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ alt="Build Status"> Build Status @@ -49,7 +49,7 @@ src="https://img.shields.io/badge/types-Mypy-blue.svg" alt="types - Mypy"> License @@ -57,17 +57,30 @@ @@ -115,7 +128,7 @@ This readme provides a high-level overview of the Pact Python library. For detai - [Provider testing](docs/provider.md) - [Examples](examples/README.md) -Documentation for the API is generated from the docstrings in the code which you can view at [`pact-foundation.github.io/pact-python/pact`](https://pact-foundation.github.io/pact-python/pact). Please be aware that only the [`pact.v3` module][pact.v3] is thoroughly documented at this time. +Documentation for the API is generated from the docstrings in the code which you can view at [`pact-foundation.github.io/pact-python/pact`](https://pact-foundation.github.io/pact-python/API). ### Need Help @@ -129,20 +142,11 @@ Documentation for the API is generated from the docstrings in the code which you [GitHub Discussions]: https://github.com/pact-foundation/pact-python/discussions [GitHub Issues]: https://github.com/pact-foundation/pact-python/issues -## V3 Preview - -Pact Python is currently undergoing a major rewrite which will be released with the `3.0.0` version. This rewrite will replace the existing Ruby backend with a Rust backend which will provide a significant performance improvement and will allow us to support more features in the future. You can find more information about this rewrite in [this tracking issue on GitHub](https://github.com/pact-foundation/pact-python/issues/396). - -You can preview the new version by using the [`pact.v3` module][pact.v3]. The new version is not yet feature complete, and may be subject to changes. Having said that, we would love to get your feedback on the new version: - -- For any issues you find, please [raise an issue][GitHub Issues] on GitHub. -- For any feedback you have, please join the discussion either on [GitHub Discussions] or in the [`#pact-python`](https://pact-foundation.slack.com/archives/C9VECUP6E) channel on the [Pact Foundation Slack]. - ## Installation The latest version of Pact Python can be installed from PyPi: -```sh +```console pip install pact-python # 🚀 now write some tests! ``` @@ -153,9 +157,9 @@ Pact Python tries to support all versions of Python that are still supported by In order to support the broadest range of use cases, Pact Python tries to impose the least restrictions on the versions of libraries that it uses. -### Do Not Track +### Telemetry -In order to get better statistics as to who is using Pact, we have an anonymous tracking event that triggers when Pact installs for the first time. The only things we [track](https://docs.pact.io/metrics) are your type of OS, and the version information for the package being installed. No personally identifiable information is sent as part of this request. You can disable tracking by setting the environment variable `PACT_DO_NOT_TRACK=1`: +In order to get better statistics as to who is using Pact, we collect some anonymous telemetry. The only things we [record](https://docs.pact.io/metrics) are your type of OS, and the version information for the package. No personally identifiable information is sent as part of this request. You can disable telemetry by setting the environment variable `PACT_DO_NOT_TRACK=1`: ## Contributing diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 892e74a0a..f86c2df59 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -6,6 +6,8 @@ - [Releases](releases.md) - [Changelog](CHANGELOG.md) - [Contributing](CONTRIBUTING.md) -- [Pact](pact/) - [Examples](examples/) +- [API Documentation](api/) +- [`pact-ffi`](pact-python-ffi/) +- [`pact-cli`](pact-python-cli/) - [Blog](blog/index.md) diff --git a/docs/scripts/markdown.py b/docs/scripts/markdown.py index ffb6b1194..6942592b9 100644 --- a/docs/scripts/markdown.py +++ b/docs/scripts/markdown.py @@ -125,5 +125,22 @@ def process_markdown( ".", ignore=[ "docs", + "pact-python-cli", + "pact-python-ffi", + ".github", + ], + ) + process_markdown( + "pact-python-ffi", + mapping=[ + ("pact-python-ffi/docs", "pact-python-ffi"), + ("pact-python-ffi", "pact-python-ffi"), + ], + ) + process_markdown( + "pact-python-cli", + mapping=[ + ("pact-python-cli/docs", "pact-python-cli"), + ("pact-python-cli", "pact-python-cli"), ], ) diff --git a/docs/scripts/python.py b/docs/scripts/python.py index 5dd8f8190..864e0174e 100644 --- a/docs/scripts/python.py +++ b/docs/scripts/python.py @@ -195,10 +195,6 @@ def process_python( "a" if destination.exists() else "w", encoding="utf-8", ) as fd: - print( - "# " + python_identifier.split(".")[-1].replace("_", " ").title(), - file=fd, - ) print("::: " + python_identifier, file=fd) mkdocs_gen_files.set_edit_path( @@ -211,8 +207,26 @@ def process_python( process_python( "src/pact", destination_mapping=[ - ("src/pact", "pact"), + ("src/pact", "api"), ], python_mapping=[("src.pact", "pact")], + ignore=["src/pact/v2"], + ) + process_python( + "pact-python-cli/src/pact_cli", + python_mapping=[("pact-python-cli.src.pact_cli", "pact_cli")], + destination_mapping=[ + ("pact-python-cli/src/pact_cli", "pact-python-cli/api"), + ], + ) + process_python( + "pact-python-ffi/src/pact_ffi", + python_mapping=[("pact-python-ffi.src.pact_ffi", "pact_ffi")], + destination_mapping=[ + ("pact-python-ffi/src/pact_ffi", "pact-python-ffi/api"), + ], + ) + process_python( + "examples", + ignore=["examples/v2"], ) - process_python("examples") diff --git a/mkdocs.yml b/mkdocs.yml index b11e5387c..a3bb987a1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,6 +15,7 @@ plugins: - literate-nav: nav_file: SUMMARY.md - section-index + - gh-admonitions # Library documentation - gen-files: scripts: diff --git a/pact-python-cli/README.md b/pact-python-cli/README.md index 493c44abe..170b9d27e 100644 --- a/pact-python-cli/README.md +++ b/pact-python-cli/README.md @@ -5,6 +5,89 @@ > > This package is used to package and bundle the Pact CLI _only_. It does not provide any Python functionality or API. + +
Community + Issues + Discussions + GitHub Stars +
Slack Stack Overflow Twitter
+ + + + + + + + + + + + + + + + +
Package + Version + Python Versions + Downloads +
CI/CD + Test Status + Build Status + Build Status +
Meta + Hatch project + linting - Ruff + style - Ruff + types - Mypy + License +
Community + Issues + Discussions + GitHub Stars +
+ Slack + Stack Overflow + Twitter +
+ + --- This sub-package is part of the [Pact Python](https://github.com/pact-foundation/pact-python) project and exists solely to distribute the [Pact CLI](https://github.com/pact-foundation/pact-ruby-standalone) as a Python package. If you are looking for the main Pact Python library for contract testing, please see the [root package](https://github.com/pact-foundation/pact-python#pact-python). diff --git a/pact-python-cli/docs/SUMMARY.md b/pact-python-cli/docs/SUMMARY.md new file mode 100644 index 000000000..7ea99c1c4 --- /dev/null +++ b/pact-python-cli/docs/SUMMARY.md @@ -0,0 +1,5 @@ + + +- [`pact-cli`](README.md) +- [Changelog](CHANGELOG.md) +- [API](api/) diff --git a/pact-python-ffi/README.md b/pact-python-ffi/README.md index 23d9231a1..c428e3870 100644 --- a/pact-python-ffi/README.md +++ b/pact-python-ffi/README.md @@ -4,6 +4,89 @@ > > This package provides direct access to the Pact Foreign Function Interface (FFI) with minimal abstraction. It is intended for advanced users who need low-level control over Pact operations in Python. + +
+ + + + + + + + + + + + + + + + +
Package + Version + Python Versions + Downloads +
CI/CD + Test Status + Build Status + Build Status +
Meta + Hatch project + linting - Ruff + style - Ruff + types - Mypy + License +
Community + Issues + Discussions + GitHub Stars +
+ Slack + Stack Overflow + Twitter +
+ + --- This sub-package is part of the [Pact Python](https://github.com/pact-foundation/pact-python) project and exists to expose the [Pact FFI](https://github.com/pact-foundation/pact-reference) directly to Python. If you are looking for the main Pact Python library for contract testing, please see the [root package](https://github.com/pact-foundation/pact-python#pact-python). @@ -13,7 +96,7 @@ This sub-package is part of the [Pact Python](https://github.com/pact-foundation - The module provides a thin Python wrapper around the Pact FFI (C API). - Most classes correspond directly to structs from the FFI, and are designed to wrap the underlying C pointers. - Many classes implement the `__del__` method to ensure memory allocated by the Rust library is freed when the Python object is destroyed, preventing memory leaks. -- Functions from the FFI are exposed directly: if a function `foo` exists in the FFI, it is accessible as `pact_ffi.foo(...)`. +- Functions from the FFI are exposed directly: if a function `foo` exists in the FFI, it is accessible as `pact_ffi.foo`. - The API is not guaranteed to be stable and is intended for use by advanced users or for building higher-level libraries. For typical contract testing, use the main Pact Python client library. ## Installation diff --git a/pact-python-ffi/docs/SUMMARY.md b/pact-python-ffi/docs/SUMMARY.md new file mode 100644 index 000000000..cc957d0cb --- /dev/null +++ b/pact-python-ffi/docs/SUMMARY.md @@ -0,0 +1,5 @@ + + +- [`pact-ffi`](README.md) +- [Changelog](CHANGELOG.md) +- [API](api/) diff --git a/pyproject.toml b/pyproject.toml index d522dbe56..fb6e52f0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ dependencies = [ "ruff==0.12.7", ] devel-docs = [ + "mkdocs-github-admonitions-plugin~=0.0", "mkdocs-literate-nav~=0.6", "mkdocs-material[imaging]~=9.4", "mkdocs-section-index~=0.3", diff --git a/src/pact/__init__.py b/src/pact/__init__.py index cf5edea81..1293d1561 100644 --- a/src/pact/__init__.py +++ b/src/pact/__init__.py @@ -1,71 +1,102 @@ """ Pact Python V3. -This module provides a preview of the next major version of Pact Python. It is -subject to breaking changes and may have bugs; however, it is available for -testing and feedback. If you encounter any issues, please report them on -[GitHub](https://github.com/pact-foundation/pact-python/issues), and if you have -any feedback, please let us know on either the [GitHub -discussions](https://github.com/pact-foundation/pact-python/discussions) or on -[Slack](https://slack.pact.io/). - -The next major release will use the [Pact Rust -library](https://github.com/pact-foundation/pact-reference) to provide full -support for all Pact features, and bring feature parity between the Python -library and the other Pact libraries. - -## Migration Plan - -This change will introduce some breaking changes where needed, but it will be -done in a staged manner to give everyone the opportunity to migrate. - -### :construction: Stage 1 (from v2.2) - -- The main Pact Python library remains the same. Bugs and minor features will - continue to be added to the existing library, but no new major features will - be added as the focus will be on the new library. -- The new library is exposed within `pact.v3` and can be used alongside the - existing library. During this stage, no guarantees are made about the - stability of the `pact.v3` module. -- Users are **not** recommended to use the new library in any production - critical code at this stage, but are encouraged to try it out and provide - feedback. -- The existing library will raise - [`PendingDeprecationWarning`][PendingDeprecationWarning] warnings when it is - used (if these warnings are enabled). - -### :hammer_and_wrench: Stage 2 (from v2.3, tbc) - -- The library within `pact.v3` is considered generally stable and users are - encouraged to start migrating to it. -- A detailed migration guide will be provided. -- The existing library will raise [`DeprecationWarning`][DeprecationWarning] - warnings when it is used to help raise awareness of the upcoming change. -- This stage will likely last a few months to give everyone the opportunity to - migrate. - -### :rocket: Stage 3 (from v3) - -- The `pact.v3` module is renamed to `pact` - - - People who have previously migrated to `pact.v3` should be able to do a - `s/pact.v3/pact/` and have everything work. - - If the previous stage identifies any breaking changes as necessary, they - will be made at this point and a detailed migration guide will be - provided. - -- The existing library is moved to the `pact.v2` scope. - - - :bangbang: This will be a very major and breaking change. Previous code - running against `v2` of Pact Python will **not** work against `v3` of - Pact Python. - - Users still wanting to use the `v2` library will need to update their - code to use the new `pact.v2` module. A `s/pact/pact.v2/` should be - sufficient. - - The `pact.v2` module will be considered deprecated, and will eventually - be removed in a future release. No new features and only critical bug - fixes will be made to this part of the library. - +This package provides contract testing capabilities for Python applications +using the [Pact specification](https://docs.pact.io/). Built on the [Pact Rust +FFI library](https://github.com/pact-foundation/pact-reference), it offers full +support for all Pact features and maintains compatibility with other Pact +implementations. + +## Package Structure + +### Main Classes + +The primary entry points for contract testing are: + +- [`Pact`][pact.Pact]: For consumer-side contract testing, defining expected + interactions and generating contract files. +- [`Verifier`][pact.Verifier]: For provider-side contract verification, + validating that a provider implementation satisfies consumer contracts. + +These functions are defined in [`pact.pact`][pact.pact] and +[`pact.verifier`][pact.verifier] and re-exported for convenience. + +### Matching and Generation + +For flexible contract definitions, use the matching and generation modules: + +```python +from pact import match, generate + +# Import modules, not individual functions +# Use functions via module namespace to avoid shadowing built-ins +user_id = match.int(min=1) +user_name = match.str(size=20) +created_at = match.datetime() + +# Generators work similarly +response_id = generate.uuid() +score = generate.float(precision=2) +``` + +The functions within these modules are designed to align with a number of +Python built-in types and functions. As such, the module should be imported +as a whole, rather than importing individual functions to avoid potential +shadowing of built-ins. + +### Utility Modules + +- `error`: Exception classes used throughout the package. Typically not + imported directly unless implementing custom error handling. +- `types`: Type definitions and protocols. This does not provide much + functionality, but will be used by your type-checker. + +## Basic Usage + +### Consumer Testing + +```python +from pact import Pact, match + +# Create a consumer contract +pact = Pact("consumer", "provider") + +# Define expected interactions +( + pact.upon_receiving("get user") + .given("user exists") + .with_request(method="GET", path="/users/123") + .will_respond_with( + status=200, + body={ + "id": match.int(123), + "name": match.str("alice"), + }, + ) +) + +# Use in tests with the mock server +with pact.serve() as server: + # Make requests to server.url + # Test your consumer code + pass +``` + +### Provider Verification + +```python +from pact import Verifier + +# Verify provider against contracts +verifier = Verifier() +verifier.verify_with_broker( + provider="provider-name", + broker_url="https://my-org.pactflow.io", +) +``` + +For more detailed usage examples, see the +[examples](https://pact-foundation.github.io/pact-python/examples). """ from pact.__version__ import __version__, __version_tuple__ @@ -76,4 +107,9 @@ __license__ = "MIT" __author__ = "Pact Foundation" -__all__ = ["Pact", "Verifier", "__version__", "__version_tuple__"] +__all__ = [ + "Pact", + "Verifier", + "__version__", + "__version_tuple__", +] diff --git a/src/pact/_server.py b/src/pact/_server.py index 771853f20..61f9d1a9b 100644 --- a/src/pact/_server.py +++ b/src/pact/_server.py @@ -113,8 +113,9 @@ def __init__( Args: handler: - The handler function to call when a request is received. It must - accept two positional arguments: + The handler function to call when a request is received. + + The handler must accept two positional arguments: - The body of the request if present as a byte string, or `None`. @@ -173,6 +174,10 @@ def __enter__(self) -> Self: This method starts the Pact server in a separate thread to handle the communication between the server and the underlying Pact Core library. + + Returns: + Self: + The started message producer server instance. """ self._server = HandlerHttpServer( (self.host, self.port), @@ -194,6 +199,16 @@ def __exit__( ) -> None: """ Exit the Pact message server context. + + Args: + exc_type: + The exception type, if an exception was raised. + + exc_value: + The exception value, if an exception was raised. + + traceback: + The traceback, if an exception was raised. """ if not self._thread or not self._server: warnings.warn( @@ -239,7 +254,7 @@ def version_string(self) -> str: This method is overridden to return a custom server version string. """ - return f"Pact Python Message Relay/{__version__}" + return f"pact-python/{__version__} message-relay" def do_POST(self) -> None: """ @@ -328,10 +343,13 @@ def __init__( Args: handler: - The handler function to call when a state callback is - received. + The handler function to call when a state callback is received. - The + The handler must accept three positional arguments: + + - The state name as a string. + - The action as a string. + - The params as a dictionary. host: The host to run the server on. @@ -375,6 +393,9 @@ def __enter__(self) -> Self: This method starts the Pact server in a separate thread to handle the communication between the server and the underlying Pact Core library. + + Returns: + The started state callback server instance. """ self._server = HandlerHttpServer( (self.host, self.port), @@ -396,6 +417,16 @@ def __exit__( ) -> None: """ Exit the state handler context. + + Args: + exc_type: + The exception type, if an exception was raised. + + exc_value: + The exception value, if an exception was raised. + + traceback: + The traceback, if an exception was raised. """ if not self._thread or not self._server: warnings.warn( @@ -427,7 +458,7 @@ def version_string(self) -> str: This method is overridden to return a custom server version string. """ - return f"Pact Python State Callback/{__version__}" + return f"pact-python/{__version__} state-callback" def do_POST(self) -> None: """ diff --git a/src/pact/_util.py b/src/pact/_util.py index cf9099ebe..6d242b81c 100644 --- a/src/pact/_util.py +++ b/src/pact/_util.py @@ -125,11 +125,15 @@ def format_code_to_java_format(code: str) -> str: Args: code: The Python format code to convert, without the leading `%`. This - will typically be a single character, but may be two characters - for some codes. + will typically be a single character, but may be two characters for + some codes. Returns: The equivalent Java SimpleDateFormat format string. + + Raises: + ValueError: + If the code is locale-dependent or unsupported. """ if code in ["U", "V", "W"]: warnings.warn( diff --git a/src/pact/error.py b/src/pact/error.py index 59ad49cd8..c19d6a55f 100644 --- a/src/pact/error.py +++ b/src/pact/error.py @@ -36,8 +36,8 @@ def __init__(self, description: str, error: Exception) -> None: description: Description of the interaction that failed verification. - error: Error that occurred during the verification of the - interaction. + error: + Error that occurred during the verification of the interaction. """ super().__init__(f"Error verifying interaction '{description}': {error}") self._description = description diff --git a/src/pact/generate/__init__.py b/src/pact/generate/__init__.py index ee8721c60..724de7d4c 100644 --- a/src/pact/generate/__init__.py +++ b/src/pact/generate/__init__.py @@ -98,6 +98,9 @@ def int( max: The maximum value for the integer. + + Returns: + A generator that produces random integers. """ params: dict[builtins.str, builtins.int] = {} if min is not None: @@ -114,6 +117,16 @@ def integer( ) -> Generator: """ Alias for [`generate.int`][pact.generate.int]. + + Args: + min: + The minimum value for the integer. + + max: + The maximum value for the integer. + + Returns: + A generator that produces random integers. """ return int(min=min, max=max) @@ -129,6 +142,9 @@ def float(precision: builtins.int | None = None) -> Generator: Args: precision: The number of digits to generate. + + Returns: + A generator that produces random decimal values. """ params: dict[builtins.str, builtins.int] = {} if precision is not None: @@ -139,6 +155,13 @@ def float(precision: builtins.int | None = None) -> Generator: def decimal(precision: builtins.int | None = None) -> Generator: """ Alias for [`generate.float`][pact.generate.float]. + + Args: + precision: + The number of digits to generate. + + Returns: + A generator that produces random decimal values. """ return float(precision=precision) @@ -150,6 +173,9 @@ def hex(digits: builtins.int | None = None) -> Generator: Args: digits: The number of digits to generate. + + Returns: + A generator that produces random hexadecimal values. """ params: dict[builtins.str, builtins.int] = {} if digits is not None: @@ -160,6 +186,13 @@ def hex(digits: builtins.int | None = None) -> Generator: def hexadecimal(digits: builtins.int | None = None) -> Generator: """ Alias for [`generate.hex`][pact.generate.hex]. + + Args: + digits: + The number of digits to generate. + + Returns: + A generator that produces random hexadecimal values. """ return hex(digits=digits) @@ -171,6 +204,9 @@ def str(size: builtins.int | None = None) -> Generator: Args: size: The size of the string to generate. + + Returns: + A generator that produces random strings. """ params: dict[builtins.str, builtins.int] = {} if size is not None: @@ -181,6 +217,13 @@ def str(size: builtins.int | None = None) -> Generator: def string(size: builtins.int | None = None) -> Generator: """ Alias for [`generate.str`][pact.generate.str]. + + Args: + size: + The size of the string to generate. + + Returns: + A generator that produces random strings. """ return str(size=size) @@ -194,6 +237,9 @@ def regex(regex: builtins.str) -> Generator: Args: regex: The regex pattern to match. + + Returns: + A generator that produces strings matching the given regex pattern. """ return GenericGenerator("Regex", {"regex": regex}) @@ -216,6 +262,9 @@ def uuid( format: The format of the UUID to generate. This parameter is only supported under the V4 specification. + + Returns: + A generator that produces UUIDs in the specified format. """ return GenericGenerator("Uuid", {"format": format}) @@ -236,7 +285,6 @@ def date( function accepts Python's [`strftime`](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior) format and performs the conversion to Java's format internally. - Args: format: Expected format of the date. @@ -245,9 +293,12 @@ def date( disable_conversion: If True, the conversion from Python's `strftime` format to Java's - `SimpleDateFormat` format will be disabled, and the format must be - in Java's `SimpleDateFormat` format. As a result, the value must + `SimpleDateFormat` format will be disabled, and the format must + be in Java's `SimpleDateFormat` format. As a result, the value must be a string as Python cannot format the date in the target format. + + Returns: + A generator that produces dates in the specified format. """ if not disable_conversion: format = strftime_to_simple_date_format(format or "%Y-%m-%d") @@ -282,6 +333,9 @@ def time( `SimpleDateFormat` format will be disabled, and the format must be in Java's `SimpleDateFormat` format. As a result, the value must be a string as Python cannot format the time in the target format. + + Returns: + A generator that produces times in the specified format. """ if not disable_conversion: format = strftime_to_simple_date_format(format or "%H:%M:%S") @@ -316,6 +370,9 @@ def datetime( If True, the conversion from Python's `strftime` format to Java's `SimpleDateFormat` format will be disabled, and the format must be in Java's `SimpleDateFormat` format. As a result, the value must be + + Returns: + A generator that produces datetimes in the specified format. """ if not disable_conversion: format = strftime_to_simple_date_format(format or "%Y-%m-%dT%H:%M:%S%z") @@ -329,6 +386,9 @@ def timestamp( ) -> Generator: """ Alias for [`generate.datetime`][pact.generate.datetime]. + + Returns: + A generator that produces datetimes in the specified format. """ return datetime(format=format, disable_conversion=disable_conversion) @@ -336,6 +396,9 @@ def timestamp( def bool() -> Generator: """ Create a random boolean generator. + + Returns: + A generator that produces random boolean values. """ return GenericGenerator("RandomBoolean") @@ -343,6 +406,9 @@ def bool() -> Generator: def boolean() -> Generator: """ Alias for [`generate.bool`][pact.generate.bool]. + + Returns: + A generator that produces random boolean values. """ return bool() @@ -357,6 +423,9 @@ def provider_state(expression: builtins.str | None = None) -> Generator: Args: expression: The expression to use to look up the provider state. + + Returns: + A generator that produces values from the provider state context. """ params: dict[builtins.str, builtins.str] = {} if expression is not None: @@ -379,6 +448,9 @@ def mock_server_url( example: An example URL to use. + + Returns: + A generator that produces mock server URLs. """ params: dict[builtins.str, builtins.str] = {} if regex is not None: diff --git a/src/pact/interaction/_async_message_interaction.py b/src/pact/interaction/_async_message_interaction.py index ac6048e45..a76a5d32a 100644 --- a/src/pact/interaction/_async_message_interaction.py +++ b/src/pact/interaction/_async_message_interaction.py @@ -28,7 +28,7 @@ def __init__(self, pact_handle: pact_ffi.PactHandle, description: str) -> None: This function should not be called directly. Instead, an AsyncMessageInteraction should be created using the - [`upon_receiving(...)`][pact.Pact.upon_receiving] method of a + [`upon_receiving`][pact.Pact.upon_receiving] method of a [`Pact`][pact.Pact] instance using the `"Async"` interaction type. Args: diff --git a/src/pact/interaction/_base.py b/src/pact/interaction/_base.py index d1a150da6..fece0e1f7 100644 --- a/src/pact/interaction/_base.py +++ b/src/pact/interaction/_base.py @@ -207,23 +207,10 @@ def given( parameters: Key-value pairs of parameters to use for the provider state. - These must be encodable using [`json.dumps(...)`][json.dumps]. - Alternatively, a string contained the JSON object can be passed + These must be encodable using [`json.dumps`][json.dumps]. + Alternatively, a string containing the JSON object can be passed directly. - If the string does not contain a valid JSON object, then the - string is passed directly as follows: - - ```python - ( - pact.upon_receiving("a request").given( - "a user exists", - name="value", - value=parameters, - ) - ) - ``` - Raises: ValueError: If the combination of arguments is invalid or inconsistent. @@ -293,20 +280,20 @@ def with_binary_body( Note that for HTTP interactions, this function will overwrite the body if it has been set using - [`with_body(...)`][pact.interaction.Interaction.with_body]. + [`with_body`][pact.interaction.Interaction.with_body]. Args: - part: - Whether the body should be added to the request or the response. - If `None`, then the function intelligently determines whether - the body should be added to the request or the response. + body: + Body of the request. content_type: Content type of the body. This is ignored if the `Content-Type` header has already been set. - body: - Body of the request. + part: + Whether the body should be added to the request or the response. + If `None`, then the function intelligently determines whether + the body should be added to the request or the response. """ pact_ffi.with_binary_body( self._handle, @@ -328,7 +315,7 @@ def with_metadata( This function may either be called with a single dictionary of metadata, or with keyword arguments that are the key-value pairs of the metadata - (or a combination therefore): + (or a combination thereof): ```python interaction.with_metadata({"key": "value", "key two": "value two"}) @@ -352,7 +339,7 @@ def with_metadata( ``` Args: - ___metadata: + __metadata: Dictionary of metadata keys and associated values. __part: @@ -394,7 +381,24 @@ def with_multipart_file( """ Adds a binary file as the body of a multipart request or response. - The content type of the body will be set to a MIME multipart message. + Args: + part_name: + Name of the multipart part. + + path: + Path to the file to add. + + content_type: + Content type of the part. + + part: + Whether the part should be added to the request or the + response. + If `None`, then the function intelligently determines whether + the part should be added to the request or the response. + + boundary: + Boundary string for the multipart message. """ pact_ffi.with_multipart_file_v2( self._handle, @@ -440,7 +444,7 @@ def set_comment(self, key: str, value: Any | None) -> Self: # noqa: ANN401 value: Value for the comment. This must be encodable using - [`json.dumps(...)`][json.dumps], or an existing JSON string. The + [`json.dumps`][json.dumps], or an existing JSON string. The value of `None` will remove the comment with the given key. # Warning @@ -469,8 +473,7 @@ def add_text_comment(self, comment: str) -> Self: Internally, the comments are appended to an array under the `text` comment key. Care should be taken to ensure that conflicts are not - introduced by - [`set_comment`][pact.interaction.Interaction.set_comment]. + introduced by [`set_comment`][pact.interaction.Interaction.set_comment]. """ pact_ffi.add_text_comment(self._handle, comment) return self @@ -541,8 +544,8 @@ def with_matching_rules( Args: rules: - Matching rules to add to the interaction. This must be - encodable using [`json.dumps(...)`][json.dumps], or a string. + Matching rules to add to the interaction. This must be encodable + using [`json.dumps`][json.dumps], or a string. part: Whether the matching rules should be added to the request or the @@ -574,8 +577,8 @@ def with_generators( Args: generators: - Generators to add to the interaction. This must be encodable using - [`json.dumps(...)`][json.dumps], or a string. + Generators to add to the interaction. This must be encodable + using [`json.dumps`][json.dumps], or a string. part: Whether the generators should be added to the request or the diff --git a/src/pact/interaction/_http_interaction.py b/src/pact/interaction/_http_interaction.py index e0b47828c..57dd1f2b2 100644 --- a/src/pact/interaction/_http_interaction.py +++ b/src/pact/interaction/_http_interaction.py @@ -6,7 +6,7 @@ import json from collections import defaultdict -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Literal import pact_ffi from pact.interaction._base import Interaction @@ -35,7 +35,7 @@ class HttpInteraction(Interaction): response, this class provides a common interface for both. The functions intelligently determine whether the element should be added to the request or the response based on whether - [`will_respond_with(...)`][pact.interaction.HttpInteraction.will_respond_with] + [`will_respond_with`][pact.interaction.HttpInteraction.will_respond_with] has been called. For example, the following two interactions are equivalent: @@ -67,7 +67,7 @@ def __init__(self, pact_handle: pact_ffi.PactHandle, description: str) -> None: This class should not be instantiated directly. Instead, an `HttpInteraction` should be created using the - [`upon_receiving(...)`][pact.Pact.upon_receiving] method of a + [`upon_receiving`][pact.Pact.upon_receiving] method of a [`Pact`][pact.Pact] instance. """ super().__init__(description) @@ -99,7 +99,7 @@ def _interaction_part(self) -> pact_ffi.InteractionPart: """ return self.__interaction_part - def with_request(self, method: str, path: str | Matcher[Any]) -> Self: + def with_request(self, method: str, path: str | Matcher[object]) -> Self: """ Set the request. @@ -108,6 +108,7 @@ def with_request(self, method: str, path: str | Matcher[Any]) -> Self: Args: method: HTTP method for the request. + path: Path for the request. """ @@ -121,15 +122,13 @@ def with_request(self, method: str, path: str | Matcher[Any]) -> Self: def with_header( self, name: str, - value: str | dict[str, str] | Matcher[Any], + value: str | dict[str, str] | Matcher[object], part: Literal["Request", "Response"] | None = None, ) -> Self: r""" Add a header to the request. - # Repeated Headers - - If the same header has multiple values ([see RFC9110 + If the same header has multiple values (see [RFC9110 §5.2](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.2)), then the same header must be specified multiple times with _order being preserved_. For example @@ -155,49 +154,6 @@ def with_header( [RFC 9110 §5.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.1). - # JSON Matching - - Pact's matching rules are defined in the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) - and support a wide range of matching rules. These can be specified - using a JSON object as a strong using `json.dumps(...)`. For example, - the above rule whereby the `X-Foo` header has multiple values can be - specified as: - - ```python - ( - pact.upon_receiving("a request") - .with_header( - "X-Foo", - json.dumps({ - "value": ["bar", "baz"], - }), - ) - ) - ``` - - It is also possible to have a more complicated Regex pattern for the - header. For example, a pattern for an `Accept-Version` header might be - specified as: - - ```python - ( - pact.upon_receiving("a request").with_header( - "Accept-Version", - json.dumps({ - "value": "1.2.3", - "pact:matcher:type": "regex", - "regex": r"\d+\.\d+\.\d+", - }), - ) - ) - ``` - - If the value of the header is expected to be a JSON object and clashes - with the above syntax, then it is recommended to make use of the - [`set_header(...)`][pact.interaction.HttpInteraction.set_header] - method instead. - Args: name: Name of the header. @@ -207,10 +163,12 @@ def with_header( part: Whether the header should be added to the request or the - response. If `None`, then the function intelligently determines - whether the header should be added to the request or the - response, based on whether the - [`will_respond_with(...)`][pact.interaction.HttpInteraction.will_respond_with] + response. + + If `None`, then the function intelligently determines whether + the header should be added to the request or the response, based + on whether the + [`will_respond_with`][pact.interaction.HttpInteraction.will_respond_with] method has been called. """ interaction_part = self._parse_interaction_part(part) @@ -238,32 +196,20 @@ def with_headers( """ Add multiple headers to the request. - Note that due to the requirement of Python dictionaries to have unique - keys, it is _not_ possible to specify a header multiple times to create - a multi-valued header. Instead, you may: - - - Use an alternative data structure. Any iterable of key-value pairs - is accepted, including a list of tuples, a list of lists, or a - dictionary view. + While it is often convenient to use a dictionary to specify headers, + this does not support repeated headers. If you need to specify repeated + headers, consider one of the following: - - Make multiple calls to - [`with_header(...)`][pact.interaction.HttpInteraction.with_header] - or - [`with_headers(...)`][pact.interaction.HttpInteraction.with_headers]. + - An alternative dictionary implementation which supports repeated + keys such as [multidict](https://pypi.org/project/multidict/). - - Specify the multiple values in a JSON object of the form: + - Passing in an iterable of key-value tuples. - ```python ( - pact.upon_receiving("a request") .with_headers({ - "X-Foo": json.dumps({ - "value": ["bar", "baz"], - }), - ) - ) - ``` + - Make multiple calls to this function or + [`with_header`][pact.interaction.HttpInteraction.with_header]. See - [`with_header(...)`][pact.interaction.HttpInteraction.with_header] + [`with_header`][pact.interaction.HttpInteraction.with_header] for more information. Args: @@ -272,10 +218,12 @@ def with_headers( part: Whether the header should be added to the request or the - response. If `None`, then the function intelligently determines - whether the header should be added to the request or the - response, based on whether the - [`will_respond_with(...)`][pact.interaction.HttpInteraction.will_respond_with] + response. + + If `None`, then the function intelligently determines whether + the header should be added to the request or the response, based + on whether the + [`will_respond_with`][pact.interaction.HttpInteraction.will_respond_with] method has been called. """ if isinstance(headers, dict): @@ -294,8 +242,8 @@ def set_header( Add a header to the request. Unlike - [`with_header(...)`][pact.interaction.HttpInteraction.with_header], - this function does no additional processing of the header value. This is + [`with_header`][pact.interaction.HttpInteraction.with_header], this + function does no additional processing of the header value. This is useful for headers that contain a JSON object. Args: @@ -307,10 +255,12 @@ def set_header( part: Whether the header should be added to the request or the - response. If `None`, then the function intelligently determines - whether the header should be added to the request or the - response, based on whether the - [`will_respond_with(...)`][pact.interaction.HttpInteraction.will_respond_with] + response. + + If `None`, then the function intelligently determines whether + the header should be added to the request or the response, based + on whether the + [`will_respond_with`][pact.interaction.HttpInteraction.will_respond_with] method has been called. """ pact_ffi.set_header( @@ -329,12 +279,19 @@ def set_headers( """ Add multiple headers to the request. - This function intelligently determines whether the header should be - added to the request or the response, based on whether the - [`will_respond_with(...)`][pact.interaction.HttpInteraction.will_respond_with] - method has been called. + While it is often convenient to use a dictionary to specify headers, + this does not support repeated headers. If you need to specify repeated + headers, consider one of the following: - See [`set_header(...)`][pact.interaction.HttpInteraction.set_header] for + - An alternative dictionary implementation which supports repeated + keys such as [multidict](https://pypi.org/project/multidict/). + + - Passing in an iterable of key-value tuples. + + - Make multiple calls to this function or + [`with_header`][pact.interaction.HttpInteraction.with_header]. + + See [`set_header`][pact.interaction.HttpInteraction.set_header] for more information. Args: @@ -343,10 +300,12 @@ def set_headers( part: Whether the headers should be added to the request or the - response. If `None`, then the function intelligently determines - whether the header should be added to the request or the - response, based on whether the - [`will_respond_with(...)`][pact.interaction.HttpInteraction.will_respond_with] + response. + + If `None`, then the function intelligently determines whether + the headers should be added to the request or the response, + based on whether the + [`will_respond_with`][pact.interaction.HttpInteraction.will_respond_with] method has been called. """ if isinstance(headers, dict): @@ -358,13 +317,12 @@ def set_headers( def with_query_parameter( self, name: str, - value: str | dict[str, str] | Matcher[Any], + value: object | Matcher[object], ) -> Self: r""" Add a query to the request. - This is the query parameter(s) that the consumer will send to the - provider. + This is the query parameter that the consumer will send to the provider. If the same parameter can support multiple values, then the same parameter can be specified multiple times: @@ -377,39 +335,6 @@ def with_query_parameter( ) ``` - The above can equivalently be specified as: - - ```python - ( - pact.upon_receiving("a request").with_query_parameter( - "name", - json.dumps({ - "value": ["John", "Mary"], - }), - ) - ) - ``` - - It is also possible to have a more complicated Regex pattern for the - parameter. For example, a pattern for an `version` parameter might be - specified as: - - ```python - ( - pact.upon_receiving("a request").with_query_parameter( - "version", - json.dumps({ - "value": "1.2.3", - "pact:matcher:type": "regex", - "regex": r"\d+\.\d+\.\d+", - }), - ) - ) - ``` - - For more information on the format of the JSON object, see the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). - Args: name: Name of the query parameter. @@ -433,13 +358,26 @@ def with_query_parameter( def with_query_parameters( self, - parameters: dict[str, Any] | Iterable[tuple[str, Any]], + parameters: dict[str, object | Matcher[object]] + | Iterable[tuple[str, object | Matcher[object]]], ) -> Self: """ Add multiple query parameters to the request. + While it is often convenient to use a dictionary to specify query + parameters, this does not support repeated keys. If you need to specify + repeated keys, consider one of the following: + + - An alternative dictionary implementation which supports repeated + keys such as [multidict](https://pypi.org/project/multidict/). + + - Passing in an iterable of key-value tuples. + + - Make multiple calls to this function or + [`with_query_parameter`][pact.interaction.HttpInteraction.with_query_parameter]. + See - [`with_query_parameter(...)`][pact.interaction.HttpInteraction.with_query_parameter] + [`with_query_parameter`][pact.interaction.HttpInteraction.with_query_parameter] for more information. Args: @@ -458,7 +396,7 @@ def will_respond_with(self, status: int) -> Self: Ideally, this function is called once all of the request information has been set. This allows functions such as - [`with_header(...)`][pact.interaction.HttpInteraction.with_header] + [`with_header`][pact.interaction.HttpInteraction.with_header] to intelligently determine whether this is a request or response header. Alternatively, the `part` argument can be used to explicitly specify diff --git a/src/pact/interaction/_sync_message_interaction.py b/src/pact/interaction/_sync_message_interaction.py index 40ac22373..42a8453e4 100644 --- a/src/pact/interaction/_sync_message_interaction.py +++ b/src/pact/interaction/_sync_message_interaction.py @@ -30,7 +30,7 @@ def __init__(self, pact_handle: pact_ffi.PactHandle, description: str) -> None: This function should not be called directly. Instead, an AsyncMessageInteraction should be created using the - [`upon_receiving(...)`][pact.Pact.upon_receiving] method of a + [`upon_receiving`][pact.Pact.upon_receiving] method of a [`Pact`][pact.Pact] instance using the `"Sync"` interaction type. Args: @@ -72,8 +72,7 @@ def will_respond_with(self) -> Self: method of the [`HttpInteraction`][pact.pact.HttpInteraction] class, albeit more generic for synchronous message interactions. - For example, the following two snippets are - equivalent: + For example, the following two snippets are equivalent: ```python Pact(...).upon_receiving("A sync request", interaction="Sync") @@ -90,7 +89,6 @@ def will_respond_with(self) -> Self: Returns: The current instance of the interaction. - """ self.__interaction_part = pact_ffi.InteractionPart.RESPONSE return self diff --git a/src/pact/match/__init__.py b/src/pact/match/__init__.py index 92ab241d0..e643fdbbd 100644 --- a/src/pact/match/__init__.py +++ b/src/pact/match/__init__.py @@ -161,6 +161,9 @@ def int( max: If provided, the maximum value of the integer to generate. + + Returns: + Matcher for integer values. """ if value is UNSET: return GenericMatcher( @@ -204,6 +207,9 @@ def float( precision: The number of decimal precision to generate. + + Returns: + Matcher for floating point numbers. """ if value is UNSET: return GenericMatcher( @@ -276,15 +282,18 @@ def number( min: The minimum value of the number to generate. Only used when value is - an integer. Defaults to None. + an integer. max: The maximum value of the number to generate. Only used when value is - an integer. Defaults to None. + an integer. precision: The number of decimal digits to generate. Only used when value is a - float. Defaults to None. + float. + + Returns: + Matcher for numbers (integer, float, or Decimal). """ if value is UNSET: if min is not None or max is not None: @@ -354,8 +363,11 @@ def str( The size of the string to generate during a consumer test. generator: - Alternative generator to use when generating a consumer test. If - set, the `size` argument is ignored. + Alternative generator to use when generating a consumer test. + If set, the `size` argument is ignored. + + Returns: + Matcher for string values. """ if value is UNSET: if size and generator: @@ -408,6 +420,9 @@ def regex( regex: The regular expression to match against. + + Returns: + Matcher for strings matching the given regular expression. """ if regex is None: msg = "A regex pattern must be provided." @@ -444,9 +459,8 @@ def uuid( Match a UUID value. This matcher internally combines the [`regex`][pact.match.regex] matcher - with a UUID regex pattern. See [RFC - 4122](https://datatracker.ietf.org/doc/html/rfc4122) for details about the - UUID format. + with a UUID regex pattern. See [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) + for details about the UUID format. While RFC 4122 requires UUIDs to be output as lowercase, UUIDs are case insensitive on input. Some common alternative formats can be enforced using @@ -467,6 +481,9 @@ def uuid( `urn:uuid:` If not provided, the matcher will accept any lowercase or uppercase. + + Returns: + Matcher for UUID strings. """ pattern = ( rf"^{_UUID_FORMATS[format]}$" @@ -493,6 +510,9 @@ def bool(value: builtins.bool | Unset = UNSET, /) -> Matcher[builtins.bool]: Args: value: Default value to use when generating a consumer test. + + Returns: + Matcher for boolean values. """ if value is UNSET: return GenericMatcher("boolean", generator=generate.bool()) @@ -542,6 +562,9 @@ def date( `SimpleDateFormat` format will be disabled, and the format must be in Java's `SimpleDateFormat` format. As a result, the value must be a string as Python cannot format the date in the target format. + + Returns: + Matcher for date strings. """ if disable_conversion: if not isinstance(value, builtins.str): @@ -613,6 +636,9 @@ def time( `SimpleDateFormat` format will be disabled, and the format must be in Java's `SimpleDateFormat` format. As a result, the value must be a string as Python cannot format the time in the target format. + + Returns: + Matcher for time strings. """ if disable_conversion: if not isinstance(value, builtins.str): @@ -675,7 +701,6 @@ def datetime( format: Expected format of the timestamp. - format](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) If not provided, an ISO 8601 timestamp format will be used: `%Y-%m-%dT%H:%M:%S%z`. @@ -685,6 +710,9 @@ def datetime( `SimpleDateFormat` format will be disabled, and the format must be in Java's `SimpleDateFormat` format. As a result, the value must be a string as Python cannot format the timestamp in the target format. + + Returns: + Matcher for datetime strings. """ if disable_conversion: if not isinstance(value, builtins.str): @@ -770,6 +798,9 @@ def type( generator: The generator to use when generating the value. + + Returns: + Matcher for the given value type. """ if value is UNSET: if not generator: @@ -815,6 +846,9 @@ def each_like( max: The maximum number of items that must match the value. + + Returns: + Matcher for arrays where each item matches the given value. """ if min is not None and min < 1: warnings.warn( @@ -839,6 +873,9 @@ def includes( generator: The generator to use when generating the value. + + Returns: + Matcher for strings that include the given value. """ return GenericMatcher( "include", @@ -857,6 +894,9 @@ def array_containing(variants: Sequence[_T | Matcher[_T]], /) -> Matcher[Sequenc Args: variants: A list of variants to match against. + + Returns: + Matcher for arrays containing the given variants. """ return ArrayContainsMatcher(variants=variants) @@ -876,6 +916,9 @@ def each_key_matches( rules: The matching rules to match against each key. + + Returns: + Matcher for dictionaries where each key matches the given rules. """ if isinstance(rules, Matcher): rules = [rules] @@ -897,6 +940,9 @@ def each_value_matches( rules: The matching rules to match against each value. + + Returns: + Matcher for dictionaries where each value matches the given rules. """ if isinstance(rules, Matcher): rules = [rules] diff --git a/src/pact/match/matcher.py b/src/pact/match/matcher.py index 9d3f4795c..acba811e5 100644 --- a/src/pact/match/matcher.py +++ b/src/pact/match/matcher.py @@ -140,6 +140,9 @@ def __init__( def has_value(self) -> bool: """ Check if the matcher has a value. + + Returns: + True if the matcher has a value, otherwise False. """ return not isinstance(self.value, Unset) @@ -155,8 +158,7 @@ def to_integration_json(self) -> dict[str, Any]: > https://docs.pact.io/implementation_guides/rust/pact_ffi/integrationjson Returns: - dict[str, Any]: - The matcher as an integration JSON object. + The matcher as an integration JSON object. """ return { "pact:matcher:type": self.type, @@ -185,8 +187,7 @@ def to_matching_rule(self) -> dict[str, Any]: > https://github.com/pact-foundation/pact-specification/tree/version-2?tab=readme-ov-file#matchers Returns: - dict[str, Any]: - The matcher as a matching rule. + The matcher as a matching rule. """ return { "match": self.type, @@ -302,6 +303,13 @@ class MatchingRuleJSONEncoder(JSONEncoder): def default(self, o: Any) -> Any: # noqa: ANN401 """ Encode the object to JSON. + + Args: + o: + The object to encode. + + Returns: + The encoded object. """ if isinstance(o, Matcher): return o.to_matching_rule() @@ -318,6 +326,13 @@ class IntegrationJSONEncoder(JSONEncoder): def default(self, o: Any) -> Any: # noqa: ANN401 """ Encode the object to JSON. + + Args: + o: + The object to encode. + + Returns: + The encoded object. """ if isinstance(o, Matcher): return o.to_integration_json() diff --git a/src/pact/pact.py b/src/pact/pact.py index bbd058165..bc5469871 100644 --- a/src/pact/pact.py +++ b/src/pact/pact.py @@ -133,6 +133,10 @@ def __init__( provider: Name of the provider. + + Raises: + ValueError: + If the consumer or provider name is empty. """ if not consumer: msg = "Consumer name cannot be empty." @@ -202,7 +206,7 @@ def with_specification( Args: version: - Pact specification version. The can be either a string or a + Pact specification version. This can be either a string or a [`PactSpecification`][pact_ffi.PactSpecification] instance. The version string is case insensitive and has an optional `v` @@ -289,6 +293,10 @@ def upon_receiving( interaction: Type of interaction. Defaults to `HTTP`. This must be one of `HTTP`, `Async`, or `Sync`. + + Raises: + ValueError: + If the interaction type is invalid. """ if interaction == "HTTP": return HttpInteraction(self._handle, description) @@ -347,7 +355,7 @@ def serve( # noqa: PLR0913 independently of `raises`. Returns: - A [`PactServer`][pact.pact.PactServer] instance. + PactServer instance. """ return PactServer( self._handle, @@ -390,6 +398,10 @@ def interactions( The kind is used to specify the type of interactions that will be iterated over. + + Raises: + ValueError: + If the kind is unknown. """ # TODO: Add an iterator for `All` interactions. # https://github.com/pact-foundation/pact-python/issues/451 @@ -460,6 +472,17 @@ def verify( Whether or not to raise an exception if the handler fails to process a message. If set to `False`, then the function will return a list of errors. + + Returns: + `None` if raises is True and no errors occurred, otherwise a list of + [`InteractionVerificationError`][pact.error.InteractionVerificationError]. + + Raises: + TypeError: + If the message type is unknown. + + PactVerificationError: + If raises is True and there are errors. """ errors: list[InteractionVerificationError] = [] for message in self.interactions(kind): @@ -536,7 +559,7 @@ class PactServer: stopping the mock server when the block is exited. Note that the server should not be started directly, but rather through the - [`serve(...)`][pact.Pact.serve] method of a [`Pact`][pact.Pact]: + [`serve`][pact.Pact.serve] method of a [`Pact`][pact.Pact]: ```python pact = Pact("consumer", "provider") @@ -581,7 +604,7 @@ def __init__( # noqa: PLR0913 Handle for the Pact. host: - Hostname of IP for the mock server. + Hostname or IP for the mock server. port: Port to bind the mock server to. The value of `None` will select diff --git a/src/pact/verifier.py b/src/pact/verifier.py index c9c84a8dd..86b24a501 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -266,6 +266,11 @@ def add_transport( This is typically only used for the `http` protocol, where this value can either be `http` (the default) or `https`. + + Raises: + ValueError: + If mutually exclusive parameters are provided, or required + parameters are missing, or host/protocol mismatches. """ if url and any(x is not None for x in (protocol, port, path, scheme)): msg = "The `url` parameter is mutually exclusive with other parameters" @@ -357,8 +362,14 @@ def message_handler( Args: handler: - The message handler. This should be a callable that takes no - arguments: the + The message handler. + + This should be a callable or a dictionary mapping message names + to callables, Message dicts, or bytes. + + Raises: + TypeError: + If the handler or its values are invalid. """ logger.debug( "Setting message handler for verifier", @@ -441,13 +452,16 @@ def filter( Args: description: - The interaction description. This should be a regular - expression. If unspecified, no filtering will be done based on - the description. + The interaction description. + + This should be a regular expression. If unspecified, no + filtering will be done based on the description. state: - The interaction state. This should be a regular expression. If - unspecified, no filtering will be done based on the state. + The interaction state. + + This should be a regular expression. If unspecified, no + filtering will be done based on the state. no_state: Whether to include interactions with no state. @@ -553,6 +567,13 @@ def state_handler( or in the query string (`False`). This must be left as `None` if providing one or more handler functions; and it must be set to a boolean if providing a URL. + + Raises: + ValueError: + If the handler/body combination is invalid. + + TypeError: + If the handler type is invalid. """ # A tuple is required instead of `StateHandlerUrl` for support for # Python 3.9. This should be changed to `StateHandlerUrl` in the future. @@ -604,6 +625,10 @@ def _state_handler_url( Returns: The verifier instance. + + Raises: + ValueError: + If the body parameter is not a boolean when providing a URL. """ logger.debug( "Setting URL state handler for verifier", @@ -646,6 +671,10 @@ def _state_handler_dict( Returns: The verifier instance. + + Raises: + TypeError: + If any value in the dictionary is not callable. """ if any(not callable(f) for f in handler.values()): msg = "All values in the dictionary must be callable" @@ -705,6 +734,10 @@ def _set_function_state_handler( Returns: The verifier instance. + + Raises: + TypeError: + If the handler is not callable. """ logger.debug( "Setting function state handler for verifier", @@ -753,6 +786,10 @@ def set_request_timeout(self, timeout: int) -> Self: Args: timeout: The request timeout in milliseconds. + + Raises: + ValueError: + If the timeout is negative. """ if timeout < 0: msg = "Request timeout must be a positive integer" @@ -854,9 +891,11 @@ def add_custom_headers( Args: headers: - The headers to add. This can be a dictionary or an iterable of - key-value pairs. The iterable is preferred as it ensures that - repeated headers are not lost. + The headers to add. + + The value can be: + - a dictionary of header key-value pairs + - an iterable of (key, value) tuples """ if isinstance(headers, dict): headers = headers.items() @@ -895,12 +934,11 @@ def add_source( Args: source: - The source of the interactions. This may be either of the - following: + The source of the interactions. This may be either of the following: - - A local file path to a Pact file. - - A local file path to a directory containing Pact files. - - A URL to a Pact file. + - A local file path to a Pact file. + - A local file path to a directory containing Pact files. + - A URL to a Pact file. If using a URL, the `username` and `password` parameters can be used to provide basic HTTP authentication, or the `token` @@ -920,6 +958,10 @@ def add_source( The token to use for bearer token authentication. This is only used when the source is a URL. Note that this is mutually exclusive with `username` and `password`. + + Raises: + ValueError: + If the source scheme is invalid. """ if isinstance(source, Path): return self._add_source_local(source) @@ -964,6 +1006,10 @@ def _add_source_local(self, source: str | Path) -> Self: - A local file path to a Pact file. - A local file path to a directory containing Pact files. + + Raises: + ValueError: + If the source is not a file or directory. """ source = Path(source) if source.is_dir(): @@ -1007,6 +1053,10 @@ def _add_source_remote( The token to use for bearer token authentication. This is mutually exclusive with `username` and `password` (whether they be specified through arguments, or embedded in the URL). + + Raises: + ValueError: + If mutually exclusive authentication parameters are provided. """ url = URL(url) @@ -1085,7 +1135,7 @@ def broker_source( Args: url: - The broker URL. TThe URL may contain a username and password for + The broker URL. The URL may contain a username and password for basic HTTP authentication. username: @@ -1103,6 +1153,10 @@ def broker_source( selector: Whether to return a BrokerSelectorBuilder instance. + + Raises: + ValueError: + If mutually exclusive authentication parameters are provided. """ url = URL(url) @@ -1145,6 +1199,10 @@ def verify(self) -> Self: Returns: Whether the interactions were verified successfully. + + Raises: + RuntimeError: + If no transports have been set. """ if not self._transports: msg = "No transports have been set" From e71af3b4d8cf1a35093878b224ffb2ae0fa665a0 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 6 Aug 2025 10:32:06 +1000 Subject: [PATCH 0911/1376] fix: matcher type variance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If `A ⊑ B`, then in most cases `Matcher[A] ⊑ Matcher[B]`. The only exception is the key matcher, as a dictionary is invariant on its key type. Signed-off-by: JP-Ellis --- src/pact/interaction/_http_interaction.py | 5 +++-- src/pact/match/matcher.py | 23 ++++++++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/pact/interaction/_http_interaction.py b/src/pact/interaction/_http_interaction.py index 57dd1f2b2..d9342d12f 100644 --- a/src/pact/interaction/_http_interaction.py +++ b/src/pact/interaction/_http_interaction.py @@ -6,6 +6,7 @@ import json from collections import defaultdict +from collections.abc import Mapping from typing import TYPE_CHECKING, Literal import pact_ffi @@ -358,7 +359,7 @@ def with_query_parameter( def with_query_parameters( self, - parameters: dict[str, object | Matcher[object]] + parameters: Mapping[str, object | Matcher[object]] | Iterable[tuple[str, object | Matcher[object]]], ) -> Self: """ @@ -384,7 +385,7 @@ def with_query_parameters( parameters: Query parameters to add to the request. """ - if isinstance(parameters, dict): + if isinstance(parameters, Mapping): parameters = parameters.items() for name, value in parameters: self.with_query_parameter(name, value) diff --git a/src/pact/match/matcher.py b/src/pact/match/matcher.py index acba811e5..95fd672f4 100644 --- a/src/pact/match/matcher.py +++ b/src/pact/match/matcher.py @@ -18,10 +18,11 @@ from pact.generate.generator import Generator from pact.types import UNSET, Matchable, MatcherType, Unset +_T_co = TypeVar("_T_co", covariant=True) _T = TypeVar("_T") -class Matcher(ABC, Generic[_T]): +class Matcher(ABC, Generic[_T_co]): """ Abstract matcher. @@ -75,7 +76,7 @@ def to_matching_rule(self) -> dict[str, Any]: """ -class GenericMatcher(Matcher[_T]): +class GenericMatcher(Matcher[_T_co]): """ Generic matcher. @@ -87,7 +88,7 @@ def __init__( self, type: MatcherType, # noqa: A002 /, - value: _T | Unset = UNSET, + value: _T_co | Unset = UNSET, generator: Generator | None = None, extra_fields: Mapping[str, Any] | None = None, **kwargs: Matchable, @@ -123,7 +124,7 @@ def __init__( The type of the matcher. """ - self.value: _T | Unset = value + self.value: _T_co | Unset = value """ Default value used by Pact when executing tests. """ @@ -196,14 +197,14 @@ def to_matching_rule(self) -> dict[str, Any]: } -class ArrayContainsMatcher(Matcher[Sequence[_T]]): +class ArrayContainsMatcher(Matcher[Sequence[_T_co]]): """ Array contains matcher. A matcher that checks if an array contains a value. """ - def __init__(self, variants: Sequence[_T | Matcher[_T]]) -> None: + def __init__(self, variants: Sequence[_T_co | Matcher[_T_co]]) -> None: """ Initialize the matcher. @@ -211,7 +212,7 @@ def __init__(self, variants: Sequence[_T | Matcher[_T]]) -> None: variants: List of possible values to match against. """ - self._matcher: Matcher[Sequence[_T]] = GenericMatcher( + self._matcher: Matcher[Sequence[_T_co]] = GenericMatcher( "arrayContains", extra_fields={"variants": variants}, ) @@ -258,7 +259,7 @@ def to_matching_rule(self) -> dict[str, Any]: # noqa: D102 return self._matcher.to_matching_rule() -class EachValueMatcher(Matcher[Mapping[Matchable, _T]]): +class EachValueMatcher(Matcher[Mapping[Matchable, _T_co]]): """ Each value matcher. @@ -267,8 +268,8 @@ class EachValueMatcher(Matcher[Mapping[Matchable, _T]]): def __init__( self, - value: Mapping[Matchable, _T], - rules: list[Matcher[_T]] | None = None, + value: Mapping[Matchable, _T_co], + rules: list[Matcher[_T_co]] | None = None, ) -> None: """ Initialize the matcher. @@ -280,7 +281,7 @@ def __init__( rules: List of matchers to apply to each value in the mapping. """ - self._matcher: Matcher[Mapping[Matchable, _T]] = GenericMatcher( + self._matcher: Matcher[Mapping[Matchable, _T_co]] = GenericMatcher( "eachValue", value=value, extra_fields={"rules": rules}, From 3ccb493c8480ee758b697d38db97821d51395769 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Aug 2025 10:01:58 +1000 Subject: [PATCH 0912/1376] chore(deps): fix optional dependencies - The `pytest-mock` is not used in testing - The `typing-extensions` was missing Signed-off-by: JP-Ellis --- pact-python-ffi/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 3da6901e6..c33166d35 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -49,8 +49,8 @@ dependencies = ["cffi~=1.0"] "pact-python-ffi[devel-types]", "ruff==0.12.7", ] - devel-test = ["pytest-cov~=6.0", "pytest-mock~=3.0", "pytest~=8.0"] - devel-types = ["mypy==1.17.1"] + devel-test = ["pytest-cov~=6.0", "pytest~=8.0"] + devel-types = ["mypy==1.17.1", "typing-extensions~=4.0"] ################################################################################ ## Build System From 4c4adcdaa4e01530cf784f94b81c7c0937ce681e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Aug 2025 10:05:55 +1000 Subject: [PATCH 0913/1376] chore(deps): remove unused optional dependency Signed-off-by: JP-Ellis --- pact-python-cli/pyproject.toml | 2 +- pact-python-cli/tests/test_init.py | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 644426d67..4207f1d4f 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -56,7 +56,7 @@ requires-python = ">=3.9" "pact-python-cli[devel-types]", "ruff==0.12.7", ] - devel-test = ["pytest-cov~=6.0", "pytest-mock~=3.0", "pytest~=8.0"] + devel-test = ["pytest-cov~=6.0", "pytest~=8.0"] devel-types = ["mypy==1.17.1"] ################################################################################ diff --git a/pact-python-cli/tests/test_init.py b/pact-python-cli/tests/test_init.py index 8f131fc5b..2ac7da782 100644 --- a/pact-python-cli/tests/test_init.py +++ b/pact-python-cli/tests/test_init.py @@ -8,17 +8,12 @@ import sys import time from pathlib import Path -from typing import TYPE_CHECKING +from unittest.mock import patch import pytest import pact_cli -if TYPE_CHECKING: - from unittest.mock import MagicMock - - import pytest_mock - def bin_to_sitepackages(exec_path: str | Path) -> Path: """ @@ -151,24 +146,29 @@ def test_exec_wrapper_mock_service() -> None: pytest.param("pactflow", id="pactflow"), ], ) -def test_exec_directly(executable: str, mocker: pytest_mock.MockerFixture) -> None: +def test_exec_directly(executable: str) -> None: """ Test pact_cli._exec with --help, mocking sys.argv and capturing output. """ cmd: str args: list[str] - mocker.patch.object(sys, "argv", new=[executable, "--help"]) - mock_execv: MagicMock = mocker.patch("os.execv") - pact_cli._exec() # noqa: SLF001 + with ( + patch.object(sys, "argv", new=[executable, "--help"]), + patch("os.execv") as mock_execv, + ): + pact_cli._exec() # noqa: SLF001 mock_execv.assert_called_once() cmd, args = mock_execv.call_args[0] assert (os.sep + executable) in cmd assert args == [cmd, "--help"] - mocker.patch.object(sys, "argv", new=[executable]) - mock_execv.reset_mock() - pact_cli._exec() # noqa: SLF001 + patch.object(sys, "argv", new=[executable]) + with ( + patch.object(sys, "argv", new=[executable]), + patch("os.execv") as mock_execv, + ): + pact_cli._exec() # noqa: SLF001 mock_execv.assert_called_once() cmd, args = mock_execv.call_args[0] assert (os.sep + executable) in cmd From aff5e708c719191c75b937fda44bfee57ac38d3f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Aug 2025 11:06:11 +1000 Subject: [PATCH 0914/1376] feat!: simplify `given` Instead of having the overloaded function: - `given(state, *, key, value)` - `given(state, *, parameters)` The `given` function's new signature is: - `given(state, parameters, /, **kwargs)` In this form, the `state` and (optional) `parameters` must _always_ be positional, and all keyword arguments get used as parameters. This allows for the `parameters` key to be used in the kwargs. BREAKING CHANGE: The signature of `Interaction.given` has been updated. The following changes are required: - Change `given("state", key="user_id", value=123)` to `given("state", user_id=123)`. This can take an arbitrary number of keyword arguments. If the key is not a valid Python keyword argument, use the dictionary input below. - Change `given("state", parameters={"user_id": 123})` to `given("state", {"user_id": 123})`. Signed-off-by: JP-Ellis --- examples/plugins/protobuf/test_consumer.py | 4 +- src/pact/interaction/_base.py | 116 ++++++------------ tests/compatibility_suite/test_v3_consumer.py | 2 +- .../test_v3_message_consumer.py | 2 +- .../util/interaction_definition.py | 4 +- tests/test_http_interaction.py | 8 +- tests/test_match.py | 2 +- 7 files changed, 46 insertions(+), 92 deletions(-) diff --git a/examples/plugins/protobuf/test_consumer.py b/examples/plugins/protobuf/test_consumer.py index 08559d6be..681eadfcd 100644 --- a/examples/plugins/protobuf/test_consumer.py +++ b/examples/plugins/protobuf/test_consumer.py @@ -77,7 +77,7 @@ def test_get_person_by_id(pact: Pact) -> None: ( pact.upon_receiving("a request to get person by ID") - .given("person with the given ID exists", parameters={"user_id": 1}) + .given("person with the given ID exists", user_id=1) .with_request("GET", "/person/1") .will_respond_with(200) .with_header("Content-Type", "application/x-protobuf") @@ -120,7 +120,7 @@ def test_get_nonexistent_person(pact: Pact) -> None: """ ( pact.upon_receiving("a request to get non-existent person") - .given("person with the given ID does not exist", parameters={"user_id": 999}) + .given("person with the given ID does not exist", user_id=999) .with_request("GET", "/person/999") .will_respond_with(404) .with_header("Content-Type", "application/json") diff --git a/src/pact/interaction/_base.py b/src/pact/interaction/_base.py index fece0e1f7..036239f96 100644 --- a/src/pact/interaction/_base.py +++ b/src/pact/interaction/_base.py @@ -13,7 +13,7 @@ import abc import json -from typing import TYPE_CHECKING, Any, Literal, overload +from typing import TYPE_CHECKING, Any, Literal import pact_ffi from pact.match.matcher import IntegrationJSONEncoder @@ -117,22 +117,12 @@ def _parse_interaction_part( msg = f"Invalid part: {part}" raise ValueError(msg) - @overload - def given(self, state: str) -> Self: ... - - @overload - def given(self, state: str, *, name: str, value: str) -> Self: ... - - @overload - def given(self, state: str, *, parameters: dict[str, Any] | str) -> Self: ... - def given( self, state: str, - *, - name: str | None = None, - value: str | None = None, - parameters: dict[str, Any] | str | None = None, + parameters: dict[str, object] | None = None, + /, + **kwargs: object, ) -> Self: """ Set the provider state. @@ -149,88 +139,52 @@ def given( pact.upon_receiving("a request").given("a user exists") ``` - It is also possible to specify a parameter that will be used to match - the provider state. For example, to match a provider state of `a user - exists` with a parameter `id` that has the value `123`, you would use: - - ```python - ( - pact.upon_receiving("a request").given( - "a user exists", - name="id", - value="123", - ) - ) - ``` - - Lastly, it is possible to specify multiple parameters that will be used - to match the provider state. For example, to match a provider state of - `a user exists` with a parameter `id` that has the value `123` and a - parameter `name` that has the value `John`, you would use: + In many circumstances, it is useful to parameterize the state with + additional data. In the example above, this could be with: ```python - ( - pact.upon_receiving("a request").given( - "a user exists", - parameters={ - "id": "123", - "name": "John", - }, - ) + pact.upon_receiving("a request").given( + "a user exists", + id=123, + name="Alice", ) ``` This function can be called repeatedly to specify multiple provider - states for the same Interaction. If the same `state` is specified with - different parameters, then the parameters are merged together. The above - example with multiple parameters can equivalently be specified as: - - ```python - ( - pact.upon_receiving("a request") - .given("a user exists", name="id", value="123") - .given("a user exists", name="name", value="John") - ) - ``` + states for the same Interaction. If the same state is specified with + different parameters, then the parameters are merged together. Args: state: Provider state for the Interaction. - name: - Name of the parameter. This must be specified in conjunction - with `value`. - - value: - Value of the parameter. This must be specified in conjunction - with `name`. - parameters: - Key-value pairs of parameters to use for the provider state. - These must be encodable using [`json.dumps`][json.dumps]. - Alternatively, a string containing the JSON object can be passed - directly. - - Raises: - ValueError: - If the combination of arguments is invalid or inconsistent. - """ - if name is not None and value is not None and parameters is None: - pact_ffi.given_with_param(self._handle, state, name, value) - elif name is None and value is None and parameters is not None: - if isinstance(parameters, dict): - pact_ffi.given_with_params( - self._handle, - state, - json.dumps(parameters), + Should some of the parameters not be valid Python key + identifiers, a dictionary can be passed in as the second + positional argument. + + ```python + pact.upon_receiving("A user request").given( + "The given user exists", + {"user-id": 123}, ) - else: - pact_ffi.given_with_params(self._handle, state, parameters) - elif name is None and value is None and parameters is None: + ``` + + kwargs: + The additional parameters for the provider state, specified as + additional arguments to the function. The values must be + serializable using Python's [`json.dumps`][json.dumps] + function. + """ + if not parameters and not kwargs: pact_ffi.given(self._handle, state) else: - msg = "Invalid combination of arguments." - raise ValueError(msg) + pact_ffi.given_with_params( + self._handle, + state, + json.dumps({**(parameters or {}), **kwargs}), + ) + return self def with_body( diff --git a/tests/compatibility_suite/test_v3_consumer.py b/tests/compatibility_suite/test_v3_consumer.py index d40b6bd12..19e044000 100644 --- a/tests/compatibility_suite/test_v3_consumer.py +++ b/tests/compatibility_suite/test_v3_consumer.py @@ -99,7 +99,7 @@ def a_provider_state_is_specified_with_the_following_data( elif value.replace(".", "", 1).isdigit(): row[key] = float(value) - pact_interaction.interaction.given(state, parameters=data[0]) + pact_interaction.interaction.given(state, data[0]) ################################################################################ diff --git a/tests/compatibility_suite/test_v3_message_consumer.py b/tests/compatibility_suite/test_v3_message_consumer.py index 34ae8e802..2a1f52c2e 100644 --- a/tests/compatibility_suite/test_v3_message_consumer.py +++ b/tests/compatibility_suite/test_v3_message_consumer.py @@ -162,7 +162,7 @@ def a_provider_state_for_the_message_is_specified_with_the_following_data( table = parse_horizontal_table(datatable) logger.debug("Specifying provider state '%s' with data: %s", state, table) parameters = {k: ast.literal_eval(v) for k, v in table[0].items()} - pact_interaction.interaction.given(state, parameters=parameters) + pact_interaction.interaction.given(state, parameters) @given("a message is defined") diff --git a/tests/compatibility_suite/util/interaction_definition.py b/tests/compatibility_suite/util/interaction_definition.py index 50d8ada4c..4a3a8c30b 100644 --- a/tests/compatibility_suite/util/interaction_definition.py +++ b/tests/compatibility_suite/util/interaction_definition.py @@ -517,8 +517,8 @@ def add_to_pact( # noqa: C901, PLR0912, PLR0915 for state in self.states or []: if state.parameters: - logger.info("given(%r, parameters=%r)", state.name, state.parameters) - interaction.given(state.name, parameters=state.parameters) + logger.info("given(%r, %r)", state.name, state.parameters) + interaction.given(state.name, state.parameters) else: logger.info("given(%r)", state.name) interaction.given(state.name) diff --git a/tests/test_http_interaction.py b/tests/test_http_interaction.py index 747b05237..695ab835a 100644 --- a/tests/test_http_interaction.py +++ b/tests/test_http_interaction.py @@ -446,14 +446,14 @@ async def test_given(pact: Pact) -> None: ) ( pact.upon_receiving("a basic request given a user exists (1)") - .given("a user exists", name="id", value="123") - .given("a user exists", name="name", value="John") + .given("a user exists", id=123) + .given("a user exists", name="John") .with_request("GET", "/user1") .will_respond_with(201) ) ( pact.upon_receiving("a basic request given a user exists (2)") - .given("a user exists", parameters={"id": "123", "name": "John"}) + .given("a user exists", {"id": "123", "name": "John"}) .with_request("GET", "/user2") .will_respond_with(202) ) @@ -590,7 +590,7 @@ async def test_pact_server_verbose( .will_respond_with(200) ) with ( - caplog.at_level(logging.WARNING, logger="pact.v3.pact"), + caplog.at_level(logging.WARNING, logger="pact.pact"), pact.serve(raises=False, verbose=True) as srv, ): async with aiohttp.ClientSession(srv.url) as session: diff --git a/tests/test_match.py b/tests/test_match.py index e10651977..f44e2aea5 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -153,7 +153,7 @@ def test_matchers() -> None: pact = Pact("consumer", "provider").with_specification("V4") ( pact.upon_receiving("a request") - .given("a state", parameters={"providerStateArgument": "providerStateValue"}) + .given("a state", {"providerStateArgument": "providerStateValue"}) .with_request("GET", match.regex("/path/to/100", regex=r"/path/to/\d{1,4}")) .with_query_parameter( "asOf", From d8df22eba9b568ac64ab0e8696a84534e4b75b7f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Aug 2025 13:30:09 +1000 Subject: [PATCH 0915/1376] chore(tests): re-organise tests To reflect the structure within `src/*`. Signed-off-by: JP-Ellis --- tests/.ruff.toml | 1 + .../test_async_message_interaction.py} | 0 tests/{ => interaction}/test_http_interaction.py | 0 .../test_sync_message_interaction.py} | 0 4 files changed, 1 insertion(+) rename tests/{test_async_interaction.py => interaction/test_async_message_interaction.py} (100%) rename tests/{ => interaction}/test_http_interaction.py (100%) rename tests/{test_sync_interaction.py => interaction/test_sync_message_interaction.py} (100%) diff --git a/tests/.ruff.toml b/tests/.ruff.toml index 393a4247b..4fcff4b2c 100644 --- a/tests/.ruff.toml +++ b/tests/.ruff.toml @@ -5,6 +5,7 @@ extend = "../pyproject.toml" ignore = [ "D103", # Require docstring in public function "D104", # Require docstring in public package + "INP001", # Forbid implicit namespaces "PLR2004", # Forbid Magic Numbers "S101", # Forbid assert statements "TID252", # Require absolute imports diff --git a/tests/test_async_interaction.py b/tests/interaction/test_async_message_interaction.py similarity index 100% rename from tests/test_async_interaction.py rename to tests/interaction/test_async_message_interaction.py diff --git a/tests/test_http_interaction.py b/tests/interaction/test_http_interaction.py similarity index 100% rename from tests/test_http_interaction.py rename to tests/interaction/test_http_interaction.py diff --git a/tests/test_sync_interaction.py b/tests/interaction/test_sync_message_interaction.py similarity index 100% rename from tests/test_sync_interaction.py rename to tests/interaction/test_sync_message_interaction.py From 456ed37984d457173beb1e6e05c20c12f5e25138 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Aug 2025 14:58:43 +1000 Subject: [PATCH 0916/1376] fix: with metadata function signature The use of double-underscores was unnecessary, and did not work as intended. Instead, the `/` separator ensures that positional-only keys don't clash with explicit keyword arguments. The documentation has been adjusted to reflect this. Also, the `set_metadata` variant has been added to allow for raw values to be passed without further encoding/decoding. Signed-off-by: JP-Ellis --- src/pact/interaction/_base.py | 83 ++++++++++++++----- .../test_sync_message_interaction.py | 71 +++++++++++++++- 2 files changed, 130 insertions(+), 24 deletions(-) diff --git a/src/pact/interaction/_base.py b/src/pact/interaction/_base.py index 036239f96..7ae068111 100644 --- a/src/pact/interaction/_base.py +++ b/src/pact/interaction/_base.py @@ -259,20 +259,20 @@ def with_binary_body( def with_metadata( self, - __metadata: dict[str, str] | None = None, - __part: Literal["Request", "Response"] | None = None, + metadata: dict[str, object | Matcher[object]] | None = None, + part: Literal["Request", "Response"] | None = None, /, - **kwargs: str, + **kwargs: object | Matcher[object], ) -> Self: """ - Set metadata for the interaction. + Add metadata for the interaction. This function may either be called with a single dictionary of metadata, or with keyword arguments that are the key-value pairs of the metadata (or a combination thereof): ```python - interaction.with_metadata({"key": "value", "key two": "value two"}) + interaction.with_metadata({"foo": "bar", "baz": "qux"}) interaction.with_metadata(foo="bar", baz="qux") ``` @@ -281,22 +281,17 @@ def with_metadata( JSON `null` value, which will set the metadata key to an empty string or the JSON `null` value, respectively. - !!! note - - There are two special keys which cannot be used as keyword - arguments: `__metadata` and `__part`. Should there ever be a need - to set metadata with one of these keys, they must be passed through - as a dictionary: - - ```python - interaction.with_metadata({"__metadata": "value", "__part": 1}) - ``` + The values must be serializable to JSON using [`json.dumps`][json.dumps] + and may contain matchers and generators. If you wish to use a valid + JSON-encoded string as a metadata value, prefer the + [`set_metadata`][pact.interaction.Interaction.set_metadata] method as + this does not perform any additional parsing of the string. Args: - __metadata: + metadata: Dictionary of metadata keys and associated values. - __part: + part: Whether the metadata should be added to the request or the response. If `None`, then the function intelligently determines whether the body should be added to the request or the response. @@ -307,23 +302,65 @@ def with_metadata( Returns: The current instance of the interaction. """ - part = self._parse_interaction_part(__part) - for k, v in (__metadata or {}).items(): + interaction_part = self._parse_interaction_part(part) + for k, v in (metadata or {}).items(): pact_ffi.with_metadata( self._handle, k, - v, - part, + json.dumps(v, cls=IntegrationJSONEncoder), + interaction_part, ) for k, v in kwargs.items(): pact_ffi.with_metadata( self._handle, k, - v, - part, + json.dumps(v, cls=IntegrationJSONEncoder), + interaction_part, ) return self + def set_metadata( + self, + metadata: dict[str, str] | None = None, + part: Literal["Request", "Response"] | None = None, + /, + **kwargs: str, + ) -> Self: + """ + Add metadata for the interaction. + + This function behaves exactly like + [`with_metadata`][pact.interaction.Interaction.with_metadata] but does + not perform any parsing of the value strings. The strings must be valid + JSON-encoded strings. + + The value of `None` will remove the metadata key from the interaction. + This is distinct from using an empty string or a string containing the + JSON `null` value, which will set the metadata key to an empty string + or the JSON `null` value, respectively. + + Args: + metadata: + Dictionary of metadata keys and associated values. + + part: + Whether the metadata should be added to the request or the + response. If `None`, then the function intelligently determines + whether the body should be added to the request or the response. + + **kwargs: + Additional metadata key-value pairs. + + Returns: + The current instance of the interaction. + """ + interaction_part = self._parse_interaction_part(part) + for k, v in (metadata or {}).items(): + pact_ffi.with_metadata(self._handle, k, v, interaction_part) + for k, v in kwargs.items(): + pact_ffi.with_metadata(self._handle, k, v, interaction_part) + return self + def with_multipart_file( self, part_name: str, diff --git a/tests/interaction/test_sync_message_interaction.py b/tests/interaction/test_sync_message_interaction.py index 761d8b328..28a709af4 100644 --- a/tests/interaction/test_sync_message_interaction.py +++ b/tests/interaction/test_sync_message_interaction.py @@ -5,6 +5,7 @@ from __future__ import annotations import re +from unittest.mock import MagicMock import pytest @@ -16,7 +17,7 @@ def pact() -> Pact: """ Fixture for a Pact instance. """ - return Pact("consumer", "provider") + return Pact("consumer", "provider").with_specification("V4") def test_str(pact: Pact) -> None: @@ -33,3 +34,71 @@ def test_repr(pact: Pact) -> None: ) is not None ) + + +def test_with_metadata_with_positional_dict(pact: Pact) -> None: + ( + pact.upon_receiving("with_metadatadict", "Sync") + .with_body("request", content_type="text/plain") + .with_metadata({"foo": "bar"}) + .will_respond_with() + .with_body("response", content_type="text/plain") + ) + handler = MagicMock() + handler.return_value = "response" + pact.verify(handler, "Sync") + handler.assert_called_once() + assert "foo" in handler.call_args[0][1] + assert handler.call_args[0][1]["foo"] == "bar" + + +def test_with_metadata_with_keyword_args(pact: Pact) -> None: + ( + pact.upon_receiving("with_metadata_kwargs", "Sync") + .with_body("request", content_type="text/plain") + .with_metadata(foo="bar") + .will_respond_with() + .with_body("response", content_type="text/plain") + ) + handler = MagicMock() + handler.return_value = "response" + pact.verify(handler, "Sync") + handler.assert_called_once() + assert "foo" in handler.call_args[0][1] + assert handler.call_args[0][1]["foo"] == "bar" + + +def test_with_metadata_with_mixed_args(pact: Pact) -> None: + ( + pact.upon_receiving("with_metadata_mixed", "Sync") + .with_body("request", content_type="text/plain") + .with_metadata({"foo": {"bar": 1.23}}, metadata=123) + .will_respond_with() + .with_body("response", content_type="text/plain") + ) + handler = MagicMock() + handler.return_value = "response" + pact.verify(handler, "Sync") + handler.assert_called_once() + assert "foo" in handler.call_args[0][1] + assert handler.call_args[0][1]["foo"] == {"bar": 1.23} + assert "metadata" in handler.call_args[0][1] + assert handler.call_args[0][1]["metadata"] == 123 + + +def test_with_metadata_with_part(pact: Pact) -> None: + ( + pact.upon_receiving("with_metadata_part", "Sync") + .with_body("request", content_type="text/plain") + .will_respond_with() + .with_body("response", content_type="text/plain") + .with_metadata({"foo": {"bar": 1.23}}, "Request", metadata=123) + ) + handler = MagicMock() + handler.return_value = "response" + pact.verify(handler, "Sync") + handler.assert_called_once() + assert "foo" in handler.call_args[0][1] + assert handler.call_args[0][1]["foo"] == {"bar": 1.23} + assert "metadata" in handler.call_args[0][1] + assert handler.call_args[0][1]["metadata"] == 123 From 1564167e1f238be5e00e7f81d77207e48fe457e1 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Aug 2025 15:10:08 +1000 Subject: [PATCH 0917/1376] fix: allow none in with_metadata Signed-off-by: JP-Ellis --- pact-python-ffi/src/pact_ffi/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index e5e761f35..abb665d07 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -5820,7 +5820,7 @@ def with_pact_metadata( def with_metadata( interaction: InteractionHandle, key: str, - value: str, + value: str | None, part: InteractionPart, ) -> None: r""" @@ -5841,8 +5841,11 @@ def with_metadata( To include matching rules for the value, include the matching rule JSON format with the value as a single JSON document. I.e. - ```python with_metadata( - handle, "TagData", json.dumps({ + ```python + with_metadata( + handle, + "TagData", + json.dumps({ "value": {"ID": "sjhdjkshsdjh", "weight": 100.5}, "pact:matcher:type": "type", }), @@ -5881,7 +5884,7 @@ def with_metadata( success: bool = lib.pactffi_with_metadata( interaction._ref, key.encode("utf-8"), - value.encode("utf-8"), + value.encode("utf-8") if value is not None else ffi.NULL, part.value, ) if not success: From aeb3754232a3b41d14351b3f5e2293d58c5c8e05 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Aug 2025 15:44:35 +1000 Subject: [PATCH 0918/1376] feat!: deserialize metadata values Previously, all metadata values were returned as strings, irrespective of their original type specified through `with_metadata`. This behaviour has been fixed and Pact Python attempts to deserialise metadata. This works by attempting to parse the value as JSON, falling back to the original string if the deserialisation fails. BREAKING CHANGE: As the metadata values are now deserialised, the type of the metadata values may change. For example, setting metadata `user_id=123` will now pass `{"user_id": 123}` throug to the function handler. Previously, this would have been `{"user_id": "123"}`. Signed-off-by: JP-Ellis --- src/pact/pact.py | 15 +++++++++++---- .../test_v3_message_consumer.py | 11 +++++++---- .../util/interaction_definition.py | 6 +----- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/pact/pact.py b/src/pact/pact.py index bc5469871..f2a2a704f 100644 --- a/src/pact/pact.py +++ b/src/pact/pact.py @@ -62,6 +62,7 @@ from __future__ import annotations +import json import logging import warnings from pathlib import Path @@ -419,7 +420,7 @@ def interactions( @overload def verify( self, - handler: Callable[[str | bytes | None, dict[str, str]], None], + handler: Callable[[str | bytes | None, dict[str, object]], None], kind: Literal["Async", "Sync"], *, raises: Literal[True] = True, @@ -427,7 +428,7 @@ def verify( @overload def verify( self, - handler: Callable[[str | bytes | None, dict[str, str]], None], + handler: Callable[[str | bytes | None, dict[str, object]], None], kind: Literal["Async", "Sync"], *, raises: Literal[False], @@ -435,7 +436,7 @@ def verify( def verify( self, - handler: Callable[[str | bytes | None, dict[str, str]], None], + handler: Callable[[str | bytes | None, dict[str, object]], None], kind: Literal["Async", "Sync"], *, raises: bool = True, @@ -503,7 +504,13 @@ def verify( continue body = request.contents - metadata = {pair.key: pair.value for pair in request.metadata} + metadata: dict[str, object] = {} + for pair in request.metadata: + try: + v = json.loads(pair.value) + except json.JSONDecodeError: + v = pair.value + metadata[pair.key] = v try: handler(body, metadata) diff --git a/tests/compatibility_suite/test_v3_message_consumer.py b/tests/compatibility_suite/test_v3_message_consumer.py index 2a1f52c2e..8d6f413f0 100644 --- a/tests/compatibility_suite/test_v3_message_consumer.py +++ b/tests/compatibility_suite/test_v3_message_consumer.py @@ -185,7 +185,10 @@ def the_message_contains_the_following_metadata( logger.debug("Adding metadata to message: %s", metadatas) for metadata in metadatas: if metadata.get("value", "").startswith("JSON: "): - metadata["value"] = metadata["value"].replace("JSON:", "") + pact_interaction.interaction.with_metadata({ + metadata["key"]: json.loads(metadata["value"].replace("JSON: ", "")) + }) + continue pact_interaction.interaction.with_metadata({metadata["key"]: metadata["value"]}) @@ -265,7 +268,7 @@ def the_message_is_successfully_processed( def handler( body: str | bytes | None, - context: dict[str, str], + context: dict[str, object], ) -> None: messages.append(ReceivedMessage(body, context)) @@ -300,7 +303,7 @@ def the_message_is_not_successfully_processed_with_an_exception( """The message is NOT successfully processed with a "Test failed" exception.""" messages: list[ReceivedMessage] = [] - def handler(body: str | bytes | None, context: dict[str, str]) -> None: + def handler(body: str | bytes | None, context: dict[str, object]) -> None: messages.append(ReceivedMessage(body, context)) raise AssertionError(failure) @@ -615,7 +618,7 @@ def the_received_message_metadata_will_contain( for k, v in message.context.items(): if k == key: if json_matching: - assert json.loads(v) == value + assert v == value else: assert v == value break diff --git a/tests/compatibility_suite/util/interaction_definition.py b/tests/compatibility_suite/util/interaction_definition.py index 4a3a8c30b..a16d8e5eb 100644 --- a/tests/compatibility_suite/util/interaction_definition.py +++ b/tests/compatibility_suite/util/interaction_definition.py @@ -587,11 +587,7 @@ def add_to_pact( # noqa: C901, PLR0912, PLR0915 interaction.with_matching_rules(self.response_matching_rules) if self.metadata: - for key, value in self.metadata.items(): - if isinstance(value, str): - interaction.with_metadata({key: value}) - else: - interaction.with_metadata({key: json.dumps(value)}) + interaction.with_metadata(self.metadata) def matches_request(self, request: SimpleHTTPRequestHandler) -> bool: """ From e98cfdb284494329293c0f2368eb60533a3492ff Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 7 Aug 2025 15:52:43 +1000 Subject: [PATCH 0919/1376] chore: fix bad copy-paste in tests Signed-off-by: JP-Ellis --- tests/interaction/test_sync_message_interaction.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/interaction/test_sync_message_interaction.py b/tests/interaction/test_sync_message_interaction.py index 28a709af4..8cf296e2e 100644 --- a/tests/interaction/test_sync_message_interaction.py +++ b/tests/interaction/test_sync_message_interaction.py @@ -1,5 +1,5 @@ """ -Pact Async Message Interaction unit tests. +Pact Sync Message Interaction unit tests. """ from __future__ import annotations @@ -21,15 +21,15 @@ def pact() -> Pact: def test_str(pact: Pact) -> None: - interaction = pact.upon_receiving("a basic request", "Async") - assert str(interaction) == "AsyncMessageInteraction(a basic request)" + interaction = pact.upon_receiving("a basic request", "Sync") + assert str(interaction) == "SyncMessageInteraction(a basic request)" def test_repr(pact: Pact) -> None: - interaction = pact.upon_receiving("a basic request", "Async") + interaction = pact.upon_receiving("a basic request", "Sync") assert ( re.match( - r"^AsyncMessageInteraction\(InteractionHandle\(\d+\)\)$", + r"^SyncMessageInteraction\(InteractionHandle\(\d+\)\)$", repr(interaction), ) is not None From 6616714822910c19a6b8e190d6d531fdf5165d0e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 16:42:17 +0000 Subject: [PATCH 0920/1376] chore(deps): update actions/cache action to v4.2.4 (#1152) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ed6842071..ede9c8094 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -326,7 +326,7 @@ jobs: fetch-depth: 0 - name: Cache pre-commit - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: | ${{ env.PRE_COMMIT_HOME }} From 8676ef94fe3949ea3be21413dc1223187d4efd04 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 09:25:30 +1000 Subject: [PATCH 0921/1376] fix(deps): update ruff to v0.12.8 (#1153) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 760e2eb2e..d18854839 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.7 + rev: v0.12.8 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 4207f1d4f..87e91f81d 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.9" devel = [ "pact-python-cli[devel-test]", "pact-python-cli[devel-types]", - "ruff==0.12.7", + "ruff==0.12.8", ] devel-test = ["pytest-cov~=6.0", "pytest~=8.0"] devel-types = ["mypy==1.17.1"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index c33166d35..c77db1d4f 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -47,7 +47,7 @@ dependencies = ["cffi~=1.0"] devel = [ "pact-python-ffi[devel-test]", "pact-python-ffi[devel-types]", - "ruff==0.12.7", + "ruff==0.12.8", ] devel-test = ["pytest-cov~=6.0", "pytest~=8.0"] devel-types = ["mypy==1.17.1", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index fb6e52f0f..4f6d70c6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ dependencies = [ "pact-python[devel-example]", "pact-python[devel-test]", "pact-python[devel-types]", - "ruff==0.12.7", + "ruff==0.12.8", ] devel-docs = [ "mkdocs-github-admonitions-plugin~=0.0", From b7bb030067e12a486e9b7ca9ac231a97c0ea31c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 01:59:45 +0000 Subject: [PATCH 0922/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.35.2 (#1155) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ede9c8094..46c5ab55d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -309,7 +309,7 @@ jobs: fetch-depth: 0 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@a9ccf76b53d1ace194871d216f9ff058599a86db # v1.35.1 + uses: crate-ci/typos@f1231bc2bcc92b2b18da70a877cf89afce08dd42 # v1.35.2 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d18854839..ef62ac8ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,7 +78,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.35.1 + rev: v1.35.2 hooks: - id: typos From 1118ea8ee326bc1f4f5d0772416490e14dae46f2 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 8 Aug 2025 13:26:51 +1000 Subject: [PATCH 0923/1376] chore: log exceptions from apply_args The exception will still bubble up; but given this function is typically used within state callbacks which have minimal handling, the logging is important for visibility. Signed-off-by: JP-Ellis --- src/pact/_util.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pact/_util.py b/src/pact/_util.py index 6d242b81c..ac8f7f922 100644 --- a/src/pact/_util.py +++ b/src/pact/_util.py @@ -175,7 +175,7 @@ def find_free_port() -> int: return s.getsockname()[1] -def apply_args(f: Callable[..., _T], args: Mapping[str, object]) -> _T: +def apply_args(f: Callable[..., _T], args: Mapping[str, object]) -> _T: # noqa: C901 """ Apply arguments to a function. @@ -272,4 +272,8 @@ def apply_args(f: Callable[..., _T], args: Mapping[str, object]) -> _T: list(args.keys()), ) - return f() + try: + return f() + except Exception: + logger.exception("Error occurred while calling function %s", f_name) + raise From c3dc09ea9092f336127ca62685ce112ec47b47a4 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 8 Aug 2025 13:26:17 +1000 Subject: [PATCH 0924/1376] chore: improve logging from apply_args Specifically, just because an argument was not applied does not mean anything went wrong. Signed-off-by: JP-Ellis --- src/pact/_util.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pact/_util.py b/src/pact/_util.py index ac8f7f922..e34020f61 100644 --- a/src/pact/_util.py +++ b/src/pact/_util.py @@ -266,10 +266,15 @@ def apply_args(f: Callable[..., _T], args: Mapping[str, object]) -> _T: # noqa: args.clear() else: logger.debug( - "Function %s does not accept any additional arguments. " - "remaining arguments: %s", + "Function %s was called with remaining arguments: %s. " + "This is not necessarily a bug; whether extra arguments are " + "acceptable depends on the function's signature and intended usage.", f_name, list(args.keys()), + extra={ + "function_name": f_name, + "remaining_args": list(args.keys()), + }, ) try: From 01f592bd041f44c78372d6920ea3ff50a33de702 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 8 Aug 2025 13:29:09 +1000 Subject: [PATCH 0925/1376] fix: use correct datetime default format There was a mismatch between the documented default and the actual default in practice. BREAKING CHANGE: If you relied on the previous default (undocumented) behaviour, prefer specifying the format explicitly now: `match.datetime(regex="%Y-%m-%dT%H:%M:%S.%f%z")`. Signed-off-by: JP-Ellis --- src/pact/match/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pact/match/__init__.py b/src/pact/match/__init__.py index e643fdbbd..75ec6f4e1 100644 --- a/src/pact/match/__init__.py +++ b/src/pact/match/__init__.py @@ -730,7 +730,7 @@ def datetime( value=value, format=format, ) - format = format or "%Y-%m-%dT%H:%M:%S.%f%z" + format = format or "%Y-%m-%dT%H:%M:%S%z" if isinstance(value, dt.datetime): value = value.strftime(format) format = strftime_to_simple_date_format(format) From aeafe108b9ce0ea97aefa0f4b86b5a16ab4c1deb Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 8 Aug 2025 13:38:49 +1000 Subject: [PATCH 0926/1376] refactor: functional state handler And also add debug logging for when the state handler is being called. Signed-off-by: JP-Ellis --- src/pact/verifier.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pact/verifier.py b/src/pact/verifier.py index 86b24a501..14d1027e7 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -593,7 +593,7 @@ def state_handler( if body is not None: msg = "The `body` parameter must be `None` when providing a function" raise ValueError(msg) - return self._set_function_state_handler(handler, teardown=teardown) + return self._state_handler_function(handler, teardown=teardown) msg = "Invalid handler type" raise TypeError(msg) @@ -683,7 +683,7 @@ def _state_handler_dict( logger.debug( "Setting dictionary state handler for verifier", extra={ - "handler": handler, + "states": list(handler.keys()), "teardown": teardown, }, ) @@ -693,6 +693,15 @@ def _handler( action: Literal["setup", "teardown"], parameters: dict[str, Any] | None, ) -> None: + logger.debug( + "Calling state handler function for state %r", + state, + extra={ + "action": action, + "parameters": parameters, + }, + ) + apply_args( handler[state], StateHandlerArgs(state=state, action=action, parameters=parameters), @@ -708,7 +717,7 @@ def _handler( return self - def _set_function_state_handler( + def _state_handler_function( self, handler: Callable[..., None], *, From b92f461833fb435c1b9f685b4d112f5f03bd0914 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 06:48:24 +0000 Subject: [PATCH 0927/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.1.4 (#1160) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef62ac8ff..69756426a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.1.3 + rev: v2.1.4 hooks: - id: biome-check From 37c2abc12cd4705a140ddb0b332c850f2bf5b9ef Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 8 Aug 2025 14:42:52 +1000 Subject: [PATCH 0928/1376] fix: handle empty state callback The empty state callback provides a mechanism to hook in to the test setup and teardown in a generic way. This was not well handled before. Ref: pact-foundation/pact-reference#468 Signed-off-by: JP-Ellis --- src/pact/verifier.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/pact/verifier.py b/src/pact/verifier.py index 14d1027e7..9a652f928 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -547,6 +547,11 @@ def state_handler( has additional arguments, these must either have default values, or be filled by using the [`partial`][functools.partial] function. + Pact also uses a special state denoted with the empty string `""`. This + is used as a generic test setup/teardown handler. This key is optional + in dictionaries, but other implementation should ensure they can handle + (or safely ignore) this state name. + Args: handler: The handler for the state changes. This can be one of the @@ -665,6 +670,10 @@ def _state_handler_dict( the parameters. If `teardown` is `False`, the functions must take one argument: the parameters. + Note that the empty string `""` is used as a special key for a + generic test setup/teardown handler. If this key is absent, + these callbacks will be safely ignored. + teardown: Whether to teardown the provider state after an interaction is validated. @@ -693,6 +702,9 @@ def _handler( action: Literal["setup", "teardown"], parameters: dict[str, Any] | None, ) -> None: + if state == "" and state not in handler: + return + logger.debug( "Calling state handler function for state %r", state, From 61363e71ac05bebda51030f3a3810a0e1ced6902 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 Aug 2025 10:11:43 +1000 Subject: [PATCH 0929/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.35.3 (#1163) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46c5ab55d..91769ce40 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -309,7 +309,7 @@ jobs: fetch-depth: 0 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@f1231bc2bcc92b2b18da70a877cf89afce08dd42 # v1.35.2 + uses: crate-ci/typos@52bd719c2c91f9d676e2aa359fc8e0db8925e6d8 # v1.35.3 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 69756426a..9bfb339a5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,7 +78,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.35.2 + rev: v1.35.3 hooks: - id: typos From c0e1a36d2aa4ffd9598bf2405563f8ec62e98a82 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 10 Aug 2025 00:33:12 +0000 Subject: [PATCH 0930/1376] chore(deps): update pre-commit hook pre-commit/pre-commit-hooks to v6 (#1164) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9bfb339a5..df9fa6c4c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ default_install_hook_types: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-added-large-files - id: check-case-conflict From 76162ef24c569974c34983034813247dab1d3a69 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:44:39 +1000 Subject: [PATCH 0931/1376] chore(deps): update taiki-e/install-action action to v2.58.6 (#1165) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 0a3925907..92664dbd0 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@d31232495ad76f47aad66e3501e47780b49f0f3e # v2.57.5 + uses: taiki-e/install-action@5f6f3e0538d249cb0b47f8d5b636c120babeb082 # v2.58.6 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 232331c9e..391baf0f7 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@d31232495ad76f47aad66e3501e47780b49f0f3e # v2.57.5 + uses: taiki-e/install-action@5f6f3e0538d249cb0b47f8d5b636c120babeb082 # v2.58.6 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b168a47c2..0f60458ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@d31232495ad76f47aad66e3501e47780b49f0f3e # v2.57.5 + uses: taiki-e/install-action@5f6f3e0538d249cb0b47f8d5b636c120babeb082 # v2.58.6 with: tool: git-cliff,typos From 049865fb0816b542f9ee57eab1b7bf3ec91e8f22 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 04:09:22 +0000 Subject: [PATCH 0932/1376] chore(deps): update taiki-e/install-action action to v2.58.7 (#1167) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 92664dbd0..b957e3d57 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@5f6f3e0538d249cb0b47f8d5b636c120babeb082 # v2.58.6 + uses: taiki-e/install-action@1fd1160ee1f4387ce162a5a4f9c26ea274278b7f # v2.58.7 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 391baf0f7..451b816c4 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@5f6f3e0538d249cb0b47f8d5b636c120babeb082 # v2.58.6 + uses: taiki-e/install-action@1fd1160ee1f4387ce162a5a4f9c26ea274278b7f # v2.58.7 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f60458ad..0f347e555 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@5f6f3e0538d249cb0b47f8d5b636c120babeb082 # v2.58.6 + uses: taiki-e/install-action@1fd1160ee1f4387ce162a5a4f9c26ea274278b7f # v2.58.7 with: tool: git-cliff,typos From b80abbd21f0c8c41dcada3ba8622f6b655bd43ac Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 11 Aug 2025 13:09:40 +1000 Subject: [PATCH 0933/1376] chore(examples): start examples overhaul Adapt the old v2 HTTP example to one using `aiohttp` and `flask`. Signed-off-by: JP-Ellis --- examples/.gitignore | 2 + examples/.ruff.toml | 12 +- examples/README.md | 167 ++++++--- examples/__init__.py | 1 + examples/conftest.py | 70 ---- examples/http/README.md | 8 + examples/http/__init__.py | 1 + examples/http/aiohttp_and_flask/README.md | 82 +++++ examples/http/aiohttp_and_flask/__init__.py | 1 + examples/http/aiohttp_and_flask/conftest.py | 35 ++ examples/http/aiohttp_and_flask/consumer.py | 272 +++++++++++++++ examples/http/aiohttp_and_flask/provider.py | 316 ++++++++++++++++++ .../http/aiohttp_and_flask/pyproject.toml | 33 ++ .../http/aiohttp_and_flask/test_consumer.py | 161 +++++++++ .../http/aiohttp_and_flask/test_provider.py | 228 +++++++++++++ examples/pacts/.gitignore | 2 - examples/plugins/__init__.py | 1 + examples/v2/src/fastapi.py | 153 --------- examples/v2/src/flask.py | 144 -------- examples/v2/tests/test_00_consumer.py | 207 ------------ examples/v2/tests/test_01_provider_fastapi.py | 223 ------------ examples/v2/tests/test_01_provider_flask.py | 217 ------------ mkdocs.yml | 2 + 23 files changed, 1265 insertions(+), 1073 deletions(-) create mode 100644 examples/.gitignore create mode 100644 examples/http/README.md create mode 100644 examples/http/__init__.py create mode 100644 examples/http/aiohttp_and_flask/README.md create mode 100644 examples/http/aiohttp_and_flask/__init__.py create mode 100644 examples/http/aiohttp_and_flask/conftest.py create mode 100644 examples/http/aiohttp_and_flask/consumer.py create mode 100644 examples/http/aiohttp_and_flask/provider.py create mode 100644 examples/http/aiohttp_and_flask/pyproject.toml create mode 100644 examples/http/aiohttp_and_flask/test_consumer.py create mode 100644 examples/http/aiohttp_and_flask/test_provider.py delete mode 100644 examples/pacts/.gitignore delete mode 100644 examples/v2/src/fastapi.py delete mode 100644 examples/v2/src/flask.py delete mode 100644 examples/v2/tests/test_00_consumer.py delete mode 100644 examples/v2/tests/test_01_provider_fastapi.py delete mode 100644 examples/v2/tests/test_01_provider_flask.py diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 000000000..8e817a5b7 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,2 @@ +**/pacts/* +**/uv.lock diff --git a/examples/.ruff.toml b/examples/.ruff.toml index cc0d509c7..6a9b80925 100644 --- a/examples/.ruff.toml +++ b/examples/.ruff.toml @@ -1,17 +1,13 @@ #:schema https://json.schemastore.org/ruff.json extend = "../pyproject.toml" -[lint] -ignore = [ +[lint.per-file-ignores] +"test_*.py" = [ "D103", # Require docstring in public function "D104", # Require docstring in public package + "INP001", # Forbid implicit namespaces "PLR2004", # Forbid Magic Numbers + "PLR2004", # Forbid magic values "S101", # Forbid assert statements "TID252", # Require absolute imports ] - - [lint.per-file-ignores] - "tests/**.py" = [ - "INP001", # Forbid implicit namespaces - "PLR2004", # Forbid magic values - ] diff --git a/examples/README.md b/examples/README.md index b6727c20c..6f4223d40 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,20 +1,44 @@ # Examples -This directory contains an end-to-end example of using Pact in Python. While this document and the documentation within the examples themselves are intended to be mostly self-contained, it is highly recommended that you read the [Pact Documentation](https://docs.pact.io/) as well. +This directory contains examples demonstrating how to use Pact in Python for various testing scenarios. While this document and the documentation within the examples themselves are intended to be mostly self-contained, it is highly recommended that you read the [Pact Documentation](https://docs.pact.io/) as well. -Assuming you have [hatch](https://hatch.pypa.io/latest/) installed, the example suite can be executed with: - -```sh -hatch run example -``` +Each example is self-contained with its own dependency management using a `pyproject.toml` file. This allows you to run examples independently without affecting your global Python environment or other examples. The code within the examples is intended to be well-documented and you are encouraged to look through the code as well (or submit a PR if anything is unclear!). -## Overview +## Available Examples + +### HTTP Examples + +#### aiohttp and Flask + +- **Location**: `examples/http/aiohttp_and_flask/` +- **Consumer**: aiohttp-based HTTP client +- **Provider**: Flask-based HTTP server + +#### requests and FastAPI + +- **Location**: `examples/http/requests_and_fastapi/` +- **Consumer**: requests-based HTTP client +- **Provider**: FastAPI-based HTTP server + +### Message Examples + +- **Location**: `examples/message/` +- **Status**: 🚧 To be updated + +### Plugin Examples -Pact is a contract testing tool. Contract testing is a way to ensure that services (such as an API provider and a client) can communicate with each other. This example focuses on HTTP interactions, but Pact can be used to test more general interactions as well such as through message queues. +- **Location**: `examples/plugins/` +- **Status**: 🚧 To be updated -An interaction between a HTTP client (the _consumer_) and a server (the _provider_) would typically look like this: +## Running Examples + +Each example can be run independently. Navigate to the specific example directory and use your preferred dependency manager. + +## Overview + +Pact is a contract testing tool. Contract testing is a way to ensure that services (such as an API provider and a client) can communicate with each other. An interaction between a _consumer_ (i.e., a HTTP client, mobile app, website, microservice, etc.) and a _provider_ (i.e., a web server, microservice, etc.) would typically look like this:
@@ -32,9 +56,7 @@ sequenceDiagram
-To test this interaction naively would require both the consumer and provider to be running at the same time. While this is straightforward in the above example, this quickly becomes impractical as the number of interactions grows between many microservices. Pact solves this by allowing the consumer and provider to be tested independently. - -Pact achieves this by mocking the other side of the interaction: +Pact allows for each side of the interaction to be tested independently. Pact achieves this by mocking the other side of the interaction:
@@ -65,53 +87,100 @@ sequenceDiagram
-In the first stage, the consumer defines a number of interactions in the form below. Pact sets up a mock server that will respond to the requests as defined by the consumer. All these interactions, containing both the request and expected response, are sent to the Pact Broker. +Pact is **consumer driven**. This means that the consumer is responsible for defining the interactions it expects from the provider through the pattern of -> Given {provider state}
-> Upon receiving {description}
-> With {request}
-> Will respond with {response}
+ +> Given {provider state}
+> Upon receiving {description}
+> With {request}
+> Will respond with {response}
+ -In the second stage, the provider retrieves the interactions from the Pact Broker. It then sets up a mock client that will make the requests as defined by the consumer. Pact then verifies that the responses from the provider match the expected responses defined by the consumer. +When the consumer tests are executed, a Pact mock server is set up that will respond to the requests as defined by the consumer. When the consumer tests are merged into the main branch, the Pact contract is sent to the Pact Broker. -In this way, Pact is consumer-driven and can ensure that the provider is compatible with the consumer. While this example showcases both sides in Python, this is absolutely not required. The provider could be written in any language, and satisfy contracts from a number of consumers all written in different languages. +When the provider tests are executed, all contracts are retrieved from the Pact Broker. The provider then sets up a mock client that will make the requests as defined by the consumer. Pact then verifies that the responses from the provider match the expected responses defined by the consumer. -### Consumer +In this way, Pact is consumer-driven and can ensure that the provider is compatible with the consumer. While the examples showcase both sides in Python, this is absolutely not required. The provider could be written in any language, and satisfy contracts from a number of consumers all written in different languages. -The consumer in this example is a simple Python script that makes a HTTP GET request to a server. It is defined in [`src/consumer.py`][examples.v2.src.consumer]. The tests for the consumer are defined in [`tests/test_00_consumer.py`][examples.tests.test_00_consumer]. Each interaction is defined using the format mentioned above. Programmatically, this looks like: +## Consumer -```py -expected: dict[str, Any] = { - "id": Format().integer, - "name": "Verna Hampton", - "created_on": Format().iso_8601_datetime(), -} -( - pact.given("user 123 exists") - .upon_receiving("a request for user 123") - .with_request("get", "/users/123") - .will_respond_with(200, body=Like(expected)) -) -# Code that makes the request to the server -``` +Consumer tests define the contract by specifying the interactions the consumer expects from the provider. These tests focus on the consumer's perspective and needs. -### Provider +### Principles -This example showcases two different providers; one written in Flask and one written in FastAPI. Both are simple Python web servers that respond to a HTTP GET request. The Flask provider is defined in [`src/flask.py`][examples.v2.src.flask] and the FastAPI provider is defined in [`src/fastapi.py`][examples.v2.src.fastapi]. The tests for the providers are defined in [`tests/test_01_provider_flask.py`][examples.tests.test_01_provider_flask] and [`tests/test_01_provider_fastapi.py`][examples.tests.test_01_provider_fastapi]. +- **Core interactions are defined**: Only test the interactions your consumer actually uses +- **Minimal requests and responses**: Define only the required headers, query parameters, and body fields that your consumer needs +- **Consumer-driven**: The consumer decides what it needs from the provider, not what the provider offers +- **Independent testing**: Consumer tests run against a Pact mock provider, not the real provider -Unlike the consumer side, the provider side is responsible for responding to the interactions defined by the consumers. In this regard, the provider testing is rather simple: +### Best Practices -```py -code, _ = verifier.verify_with_broker( - broker_url=str(broker), - published_verification_results=True, - provider_states_setup_url=str(PROVIDER_URL / "_pact" / "provider_states"), -) -assert code == 0 -``` +- If your consumer doesn't use a field from the provider's response, it should be safe for the provider to remove that field +- Use Pact matchers to make contracts flexible (e.g., `match.integer` instead of hardcoded values) +- Test error scenarios your consumer needs to handle (4xx, 5xx responses) +- Keep contracts focused on business logic, not implementation details + +### Contract Publishing + +When consumer tests pass, the generated Pact contracts should be published to a Pact Broker. This makes them available for provider verification and enables the consumer-driven workflow. + +## Provider + +Provider tests verify that the actual provider implementation satisfies all contracts defined by its consumers. These tests ensure the provider can fulfill its obligations in the consumer-provider relationship. + +### Core Principles + +- **Verify all consumer contracts**: The provider must satisfy every interaction defined by all its consumers +- **Provider state management**: Set up the correct application state before each interaction is verified +- **Real provider testing**: Verification runs against the actual provider implementation, not mocks +- **Fail fast**: Any contract violation should cause provider tests to fail immediately + +### Provider States + +Provider states are a key concept for ensuring your provider is in the correct state before an interaction: + +- **Setup**: Use provider state callbacks to configure your application (e.g., create test data) +- **Isolation**: Each interaction should have a clean, predictable state +- **Mocking**: Mock external dependencies (databases, APIs) rather than requiring real infrastructure + +### Contract Retrieval + +Provider tests retrieve contracts from the Pact Broker and verify them against the running provider: + +- Use selectors to choose which contracts to verify (e.g., latest, main branch, specific versions) +- Configure which consumer versions to verify based on your deployment strategy +- Publish verification results back to the broker + +## Broker + +The Pact Broker acts as the central contract repository and coordination point between consumers and providers. It stores contracts, verification results, and provides tools for managing the contract testing workflow. + +### How It Works + +- **Contract storage**: Consumers publish their contracts to the broker after successful test runs +- **Contract retrieval**: Providers fetch relevant contracts from the broker for verification +- **Verification results**: Providers publish verification results back to the broker +- **Visibility**: Teams can see which contracts exist, their verification status, and compatibility matrix + +### Publishing Contracts + +Consumers should publish contracts when: + +- Consumer tests pass successfully +- Changes are merged to main branch +- Deploying to production environments + +### Retrieving Contracts + +Providers can use selectors to determine which contracts to verify: -The complication comes from the fact that the provider needs to know what state to be in before responding to the request. In order to achieve this, a testing endpoint is defined that sets the state of the provider as defined in the `provider_states_setup_url` above. For example, the consumer requests has _Given user 123 exists_ as the provider state, and the provider will need to ensure that this state is satisfied. This would typically entail setting up a database with the correct data, but it is advisable to achieve the equivalent state by mocking the appropriate calls. This has been showcased in both provider examples. +- **Latest**: Most recent contract from each consumer +- **Branch-based**: Contracts from specific git branches (e.g., `main`, `develop`) +- **Environment-based**: Contracts from consumers deployed to specific environments +- **Tag-based**: Contracts tagged with specific labels -### Broker +### Versioning Strategy -The broker acts as the intermediary between these test suites. It stores the interactions defined by the consumer and makes them available to the provider. Once the provider has verified that it satisfies all interactions, the broker also stores the verification results. The example here runs the open source broker within a Docker container. An alternative is to use the hosted [Pactflow service](https://pactflow.io). +- Use semantic versioning or commit SHA for contract versions +- Tag contracts when deploying to different environments +- Use "can-i-deploy" checks before releasing to ensure compatibility diff --git a/examples/__init__.py b/examples/__init__.py index e69de29bb..6e031999e 100644 --- a/examples/__init__.py +++ b/examples/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/examples/conftest.py b/examples/conftest.py index 2c6b2f2c5..3ced5de4d 100644 --- a/examples/conftest.py +++ b/examples/conftest.py @@ -12,91 +12,21 @@ from __future__ import annotations -import socket -import sys -import warnings from pathlib import Path -from typing import TYPE_CHECKING, Any import pytest -from testcontainers.compose import DockerCompose # type: ignore[import-untyped] -from yarl import URL import pact_ffi -if TYPE_CHECKING: - from collections.abc import Generator, Sequence - - import execnet - EXAMPLE_DIR = Path(__file__).parent.resolve() -@pytest.fixture(scope="session") -def broker(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: - """ - Fixture to run the Pact broker. - - This inspects whether the `--broker-url` option has been given. If it has, - it is assumed that the broker is already running and simply returns the - given URL. - - Otherwise, the Pact broker is started in a container. The URL of the - containerised broker is then returned. - """ - broker_url: str | None = request.config.getoption("--broker-url") - - # If we have been given a broker URL, there's nothing more to do here and we - # can return early. - if broker_url: - yield URL(broker_url) - return - - # Check whether port 9292 is already in use. If it is, we assume that the - # broker is already running and return early. - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - if s.connect_ex(("localhost", 9292)) == 0: - yield URL("http://pactbroker:pactbroker@localhost:9292") - return - - with DockerCompose( - EXAMPLE_DIR, - compose_file_name=["container-compose.yml"], - pull=True, - wait=False, - ) as _: - yield URL("http://pactbroker:pactbroker@localhost:9292") - - @pytest.fixture(scope="session") def pacts_path() -> Path: """Fixture for the Pact directory.""" return EXAMPLE_DIR / "pacts" -def pytest_xdist_setupnodes( - config: pytest.Config, # noqa: ARG001 - specs: Sequence[execnet.XSpec], -) -> None: - """ - Hook to check if the examples are run with multiple workers. - - The examples are designed to run in a specific order, with the consumer - tests running _before_ the provider tests as the provider tests require that - the consumer-generated Pacts are published. - - If multiple xdist workers are detected, a warning is printed to the console. - """ - if len(specs) > 1: - sys.stderr.write("\n") - warnings.warn( - "Running the examples with multiple workers may cause issues. " - "Consider running the examples with a single worker by setting " - "`--numprocesses=1` or using `hatch run example`.", - stacklevel=1, - ) - - @pytest.fixture(scope="session", autouse=True) def _setup_pact_logging() -> None: """ diff --git a/examples/http/README.md b/examples/http/README.md new file mode 100644 index 000000000..913b50af0 --- /dev/null +++ b/examples/http/README.md @@ -0,0 +1,8 @@ +# HTTP Examples + +This directory contains examples of HTTP-based contract testing with Pact. + +## Examples + +- [`aiohttp_and_flask/`](aiohttp_and_flask/) - Async aiohttp consumer with Flask provider +- [`requests_and_fastapi/`](requests_and_fastapi/) - requests consumer with FastAPI provider (🚧 planned) diff --git a/examples/http/__init__.py b/examples/http/__init__.py new file mode 100644 index 000000000..b1a036caf --- /dev/null +++ b/examples/http/__init__.py @@ -0,0 +1 @@ +# noqa: A005, D104 diff --git a/examples/http/aiohttp_and_flask/README.md b/examples/http/aiohttp_and_flask/README.md new file mode 100644 index 000000000..89a137336 --- /dev/null +++ b/examples/http/aiohttp_and_flask/README.md @@ -0,0 +1,82 @@ +# aiohttp and Flask Example + +This example demonstrates contract testing between an asynchronous [`aiohttp`](https://docs.aiohttp.org/en/stable/)-based client (consumer) and a [Flask](https://flask.palletsprojects.com/en/stable/) web server (provider). It showcases modern Python patterns including async/await, type hints, and standalone dependency management. + +## Overview + +- [**Consumer**][examples.http.aiohttp_and_flask.consumer]: An async HTTP client using aiohttp +- [**Provider**][examples.http.aiohttp_and_flask.provider]: A Flask web server +- [**Consumer Tests**][examples.http.aiohttp_and_flask.test_consumer]: Contract definition and consumer testing +- [**Provider Tests**][examples.http.aiohttp_and_flask.test_provider]: Provider verification against contracts + +Use the above links to view additional documentation within. + +## What This Example Demonstrates + +### Consumer Side + +- Async HTTP client implementation with aiohttp +- Consumer contract testing with Pact mock servers +- Handling different HTTP response scenarios (success, not found, etc.) +- Modern Python async patterns + +### Provider Side + +- Flask web server with RESTful endpoints +- Provider verification against consumer contracts +- Provider state setup for different test scenarios +- Mock data management for testing + +### Testing Patterns + +- Independent consumer and provider testing +- Contract-driven development workflow +- Error handling and edge case testing +- Type safety with Python type hints + +## Prerequisites + +- Python 3.9 or higher +- A dependency manager (uv recommended, pip also works) + +## Running the Example + +### Using uv (Recommended) + +We recommend using [uv](https://docs.astral.sh/uv/) to manage the virtual env and manage dependencies. The following command will automatically set up the virtual environment, install dependencies, and then execute the command within the virtual environment: + +```console +uv run --group test pytest +``` + +### Using pip + +If using the [`venv`][venv] module, the steps require are: + +1. Create the virtual environment and then activate it: + + ```console + python -m venv .venv + source .venv/bin/activate # On macOS/Linux + .venv\Scripts\activate # On Windows + ``` + +2. Install the required dependencies in the virtual environment: + + ```console + pip install -U pip # Pip 25.1 is required + pip install --group test -e . + ``` + +3. Run pytest: + + ```console + pytest + ``` + +## Related Documentation + +- [Pact Documentation](https://docs.pact.io/) +- [aiohttp Documentation](https://docs.aiohttp.org/) +- [Flask Documentation](https://flask.palletsprojects.com/) +- [pytest Documentation](https://docs.pytest.org/) diff --git a/examples/http/aiohttp_and_flask/__init__.py b/examples/http/aiohttp_and_flask/__init__.py new file mode 100644 index 000000000..6e031999e --- /dev/null +++ b/examples/http/aiohttp_and_flask/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/examples/http/aiohttp_and_flask/conftest.py b/examples/http/aiohttp_and_flask/conftest.py new file mode 100644 index 000000000..3ced5de4d --- /dev/null +++ b/examples/http/aiohttp_and_flask/conftest.py @@ -0,0 +1,35 @@ +""" +Shared PyTest configuration. + +In order to run the examples, we need to run the Pact broker. In order to avoid +having to run the Pact broker manually, or repeating the same code in each +example, we define a PyTest fixture to run the Pact broker. + +We also define a `pact_dir` fixture to define the directory where the generated +Pact files will be stored. You are encouraged to have a look at these files +after the examples have been run. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +import pact_ffi + +EXAMPLE_DIR = Path(__file__).parent.resolve() + + +@pytest.fixture(scope="session") +def pacts_path() -> Path: + """Fixture for the Pact directory.""" + return EXAMPLE_DIR / "pacts" + + +@pytest.fixture(scope="session", autouse=True) +def _setup_pact_logging() -> None: + """ + Set up logging for the pact package. + """ + pact_ffi.log_to_stderr("INFO") diff --git a/examples/http/aiohttp_and_flask/consumer.py b/examples/http/aiohttp_and_flask/consumer.py new file mode 100644 index 000000000..4f8e6004c --- /dev/null +++ b/examples/http/aiohttp_and_flask/consumer.py @@ -0,0 +1,272 @@ +""" +Simple Consumer Implementation. + +This modules defines a simple +[consumer](https://docs.pact.io/getting_started/terminology#service-consumer) +which will be tested with Pact in the [consumer +test][examples.http.aiohttp_and_flask.test_consumer]. As Pact is a +consumer-driven framework, the consumer defines the interactions which the +provider must then satisfy. + +The consumer is the application which makes requests to another service (the +provider) and receives a response to process. In this example, we have a simple +`User` class and the consumer fetches a user's information from a HTTP endpoint. + +This also showcases how Pact tests differ from merely testing adherence to an +OpenAPI specification. The Pact tests are more concerned with the practical use +of the API, rather than the formally defined specification. So you will see +below that as far as this consumer is concerned, the only information needed +from the provider is the user's ID, name, and creation date. This is despite the +provider having additional fields in the response. + +Note that the code in this module is agnostic of Pact (i.e., this would be your +production code). The `pact-python` dependency only appears in the tests. This +is because the consumer is not concerned with Pact, only the tests are. +""" + +from __future__ import annotations + +import logging +import sys +from dataclasses import dataclass +from datetime import datetime +from typing import TYPE_CHECKING, Any + +import aiohttp + +if TYPE_CHECKING: + from types import TracebackType + + from typing_extensions import Self + +logger = logging.getLogger(__name__) + + +@dataclass() +class User: + """ + Represents a user as seen by the consumer. + + This class is intentionally minimal, including only the fields the consumer + actually uses. It may differ from the [provider's user + model][examples.http.aiohttp_and_flask.provider.User], which could have + additional fields. This demonstrates the consumer-driven nature of contract + testing: the consumer defines what it needs, not what the provider exposes. + """ + + id: int + name: str + created_on: datetime + + def __post_init__(self) -> None: + """ + Validate the user data. + + Ensures that the user has a non-empty name and a positive integer ID. + + Raises: + ValueError: + If the name is empty or the ID is not positive. + """ + if not self.name: + msg = "User must have a name" + raise ValueError(msg) + + if self.id < 0: + msg = "User ID must be a positive integer" + raise ValueError(msg) + + def __repr__(self) -> str: + """ + Return a string representation of the user. + + Returns: + The user's ID and name as a string. + """ + return f"User(id={self.id!r}, name={self.name!r})" + + +class UserClient: + """ + HTTP client for interacting with a user provider service. + + This class is a simple consumer that fetches user data from a provider over + HTTP. It demonstrates how to structure consumer code for use in contract + testing, keeping it independent of Pact or any contract testing framework. + """ + + def __init__(self, hostname: str, base_path: str | None = None) -> None: + """ + Initialise the user client. + + Args: + hostname: + The base URL of the provider (must include scheme, e.g., 'http://'). + + base_path: + The base path for the provider's API endpoints. Defaults to '/'. + + Raises: + ValueError: + If the hostname does not start with 'http://' or 'https://'. + """ + if not hostname.startswith(("http://", "https://")): + msg = "Invalid base URI" + raise ValueError(msg) + self._hostname = hostname + self._base_path = base_path or "/" + if not self._base_path.endswith("/"): + self._base_path += "/" + + self._client = aiohttp.ClientSession( + base_url=self._hostname, + timeout=aiohttp.ClientTimeout(total=5), + ) + logger.debug( + "Initialised UserClient with base URL: %s%s", + self.base_url, + self._base_path, + ) + + @property + def hostname(self) -> str: + """ + The hostname as a string. + + This includes the scheme. + """ + return self._hostname + + @property + def base_path(self) -> str: + """ + The base path as a string. + """ + return self._base_path + + @property + def base_url(self) -> str: + """ + The base URL as a string. + """ + return f"{self._hostname}{self._base_path}" + + async def __aenter__(self) -> Self: + """ + The client instance itself. + """ + await self._client.__aenter__() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """ + Exit the asynchronous context for the client. + + Args: + exc_type: + The exception type, if any. + + exc_val: + The exception value, if any. + + exc_tb: + The traceback, if any. + """ + await self._client.__aexit__(exc_type, exc_val, exc_tb) + + async def get_user(self, user_id: int) -> User: + """ + Fetch a user by ID from the provider. + + This method demonstrates how a consumer fetches only the data it needs from + a provider, regardless of what else the provider may return. + + Args: + user_id: + The ID of the user to fetch. + + Returns: + A `User` instance representing the fetched user. + + Raises: + aiohttp.ClientError: + If the server returns a non-2xx response or the request fails. + """ + logger.debug("Fetching user %s", user_id) + async with self._client.get(f"{self.base_path}users/{user_id}") as response: + response.raise_for_status() + data: dict[str, Any] = await response.json() + + # Python < 3.11 don't support ISO 8601 offsets without a colon + if sys.version_info < (3, 11) and data["created_on"][-4:].isdigit(): + data["created_on"] = ( + data["created_on"][:-2] + ":" + data["created_on"][-2:] + ) + return User( + id=data["id"], + name=data["name"], + created_on=datetime.fromisoformat(data["created_on"]), + ) + + async def create_user( + self, + *, + name: str, + ) -> User: + """ + Create a new user on the provider. + + Args: + name: + The name of the user to create. + + Returns: + A `User` instance representing the newly created user. + + Raises: + aiohttp.ClientError: + If the server returns a non-2xx response or the request fails. + """ + logger.debug("Creating user %s", name) + async with ( + self._client.post( + f"{self.base_path}users", json={"name": name} + ) as response, + ): + response.raise_for_status() + data = await response.json() + # Python < 3.11 don't support ISO 8601 offsets without a colon + if sys.version_info < (3, 11) and data["created_on"][-4:].isdigit(): + data["created_on"] = ( + data["created_on"][:-2] + ":" + data["created_on"][-2:] + ) + logger.debug("Created user %s", data["id"]) + return User( + id=data["id"], + name=data["name"], + created_on=datetime.fromisoformat(data["created_on"]), + ) + + async def delete_user(self, uid: int | User) -> None: + """ + Delete a user by ID from the provider. + + Args: + uid: + The user ID (int) or a `User` instance to delete. + + Raises: + aiohttp.ClientError: + If the server returns a non-2xx response or the request fails. + """ + if isinstance(uid, User): + uid = uid.id + logger.debug("Deleting user %s", uid) + + async with self._client.delete(f"{self.base_path}users/{uid}") as response: + response.raise_for_status() diff --git a/examples/http/aiohttp_and_flask/provider.py b/examples/http/aiohttp_and_flask/provider.py new file mode 100644 index 000000000..65a9f874d --- /dev/null +++ b/examples/http/aiohttp_and_flask/provider.py @@ -0,0 +1,316 @@ +""" +Flask provider example. + +This modules defines a simple +[provider](https://docs.pact.io/getting_started/terminology#service-provider) +which will be tested with Pact in the [provider +test][examples.http.aiohttp_and_flask.test_provider]. As Pact is a +consumer-driven framework, the consumer defines the contract which the provider +must then satisfy. + +The provider is the application which receives requests from another service +(the consumer) and returns a response. In this example, we have a simple +endpoint which returns a user's information from a (fake) database. + +This also showcases how Pact tests differ from merely testing adherence to an +OpenAPI specification. The Pact tests are more concerned with the practical use +of the API, rather than the formally defined specification. The User class +defined here has additional fields which are not used by the consumer. Should +the provider later decide to add or remove fields, Pact's consumer-driven +testing will provide feedback on whether the consumer is compatible with the +provider's changes. + +Note that the code in this module is agnostic of Pact (i.e., this would be your +production code). The `pact-python` dependency only appears in the tests. This +is because the consumer is not concerned with Pact, only the tests are. +""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any, ClassVar, Literal + +from flask import Flask, Response, abort, jsonify, request + +if TYPE_CHECKING: + import werkzeug.exceptions + +logger = logging.getLogger(__name__) + + +@dataclass() +class User: + """ + Represents a user in the provider system. + + This class is used to model user data as it might exist in a real + application. In a provider context, the data model may contain more fields + than are required by any single consumer. This example demonstrates how a + provider can serve multiple consumers with different data needs, and how + consumer-driven contract testing (such as with Pact) helps ensure + compatibility as the provider evolves. + """ + + id: int + name: str + created_on: datetime + email: str | None + ip_address: str | None + hobbies: list[str] + admin: bool + + def __post_init__(self) -> None: + """ + Validate the User data. + + Ensures that the user has a non-empty name and a positive integer ID. + + Raises: + ValueError: + If the name is empty or the ID is not positive. + """ + if not self.name: + msg = "User must have a name" + raise ValueError(msg) + + if self.id < 0: + msg = "User ID must be a positive integer" + raise ValueError(msg) + + def __repr__(self) -> str: + """ + Return a string representation of the user. + + Returns: + The user's name and ID as a string. + """ + return f"User(id={self.id!r}, name={self.name!r})" + + def to_dict(self) -> dict[str, Any]: + """ + Convert the user's data to a dictionary. + + Returns: + A dictionary containing the user's data, suitable for JSON + serialization. + """ + return { + "id": self.id, + "name": self.name, + "created_on": self.created_on.strftime("%Y-%m-%dT%H:%M:%S%z"), + "email": self.email, + "ip_address": self.ip_address, + "hobbies": self.hobbies, + "admin": self.admin, + } + + +class UserDb: + """ + A simple in-memory user database abstraction for demonstration purposes. + + This class simulates a user database using a class-level dictionary. In a + real application, this would interface with a persistent database or + external user service. For testing, calls to this class can be mocked to + avoid the need for a real database. See the [test + suite][examples.http.aiohttp_and_flask.test_provider] for an example. + """ + + _db: ClassVar[dict[int, User]] = {} + + @classmethod + def create(cls, user: User) -> None: + """ + Add a new user to the database. + + Args: + user: The User instance to add. + """ + cls._db[user.id] = user + + @classmethod + def update(cls, user: User) -> None: + """ + Update an existing user in the database. + + Args: + user: The User instance with updated data. + + Raises: + KeyError: If the user does not exist. + """ + if user.id not in cls._db: + msg = f"User with id {user.id} does not exist." + raise KeyError(msg) + cls._db[user.id] = user + + @classmethod + def delete(cls, user_id: int) -> None: + """ + Delete a user from the database by their ID. + + Args: + user_id: The ID of the user to delete. + + Raises: + KeyError: If the user does not exist. + """ + if user_id not in cls._db: + msg = f"User with id {user_id} does not exist." + raise KeyError(msg) + del cls._db[user_id] + + @classmethod + def get(cls, user_id: int) -> User | None: + """ + Retrieve a user by their ID. + + Args: + user_id: The ID of the user to retrieve. + + Returns: + The User instance if found, else None. + """ + return cls._db.get(user_id) + + @classmethod + def new_user_id(cls) -> int: + """ + Return a free user ID. + """ + return max(cls._db.keys(), default=0) + 1 + + +app = Flask(__name__) + + +@app.errorhandler(404) +def not_found(error: werkzeug.exceptions.NotFound) -> tuple[Response, Literal[404]]: + """ + Handle 404 Not Found errors. + + Args: + error: + The error that occurred. + + Returns: + A JSON response with error details and HTTP 404 status code. + """ + return jsonify({ + "title": "Not Found", + "status": 404, + "detail": error.description, + "instance": request.path, + }), 404 + + +@app.errorhandler(400) +def bad_request(error: werkzeug.exceptions.BadRequest) -> tuple[Response, Literal[400]]: + """ + Handle 400 Bad Request errors. + + Args: + error: + The error that occurred. + + Returns: + A JSON response with error details and HTTP 400 status code. + """ + return jsonify({ + "title": "Bad Request", + "status": 400, + "detail": error.description, + "instance": request.path, + }), 400 + + +@app.route("/users/") +def get_user_by_id(uid: int) -> Response: + """ + Retrieve a user by their ID. + + This endpoint demonstrates how a provider might expose user data to a + consumer. If the user is not found, a 404 error is returned. + + Args: + uid: + The ID of the user to fetch. + + Returns: + A JSON response containing the user data if found. + + Raises: + werkzeug.exceptions.NotFound: + If the user does not exist in the database. + """ + logger.debug("GET /users/%s", uid) + user = UserDb.get(uid) + if not user: + abort(404, description="User not found") + return jsonify(user.to_dict()) + + +@app.route("/users/", methods=["POST"]) +def create_user() -> Response: + """ + Create a new user in the system. + + This endpoint accepts user data as JSON in the request body and adds a new + user to the fake database. The user ID is automatically assigned. This + example illustrates how a provider might handle resource creation and + validation. + + Returns: + A JSON response containing the created user data with HTTP 201 status + code. + + Raises: + werkzeug.exceptions.BadRequest: + If the request body is not valid JSON or required fields are + missing. + """ + logger.debug("GET /users/") + if request.json is None: + abort(400, description="Invalid JSON data") + + user: dict[str, Any] = request.json + new_user = User( + id=UserDb.new_user_id(), + name=user["name"], + created_on=datetime.now(tz=timezone.utc), + email=user.get("email"), + ip_address=user.get("ip_address"), + hobbies=user.get("hobbies", []), + admin=user.get("admin", False), + ) + UserDb.create(new_user) + return jsonify(new_user.to_dict()) + + +@app.route("/users/", methods=["DELETE"]) +def delete_user(uid: int) -> tuple[str | Response, int]: + """ + Delete a user by their ID. + + This endpoint removes a user from the fake database. If the user does not + exist, a 404 error is returned. This demonstrates how a provider might + implement resource deletion and error handling. + + Args: + uid: + The ID of the user to delete. + + Returns: + An empty response with HTTP 204 status code if successful. + + Raises: + werkzeug.exceptions.NotFound: + If the user does not exist in the database. + """ + logger.debug("DELETE /users/%s", uid) + if UserDb.get(uid) is None: + abort(404, description="User not found") + UserDb.delete(uid) + return "", 204 diff --git a/examples/http/aiohttp_and_flask/pyproject.toml b/examples/http/aiohttp_and_flask/pyproject.toml new file mode 100644 index 000000000..eee5c2bd1 --- /dev/null +++ b/examples/http/aiohttp_and_flask/pyproject.toml @@ -0,0 +1,33 @@ +#:schema https://json.schemastore.org/pyproject.json +[project] +name = "example-aiohttp-and-flask" + +description = "Example of using an aiohttp client and Flask server with Pact Python" + +dependencies = ["aiohttp~=3.0", "flask~=3.0", "typing-extensions~=4.0"] +requires-python = ">=3.9" +version = "1.0.0" + +[dependency-groups] +test = [ + "coverage[toml]~=7.0", + "pact-python", + "pytest-asyncio~=1.0", + "pytest~=8.0", +] + +[tool.uv.sources] +pact-python = { path = "../../../" } + +[tool.ruff] +extend = "../../.ruff.toml" + +[tool.pytest] + + [tool.pytest.ini_options] + addopts = ["--import-mode=importlib"] + asyncio_default_fixture_loop_scope = "session" + + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" diff --git a/examples/http/aiohttp_and_flask/test_consumer.py b/examples/http/aiohttp_and_flask/test_consumer.py new file mode 100644 index 000000000..d02d048e5 --- /dev/null +++ b/examples/http/aiohttp_and_flask/test_consumer.py @@ -0,0 +1,161 @@ +""" +Consumer contract tests using Pact. + +This module demonstrates how to test a consumer (see +[`consumer.py`][examples.http.aiohttp_and_flask.consumer]) against a mock +provider using Pact. The mock provider is set up by Pact to validate that the +consumer makes the expected requests and can handle the provider's responses. +Once validated, the contract can be published to a Pact Broker for use in +provider verification. + +For more information on consumer testing with Pact, see the [Pact Consumer +Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-consumer-pact-test) +section of the Pact documentation. +""" + +from __future__ import annotations + +import logging +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any + +import aiohttp +import pytest + +from examples.http.aiohttp_and_flask.consumer import UserClient +from pact import Pact, match + +if TYPE_CHECKING: + from collections.abc import Generator + from pathlib import Path + +logger = logging.getLogger(__name__) + + +@pytest.fixture +def pact(pacts_path: Path) -> Generator[Pact, None, None]: + """ + Set up a Pact mock provider for consumer tests. + + This fixture defines the consumer and provider, and sets up the mock + provider using Pact. Each test can then define the expected request and + response using the Pact DSL. This allows the consumer to be tested in + isolation from the real provider, ensuring that the contract is correct + before integration. + + Args: + pacts_path: + The path where the generated pact file will be written. + + Yields: + A Pact object for use in tests. + """ + pact = Pact("aiohttp-consumer", "flask-provider").with_specification("V4") + yield pact + pact.write_file(pacts_path) + + +@pytest.mark.asyncio +async def test_get_user(pact: Pact) -> None: + """ + Test the GET request for a user. + + This test defines the expected interaction for a GET request for a user. It + demonstrates how to use Pact to specify the expected request and response, + and how to verify that the consumer code can handle the response correctly. + """ + response: dict[str, object] = { + "id": match.int(123), + "name": match.str("Alice"), + "created_on": match.datetime(), + } + ( + pact.upon_receiving("A user request") + .given("the user exists", id=123, name="Alice") + .with_request("GET", "/users/123") + .will_respond_with(200) + .with_body(response, content_type="application/json") + ) + + with pact.serve() as srv: + async with UserClient(str(srv.url)) as client: + user = await client.get_user(123) + assert user.name == "Alice" + + +@pytest.mark.asyncio +async def test_get_unknown_user(pact: Pact) -> None: + """ + Test the GET request for an unknown user. + + This test defines the expected interaction for a GET request for a user that + does not exist. It verifies that the consumer handles error responses as + expected. + """ + response = {"detail": "User not found"} + ( + pact.upon_receiving("A request for an unknown user") + .given("the user doesn't exist", id=123) + .with_request("GET", "/users/123") + .will_respond_with(404) + .with_body(response, content_type="application/json") + ) + + with pact.serve() as srv: + async with UserClient(str(srv.url)) as client: + with pytest.raises(aiohttp.ClientError): + await client.get_user(123) + + +@pytest.mark.asyncio +async def test_create_user(pact: Pact) -> None: + """ + Test the POST request for creating a new user. + + This test defines the expected interaction for a POST request to create a + new user. It demonstrates how to specify the request and response, and how + to verify that the consumer can handle the provider's response. This also + shows how Pact can support multiple requests and responses within a single + test case. + """ + payload: dict[str, Any] = {"name": "Bob"} + response: dict[str, Any] = { + "id": match.int(1000), + "name": "Bob", + "created_on": match.datetime(datetime.now(tz=timezone.utc)), + } + + ( + pact.upon_receiving("A request to create a new user") + .with_request("POST", "/users") + .with_body(payload, content_type="application/json") + .will_respond_with(200) + .with_body(response, content_type="application/json") + ) + + with pact.serve() as srv: + async with UserClient(str(srv.url)) as client: + user = await client.create_user(name="Bob") + assert user.id == 1000 + + +@pytest.mark.asyncio +async def test_delete_user(pact: Pact) -> None: + """ + Test the DELETE request for deleting a user. + + This test defines the expected interaction for a DELETE request to delete a + user. It demonstrates how to use Pact to specify the expected request and + response, and how to verify that the consumer code can handle the response + correctly. + """ + ( + pact.upon_receiving("A user deletion request") + .given("the user exists", id=124, name="Bob") + .with_request("DELETE", "/users/124") + .will_respond_with(204) + ) + + with pact.serve() as srv: + async with UserClient(str(srv.url)) as client: + await client.delete_user(124) diff --git a/examples/http/aiohttp_and_flask/test_provider.py b/examples/http/aiohttp_and_flask/test_provider.py new file mode 100644 index 000000000..7f91e9f15 --- /dev/null +++ b/examples/http/aiohttp_and_flask/test_provider.py @@ -0,0 +1,228 @@ +""" +Provider contract tests using Pact. + +This module demonstrates how to test a Flask provider (see +[`provider.py`][examples.http.aiohttp_and_flask.provider]) against a mock +consumer using Pact. The mock consumer replays the requests defined by the +consumer contract, and Pact validates that the provider responds as expected. + +These tests show how provider verification ensures that the provider remains +compatible with the consumer contract as the provider evolves. Provider state +management is handled by mocking the database and using provider state +endpoints. For more, see the [Pact Provider +Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-provider-pact-test) +section of the Pact documentation. +""" + +from __future__ import annotations + +import contextlib +import dataclasses +import logging +from datetime import datetime, timezone +from threading import Thread +from typing import TYPE_CHECKING, Any, Literal + +import pytest + +import pact._util +from examples.http.aiohttp_and_flask.provider import User, UserDb, app +from pact import Verifier + +if TYPE_CHECKING: + from pathlib import Path + + from typing_extensions import TypeAlias + + ACTION_TYPE: TypeAlias = Literal["setup", "teardown"] + +logger = logging.getLogger(__name__) +_mock_user_db = None + + +@pytest.fixture +def app_server() -> str: + """ + Run the Flask server for provider verification. + + Returns: + The base URL of the running Flask server. + """ + hostname = "localhost" + port = pact._util.find_free_port() # noqa: SLF001 + Thread( + target=app.run, + kwargs={"host": hostname, "port": port}, + daemon=True, + ).start() + return f"http://{hostname}:{port}" + + +def test_provider(app_server: str, pacts_path: Path) -> None: + """ + Test the provider against the mock consumer contract. + + This test runs the Pact verifier against the Flask provider, using the + contract generated by the consumer tests. + + Provider state handlers are essential in Pact contract testing. They allow + the provider to be set up in a specific state before each interaction is + verified. For example, if a consumer expects a user to exist for a certain + request, the provider state handler ensures the database is populated + accordingly. This enables repeatable, isolated, and meaningful contract + verification, as each interaction can be tested in the correct context + without relying on global or persistent state. + + In this example, the state handlers `mock_user_exists` and + `mock_user_does_not_exist` are mapped to the states described in the + contract. They are responsible for setting up (and tearing down) the + in-memory database so that the provider can respond correctly to each + request defined by the consumer contract. + + For additional information on state handlers, see + [`Verifier.state_handler`][pact.verifier.Verifier.state_handler]. + """ + verifier = ( + Verifier("flask-provider") + .add_source(pacts_path) + .add_transport(url=app_server) + .state_handler( + { + "the user exists": mock_user_exists, + "the user doesn't exist": mock_user_does_not_exist, + }, + teardown=True, + ) + ) + + verifier.verify() + + +def default_mock_db() -> dict[int, User]: + """ + Standard in-memory database for provider state mocking. + + This function pre-populates a mock database with some default users. It is + used by the provider state handlers to ensure that the database is in the + correct state for each interaction. + + Returns: + A dictionary of user IDs to User objects for use in tests. + """ + return { + 1: User( + id=1, + name="Alice", + email="alice@example.com", + created_on=datetime(2020, 1, 1, 12, 0, 0, tzinfo=timezone.utc), + ip_address="1.2.3.4", + hobbies=["pact testing", "programming", "qa"], + admin=False, + ), + 2: User( + id=2, + name="Bob", + email=None, # Edge case: email is None + created_on=datetime(1999, 12, 31, 23, 59, 59, tzinfo=timezone.utc), + ip_address="", + hobbies=[], + admin=True, + ), + 10: User( + id=10, + name="Charlie", + email="charlie@example.com", + created_on=datetime(2025, 8, 8, 8, 8, 8, tzinfo=timezone.utc), + ip_address="3.4.5.6", + hobbies=[""], + admin=False, + ), + 42: User( + id=42, + name="Dana", + email="dana+test@example.com", + created_on=datetime(2022, 2, 22, 2, 22, 22, tzinfo=timezone.utc), + ip_address="255.255.255.255", + hobbies=["edge", "case", "testing"], + admin=False, + ), + } + + +def mock_user_exists( + action: Literal["setup", "teardown"], + parameters: dict[str, Any], +) -> None: + """ + Mock the provider state where a user exists. + + This handler sets up the provider so that a user with the given ID exists in + the database. Used by Pact to ensure the provider is in the correct state + for each interaction. + + Args: + action: + The action to perform, either "setup" or "teardown". + + parameters: + User information, including an `id` to guarantee presence in the + database. Additional fields may be provided to override defaults. + """ + logger.debug("mock_user_exists(%s, %r)", action, parameters) + + # We pre-populate the database with some data, and if a state requires + # some specific data, ensure the user is present. + db = default_mock_db() + user = db[uid] if (uid := parameters.get("id")) in db else next(iter(db.values())) + user = User(**{**dataclasses.asdict(user), **parameters}) + + if action == "setup": + UserDb.create(user) + return + + if action == "teardown": + with contextlib.suppress(KeyError): + UserDb.delete(user.id) + return + + msg = f"Unknown action: {action}" + raise ValueError(msg) + + +def mock_user_does_not_exist( + action: Literal["setup", "teardown"], + parameters: dict[str, Any], +) -> None: + """ + Mock the provider state where a user does not exist. + + This handler sets up the provider so that a user with the given ID does not + exist in the database. Used by Pact to ensure the provider is in the correct + state for each interaction. + + Args: + action: + The action to perform, either "setup" or "teardown". + + parameters: + User information, must contain an `id` to guarantee absence in the + database. + """ + logger.debug("mock_user_does_not_exist(%s, %r)", action, parameters) + + if "id" not in parameters: + msg = "State must contain an 'id' field to mock user non-existence" + raise ValueError(msg) + + uid = parameters["id"] + + if action == "setup": + if user := UserDb.get(uid): + UserDb.delete(user.id) + return + + if action == "teardown": + return + + msg = f"Unknown action: {action}" + raise ValueError(msg) diff --git a/examples/pacts/.gitignore b/examples/pacts/.gitignore deleted file mode 100644 index d6b7ef32c..000000000 --- a/examples/pacts/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/examples/plugins/__init__.py b/examples/plugins/__init__.py index e69de29bb..6e031999e 100644 --- a/examples/plugins/__init__.py +++ b/examples/plugins/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/examples/v2/src/fastapi.py b/examples/v2/src/fastapi.py deleted file mode 100644 index 2a3b9e06b..000000000 --- a/examples/v2/src/fastapi.py +++ /dev/null @@ -1,153 +0,0 @@ -""" -FastAPI provider example. - -This modules defines a simple -[provider](https://docs.pact.io/getting_started/terminology#service-provider) -which will be tested with Pact in the [provider -test][examples.tests.test_01_provider_fastapi]. As Pact is a consumer-driven -framework, the consumer defines the contract which the provider must then -satisfy. - -The provider is the application which receives requests from another service -(the consumer) and returns a response. In this example, we have a simple -endpoint which returns a user's information from a (fake) database. - -This also showcases how Pact tests differ from merely testing adherence to an -OpenAPI specification. The Pact tests are more concerned with the practical use -of the API, rather than the formally defined specification. The User class -defined here has additional fields which are not used by the consumer. Should -the provider later decide to add or remove fields, Pact's consumer-driven -testing will provide feedback on whether the consumer is compatible with the -provider's changes. - -Note that the code in this module is agnostic of Pact (i.e., this would be your -production code). The `pact-python` dependency only appears in the tests. This -is because the consumer is not concerned with Pact, only the tests are. -""" - -from __future__ import annotations - -import logging -from datetime import datetime, timezone -from typing import Annotated, Any, Optional - -from pydantic import BaseModel, PlainSerializer - -from fastapi import FastAPI, HTTPException - -app = FastAPI() -logger = logging.getLogger(__name__) - - -class User(BaseModel): - """User data class.""" - - id: int - name: str - created_on: Annotated[ - datetime, - PlainSerializer( - lambda dt: dt.strftime("%Y-%m-%dT%H:%M:%S%z"), - return_type=str, - when_used="json", - ), - ] - email: Optional[str] - ip_address: Optional[str] - hobbies: list[str] - admin: bool - - def __post_init__(self) -> None: - """ - Validate the User data. - - This performs the following checks: - - - The name cannot be empty - - The id must be a positive integer - - Raises: - ValueError: If any of the above checks fail. - """ - if not self.name: - msg = "User must have a name" - raise ValueError(msg) - - if self.id < 0: - msg = "User ID must be a positive integer" - raise ValueError(msg) - - def __repr__(self) -> str: - """Return the user's name.""" - return f"User({self.id}:{self.name})" - - -""" -As this is a simple example, we'll use a simple dict to represent a database. -This would be replaced with a real database in a real application. - -When testing the provider in a real application, the calls to the database would -be mocked out to avoid the need for a real database. An example of this can be -found in the [test suite][examples.tests.test_01_provider_fastapi]. -""" -FAKE_DB: dict[int, User] = {} - - -@app.get("/users/{uid}") -async def get_user_by_id(uid: int) -> User: - """ - Fetch a user by their ID. - - Args: - uid: The ID of the user to fetch - - Returns: - The user data if found, HTTP 404 if not - """ - user = FAKE_DB.get(uid) - if not user: - raise HTTPException(status_code=404, detail="User not found") - return user - - -@app.post("/users/") -async def create_new_user(user: dict[str, Any]) -> User: - """ - Create a new user . - - Args: - user: The user data to create - - Returns: - The status code 200 and user data if successfully created, HTTP 404 if not - """ - if "id" in user: - raise HTTPException(status_code=400, detail="ID should not be provided.") - uid = len(FAKE_DB) - FAKE_DB[uid] = User( - id=uid, - name=user["name"], - created_on=datetime.now(tz=timezone.utc), - email=user.get("email"), - ip_address=user.get("ip_address"), - hobbies=user.get("hobbies", []), - admin=user.get("admin", False), - ) - return FAKE_DB[uid] - - -@app.delete("/users/{uid}", status_code=204) -async def delete_user(uid: int): # noqa: ANN201 - """ - Delete an existing user . - - Args: - uid: The ID of the user to delete - - Returns: - The status code 204, HTTP 404 if not - """ - if uid not in FAKE_DB: - raise HTTPException(status_code=404, detail="User not found") - - del FAKE_DB[uid] diff --git a/examples/v2/src/flask.py b/examples/v2/src/flask.py deleted file mode 100644 index 6be632e5a..000000000 --- a/examples/v2/src/flask.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Flask provider example. - -This modules defines a simple -[provider](https://docs.pact.io/getting_started/terminology#service-provider) -which will be tested with Pact in the [provider -test][examples.tests.test_01_provider_flask]. As Pact is a consumer-driven -framework, the consumer defines the contract which the provider must then -satisfy. - -The provider is the application which receives requests from another service -(the consumer) and returns a response. In this example, we have a simple -endpoint which returns a user's information from a (fake) database. - -This also showcases how Pact tests differ from merely testing adherence to an -OpenAPI specification. The Pact tests are more concerned with the practical use -of the API, rather than the formally defined specification. The User class -defined here has additional fields which are not used by the consumer. Should -the provider later decide to add or remove fields, Pact's consumer-driven -testing will provide feedback on whether the consumer is compatible with the -provider's changes. - -Note that the code in this module is agnostic of Pact (i.e., this would be your -production code). The `pact-python` dependency only appears in the tests. This -is because the consumer is not concerned with Pact, only the tests are. -""" - -from __future__ import annotations - -import logging -from dataclasses import dataclass -from datetime import datetime, timezone -from typing import Any - -from flask import Flask, Response, abort, jsonify, request - -logger = logging.getLogger(__name__) -app = Flask(__name__) - - -@dataclass() -class User: - """User data class.""" - - id: int - name: str - created_on: datetime - email: str | None - ip_address: str | None - hobbies: list[str] - admin: bool - - def __post_init__(self) -> None: - """ - Validate the User data. - - This performs the following checks: - - - The name cannot be empty - - The id must be a positive integer - - Raises: - ValueError: If any of the above checks fail. - """ - if not self.name: - msg = "User must have a name" - raise ValueError(msg) - - if self.id < 0: - msg = "User ID must be a positive integer" - raise ValueError(msg) - - def __repr__(self) -> str: - """Return the user's name.""" - return f"User({self.id}:{self.name})" - - def dict(self) -> dict[str, Any]: - """ - Return the user's data as a dict. - """ - return { - "id": self.id, - "name": self.name, - "created_on": self.created_on.strftime("%Y-%m-%dT%H:%M:%S%z"), - "email": self.email, - "ip_address": self.ip_address, - "hobbies": self.hobbies, - "admin": self.admin, - } - - -""" -As this is a simple example, we'll use a simple dict to represent a database. -This would be replaced with a real database in a real application. - -When testing the provider in a real application, the calls to the database would -be mocked out to avoid the need for a real database. An example of this can be -found in the [test suite][examples.tests.test_01_provider_flask]. -""" -FAKE_DB: dict[int, User] = {} - - -@app.route("/users/") -def get_user_by_id(uid: int) -> Response | tuple[Response, int]: - """ - Fetch a user by their ID. - - Args: - uid: The ID of the user to fetch - - Returns: - The user data if found, HTTP 404 if not - """ - user = FAKE_DB.get(uid) - if not user: - return jsonify({"detail": "User not found"}), 404 - return jsonify(user.dict()) - - -@app.route("/users/", methods=["POST"]) -def create_user() -> Response: - if request.json is None: - abort(400, description="Invalid JSON data") - - user: dict[str, Any] = request.json - uid = len(FAKE_DB) - FAKE_DB[uid] = User( - id=uid, - name=user["name"], - created_on=datetime.now(tz=timezone.utc), - email=user.get("email"), - ip_address=user.get("ip_address"), - hobbies=user.get("hobbies", []), - admin=user.get("admin", False), - ) - return jsonify(FAKE_DB[uid].dict()) - - -@app.route("/users/", methods=["DELETE"]) -def delete_user(uid: int) -> tuple[str | Response, int]: - if uid not in FAKE_DB: - return jsonify({"detail": "User not found"}), 404 - del FAKE_DB[uid] - return "", 204 diff --git a/examples/v2/tests/test_00_consumer.py b/examples/v2/tests/test_00_consumer.py deleted file mode 100644 index ffe964c9b..000000000 --- a/examples/v2/tests/test_00_consumer.py +++ /dev/null @@ -1,207 +0,0 @@ -""" -Test the consumer with Pact. - -This module tests the consumer defined in `src/consumer.py` against a mock -provider. The mock provider is set up by Pact, and is used to ensure that the -consumer is making the expected requests to the provider, and that the provider -is responding with the expected responses. Once these interactions are -validated, the contracts can be published to a Pact Broker. The contracts can -then be used to validate the provider's interactions. - -A good resource for understanding the consumer tests is the [Pact Consumer -Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-consumer-pact-test) -section of the Pact documentation. -""" - -from __future__ import annotations - -import logging -from http import HTTPStatus -from typing import TYPE_CHECKING, Any - -import pytest -import requests -from yarl import URL - -from examples.v2.src.consumer import User, UserConsumer -from pact.v2 import Consumer, Format, Like, Provider - -if TYPE_CHECKING: - from collections.abc import Generator - from pathlib import Path - - from pact.v2.pact import Pact - -logger = logging.getLogger(__name__) - -MOCK_URL = URL("http://localhost:8080") - - -@pytest.fixture -def user_consumer() -> UserConsumer: - """ - Returns an instance of the UserConsumer class. - - As we do not want to stand up all of the consumer's dependencies, we direct - the consumer to use Pact's mock provider. This allows us to define what - requests the consumer will make to the provider, and what responses the - provider will return. - - The ability for the client to specify the expected response from the - provider is critical to Pact's consumer-driven approach as it allows the - consumer to declare the minimal response it requires from the provider (even - if the provider is returning more data than the consumer needs). - """ - return UserConsumer(str(MOCK_URL)) - - -@pytest.fixture(scope="module") -def pact(broker: URL, pacts_path: Path) -> Generator[Pact, Any, None]: - """ - Set up Pact. - - In order to test the consumer in isolation, Pact sets up a mock version of - the provider. This mock provider will expect to receive defined requests - and will respond with defined responses. - - The fixture here simply defines the Consumer and Provider, and sets up the - mock provider. With each test, we define the expected request and response - from the provider as follows: - - ```python - pact.given("UserA exists and is not an admin") \ - .upon_receiving("A request for UserA") \ - .with_request("get", "/users/123") \ - .will_respond_with(200, body=Like(expected)) - ``` - """ - consumer = Consumer("UserConsumer") - pact = consumer.has_pact_with( - Provider("UserProvider"), - pact_dir=pacts_path, - publish_to_broker=True, - # Mock service configuration - host_name=MOCK_URL.host, - port=MOCK_URL.port, - # Broker configuration - broker_base_url=str(broker), - broker_username=broker.user, - broker_password=broker.password, - ) - - pact.start_service() - yield pact - pact.stop_service() - - -def test_get_existing_user(pact: Pact, user_consumer: UserConsumer) -> None: - """ - Test request for an existing user. - - This test defines the expected request and response from the provider. The - provider will be expected to return a response with a status code of 200, - """ - # When setting up the expected response, the consumer should only define - # what it needs from the provider (as opposed to the full schema). Should - # the provider later decide to add or remove fields, Pact's consumer-driven - # approach will ensure that interaction is still valid. - expected: dict[str, Any] = { - "id": Format().integer, - "name": "Verna Hampton", - "created_on": Format().iso_8601_datetime(), - } - - ( - pact.given("user 123 exists") - .upon_receiving("a request for user 123") - .with_request("get", "/users/123") - .will_respond_with(200, body=Like(expected)) - ) - - with pact: - user = user_consumer.get_user(123) - - assert isinstance(user, User) - assert user.name == "Verna Hampton" - - pact.verify() - - -def test_get_unknown_user(pact: Pact, user_consumer: UserConsumer) -> None: - expected = {"detail": "User not found"} - - ( - pact.given("user 123 doesn't exist") - .upon_receiving("a request for user 123") - .with_request("get", "/users/123") - .will_respond_with(404, body=Like(expected)) - ) - - with pact: - with pytest.raises(requests.HTTPError) as excinfo: - user_consumer.get_user(123) - assert excinfo.value.response is not None - assert excinfo.value.response.status_code == HTTPStatus.NOT_FOUND - pact.verify() - - -def test_create_user(pact: Pact, user_consumer: UserConsumer) -> None: - """ - Test the POST request for creating a new user. - - This test defines the expected interaction for a POST request to create - a new user. It sets up the expected request and response from the provider, - including the request body and headers, and verifies that the response - status code is 200 and the response body matches the expected user data. - """ - body = {"name": "Verna Hampton"} - expected_response: dict[str, Any] = { - "id": 124, - "name": "Verna Hampton", - "created_on": Format().iso_8601_datetime(), - } - - ( - pact.given("create user 124") - .upon_receiving("A request to create a new user") - .with_request( - method="POST", - path="/users/", - body=body, - headers={"Content-Type": "application/json"}, - ) - .will_respond_with( - status=200, - body=Like(expected_response), - ) - ) - - with pact: - user = user_consumer.create_user(name="Verna Hampton") - assert user.id > 0 - assert user.name == "Verna Hampton" - assert user.created_on - - pact.verify() - - -def test_delete_request_to_delete_user(pact: Pact, user_consumer: UserConsumer) -> None: - """ - Test the DELETE request for deleting a user. - - This test defines the expected interaction for a DELETE request to delete - a user. It sets up the expected request and response from the provider, - including the request body and headers, and verifies that the response - status code is 200 and the response body matches the expected user data. - """ - ( - pact.given("delete the user 124") - .upon_receiving("a request for deleting user") - .with_request(method="DELETE", path="/users/124") - .will_respond_with(status=204) - ) - - with pact: - user_consumer.delete_user(124) - - pact.verify() diff --git a/examples/v2/tests/test_01_provider_fastapi.py b/examples/v2/tests/test_01_provider_fastapi.py deleted file mode 100644 index b997cc6ed..000000000 --- a/examples/v2/tests/test_01_provider_fastapi.py +++ /dev/null @@ -1,223 +0,0 @@ -""" -Test the FastAPI provider with Pact. - -This module tests the FastAPI provider defined in `src/fastapi.py` against the -mock consumer. The mock consumer is set up by Pact and will replay the requests -defined by the consumers. Pact will then validate that the provider responds -with the expected responses. - -The provider will be expected to be in a given state in order to respond to -certain requests. For example, when fetching a user's information, the provider -will need to have a user with the given ID in the database. In order to avoid -side effects, the provider's database calls are mocked out using functionalities -from `unittest.mock`. - -In order to set the provider into the correct state, this test module defines an -additional endpoint on the provider, in this case `/_pact/provider_states`. -Calls to this endpoint mock the relevant database calls to set the provider into -the correct state. - -A good resource for understanding the provider tests is the [Pact Provider -Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-provider-pact-test) -section of the Pact documentation. -""" - -from __future__ import annotations - -import time -from datetime import datetime, timezone -from multiprocessing import Process -from typing import TYPE_CHECKING, Any, Optional -from unittest.mock import MagicMock - -import pytest -import uvicorn -from pydantic import BaseModel -from yarl import URL - -import examples.v2.src.fastapi -from examples.v2.src.fastapi import User, app -from pact.v2 import Verifier # type: ignore[import-untyped] - -if TYPE_CHECKING: - from collections.abc import Generator - -PROVIDER_URL = URL("http://localhost:8080") - - -class ProviderState(BaseModel): - """Define the provider state.""" - - consumer: str - state: str - - -@app.post("/_pact/provider_states") -async def mock_pact_provider_states( - state: ProviderState, -) -> dict[str, Optional[str]]: - """ - Define the provider state. - - For Pact to be able to correctly test compliance with the contract, the - internal state of the provider needs to be set up correctly. Naively, this - would be achieved by setting up the database with the correct data for the - test, but this can be slow and error-prone. Instead this is best achieved by - mocking the relevant calls to the database so as to avoid any side effects. - - For Pact to be able to correctly get the provider into the correct state, - this function is used to define an additional endpoint on the provider. This - endpoint is called by Pact before each test to ensure that the provider is - in the correct state. - """ - mapping = { - "user 123 doesn't exist": mock_user_123_doesnt_exist, - "user 123 exists": mock_user_123_exists, - "create user 124": mock_post_request_to_create_user, - "delete the user 124": mock_delete_request_to_delete_user, - } - mapping[state.state]() - return {"result": f"{state} set"} - - -def run_server() -> None: - """ - Run the FastAPI server. - - This function is required to run the FastAPI server in a separate process. A - lambda cannot be used as the target of a `multiprocessing.Process` as it - cannot be pickled. - """ - host = PROVIDER_URL.host if PROVIDER_URL.host else "localhost" - port = PROVIDER_URL.port if PROVIDER_URL.port else 8080 - uvicorn.run(app, host=host, port=port) - - -@pytest.fixture(scope="module") -def verifier() -> Generator[Verifier, Any, None]: - """Set up the Pact verifier.""" - proc = Process(target=run_server, daemon=True) - verifier = Verifier( - provider="UserProvider", - provider_base_url=str(PROVIDER_URL), - ) - proc.start() - time.sleep(2) - yield verifier - proc.kill() - - -def mock_user_123_doesnt_exist() -> None: - """Mock the database for the user 123 doesn't exist state.""" - examples.v2.src.fastapi.FAKE_DB = MagicMock() - examples.v2.src.fastapi.FAKE_DB.get.return_value = None - - -def mock_user_123_exists() -> None: - """ - Mock the database for the user 123 exists state. - - You may notice that the return value here differs from the consumer's - expected response. This is because the consumer's expected response is - guided by what the consumer uses. - - By using consumer-driven contracts and testing the provider against the - consumer's contract, we can ensure that the provider is what the consumer - needs. This allows the provider to safely evolve their API (by both adding - and removing fields) without fear of breaking the interactions with the - consumers. - """ - mock_db = MagicMock() - mock_db.get.return_value = User( - id=123, - name="Verna Hampton", - email="verna@example.com", - created_on=datetime.now(tz=timezone.utc), - ip_address="10.1.2.3", - hobbies=["hiking", "swimming"], - admin=False, - ) - examples.v2.src.fastapi.FAKE_DB = mock_db - - -def mock_post_request_to_create_user() -> None: - """ - Mock the database for the post request to create a user. - """ - local_db: dict[int, User] = {} - - def local_setitem(key: int, value: User) -> None: - local_db[key] = value - - def local_getitem(key: int) -> User: - return local_db[key] - - mock_db = MagicMock() - mock_db.__len__.return_value = 124 - mock_db.__setitem__.side_effect = local_setitem - mock_db.__getitem__.side_effect = local_getitem - examples.v2.src.fastapi.FAKE_DB = mock_db - - -def mock_delete_request_to_delete_user() -> None: - """ - Mock the database for the delete request to delete a user. - """ - local_db = { - 123: User( - id=123, - name="Verna Hampton", - email="verna@example.com", - created_on=datetime.now(tz=timezone.utc), - ip_address="10.1.2.3", - hobbies=["hiking", "swimming"], - admin=False, - ), - 124: User( - id=124, - name="Jane Doe", - email="jane@example.com", - created_on=datetime.now(tz=timezone.utc), - ip_address="10.1.2.5", - hobbies=["running", "dancing"], - admin=False, - ), - } - - def local_delitem(key: int) -> None: - del local_db[key] - - def local_contains(key: int) -> bool: - return key in local_db - - mock_db = MagicMock() - mock_db.__delitem__.side_effect = local_delitem - mock_db.__contains__.side_effect = local_contains - examples.v2.src.fastapi.FAKE_DB = mock_db - - -def test_against_broker(broker: URL, verifier: Verifier) -> None: - """ - Test the provider against the broker. - - The broker will be used to retrieve the contract, and the provider will be - tested against the contract. - - As Pact is a consumer-driven, the provider is tested against the contract - defined by the consumer. The consumer defines the expected request to and - response from the provider. - - For an example of the consumer's contract, see the consumer's tests. - """ - code, _ = verifier.verify_with_broker( - broker_url=str(broker), - # Despite the auth being set in the broker URL, we still need to pass - # the username and password to the verify_with_broker method. - broker_username=broker.user, - broker_password=broker.password, - publish_version="0.0.0", - publish_verification_results=True, - provider_states_setup_url=str(PROVIDER_URL / "_pact" / "provider_states"), - ) - - assert code == 0 diff --git a/examples/v2/tests/test_01_provider_flask.py b/examples/v2/tests/test_01_provider_flask.py deleted file mode 100644 index d42fb0997..000000000 --- a/examples/v2/tests/test_01_provider_flask.py +++ /dev/null @@ -1,217 +0,0 @@ -""" -Test the Flask provider with Pact. - -This module tests the Flask provider defined in `src/flask.py` against the mock -consumer. The mock consumer is set up by Pact and will replay the requests -defined by the consumers. Pact will then validate that the provider responds -with the expected responses. - -The provider will be expected to be in a given state in order to respond to -certain requests. For example, when fetching a user's information, the provider -will need to have a user with the given ID in the database. In order to avoid -side effects, the provider's database calls are mocked out using functionalities -from `unittest.mock`. - -In order to set the provider into the correct state, this test module defines an -additional endpoint on the provider, in this case `/_pact/provider_states`. -Calls to this endpoint mock the relevant database calls to set the provider into -the correct state. - -A good resource for understanding the provider tests is the [Pact Provider -Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-provider-pact-test) -section of the Pact documentation. -""" - -from __future__ import annotations - -import time -from datetime import datetime, timezone -from multiprocessing import Process -from typing import TYPE_CHECKING, Any -from unittest.mock import MagicMock - -import pytest -from yarl import URL - -import examples.v2.src.flask -from examples.v2.src.flask import User, app -from flask import request -from pact.v2 import Verifier # type: ignore[import-untyped] - -if TYPE_CHECKING: - from collections.abc import Generator - -PROVIDER_URL = URL("http://localhost:8080") - - -@app.route("/_pact/provider_states", methods=["POST"]) -async def mock_pact_provider_states() -> dict[str, str | None]: - """ - Define the provider state. - - For Pact to be able to correctly test compliance with the contract, the - internal state of the provider needs to be set up correctly. Naively, this - would be achieved by setting up the database with the correct data for the - test, but this can be slow and error-prone. Instead this is best achieved by - mocking the relevant calls to the database so as to avoid any side effects. - - For Pact to be able to correctly get the provider into the correct state, - this function is used to define an additional endpoint on the provider. This - endpoint is called by Pact before each test to ensure that the provider is - in the correct state. - """ - if request.json is None: - msg = "Request must be JSON" - raise ValueError(msg) - state: str = request.json["state"] - mapping = { - "user 123 doesn't exist": mock_user_123_doesnt_exist, - "user 123 exists": mock_user_123_exists, - "create user 124": mock_post_request_to_create_user, - "delete the user 124": mock_delete_request_to_delete_user, - } - return {"result": mapping[state]()} # type: ignore[index] - - -def run_server() -> None: - """ - Run the Flask server. - - This function is required to run the Flask server in a separate process. A - lambda cannot be used as the target of a `multiprocessing.Process` as it - cannot be pickled. - """ - app.run( - host=PROVIDER_URL.host, - port=PROVIDER_URL.port, - ) - - -@pytest.fixture(scope="module") -def verifier() -> Generator[Verifier, Any, None]: - """Set up the Pact verifier.""" - proc = Process(target=run_server, daemon=True) - verifier = Verifier( - provider="UserProvider", - provider_base_url=str(PROVIDER_URL), - ) - proc.start() - time.sleep(2) - yield verifier - proc.kill() - - -def mock_user_123_doesnt_exist() -> None: - """Mock the database for the user 123 doesn't exist state.""" - examples.v2.src.flask.FAKE_DB = MagicMock() - examples.v2.src.flask.FAKE_DB.get.return_value = None - - -def mock_user_123_exists() -> None: - """ - Mock the database for the user 123 exists state. - - You may notice that the return value here differs from the consumer's - expected response. This is because the consumer's expected response is - guided by what the consumer uses. - - By using consumer-driven contracts and testing the provider against the - consumer's contract, we can ensure that the provider is what the consumer - needs. This allows the provider to safely evolve their API (by both adding - and removing fields) without fear of breaking the interactions with the - consumers. - """ - examples.v2.src.flask.FAKE_DB = MagicMock() - examples.v2.src.flask.FAKE_DB.get.return_value = User( - id=123, - name="Verna Hampton", - email="verna@example.com", - created_on=datetime.now(tz=timezone.utc), - ip_address="10.1.2.3", - hobbies=["hiking", "swimming"], - admin=False, - ) - - -def mock_post_request_to_create_user() -> None: - """ - Mock the database for the post request to create a user. - """ - local_db: dict[int, User] = {} - - def local_setitem(key: int, value: User) -> None: - local_db[key] = value - - def local_getitem(key: int) -> User: - return local_db[key] - - mock_db = MagicMock() - mock_db.__len__.return_value = 124 - mock_db.__setitem__.side_effect = local_setitem - mock_db.__getitem__.side_effect = local_getitem - examples.v2.src.flask.FAKE_DB = mock_db - - -def mock_delete_request_to_delete_user() -> None: - """ - Mock the database for the delete request to delete a user. - """ - local_db = { - 123: User( - id=123, - name="Verna Hampton", - email="verna@example.com", - created_on=datetime.now(tz=timezone.utc), - ip_address="10.1.2.3", - hobbies=["hiking", "swimming"], - admin=False, - ), - 124: User( - id=124, - name="Jane Doe", - email="jane@example.com", - created_on=datetime.now(tz=timezone.utc), - ip_address="10.1.2.5", - hobbies=["running", "dancing"], - admin=False, - ), - } - - def local_delitem(key: int) -> None: - del local_db[key] - - def local_contains(key: int) -> bool: - return key in local_db - - mock_db = MagicMock() - mock_db.__delitem__.side_effect = local_delitem - mock_db.__contains__.side_effect = local_contains - mock_db.is_mocked = True - examples.v2.src.flask.FAKE_DB = mock_db - - -def test_against_broker(broker: URL, verifier: Verifier) -> None: - """ - Test the provider against the broker. - - The broker will be used to retrieve the contract, and the provider will be - tested against the contract. - - As Pact is a consumer-driven, the provider is tested against the contract - defined by the consumer. The consumer defines the expected request to and - response from the provider. - - For an example of the consumer's contract, see the consumer's tests. - """ - code, _ = verifier.verify_with_broker( - broker_url=str(broker), - # Despite the auth being set in the broker URL, we still need to pass - # the username and password to the verify_with_broker method. - broker_username=broker.user, - broker_password=broker.password, - publish_version="0.0.0", - publish_verification_results=True, - provider_states_setup_url=str(PROVIDER_URL / "_pact" / "provider_states"), - ) - - assert code == 0 diff --git a/mkdocs.yml b/mkdocs.yml index a3bb987a1..19e1c472f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,7 +28,9 @@ plugins: handlers: python: import: + - https://docs.aiohttp.org/en/stable/objects.inv - https://docs.python.org/3/objects.inv + - https://flask.palletsprojects.com/en/stable/objects.inv - https://googleapis.dev/python/protobuf/latest/objects.inv - https://grpc.github.io/grpc/python/objects.inv options: From dcc060f8934f431b676e3250d054e23f7d3f8260 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 11 Aug 2025 13:15:32 +1000 Subject: [PATCH 0934/1376] chore(deps): simplify testing dependencies - `coverage` is included by `pytest-cov` - `pytest-rerunfailures` won't be needed when the broker has been removed. - `pytest-xdist` testing going back to single threads Signed-off-by: JP-Ellis --- .../http/aiohttp_and_flask/pyproject.toml | 7 +------ pyproject.toml | 21 +++---------------- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/examples/http/aiohttp_and_flask/pyproject.toml b/examples/http/aiohttp_and_flask/pyproject.toml index eee5c2bd1..6205fe030 100644 --- a/examples/http/aiohttp_and_flask/pyproject.toml +++ b/examples/http/aiohttp_and_flask/pyproject.toml @@ -9,12 +9,7 @@ requires-python = ">=3.9" version = "1.0.0" [dependency-groups] -test = [ - "coverage[toml]~=7.0", - "pact-python", - "pytest-asyncio~=1.0", - "pytest~=8.0", -] +test = ["pact-python", "pytest-asyncio~=1.0", "pytest~=8.0"] [tool.uv.sources] pact-python = { path = "../../../" } diff --git a/pyproject.toml b/pyproject.toml index 4f6d70c6d..094939d20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,22 +99,17 @@ dependencies = [ "protobuf~=6.0", "pydantic~=2.0", "pytest-cov~=6.0", - "pytest-rerunfailures~=15.0", - "pytest-xdist~=3.0", "pytest~=8.0", "testcontainers~=4.0", "uvicorn[standard]~=0.0", ] devel-test = [ "aiohttp~=3.0", - "coverage[toml]~=7.0", "flask~=3.0", "pact-python-cli", "pytest-asyncio~=1.0", "pytest-bdd~=8.0", "pytest-cov~=6.0", - "pytest-rerunfailures~=15.0", - "pytest-xdist~=3.0", "pytest~=8.0", "requests~=2.0", "testcontainers~=4.0", @@ -132,8 +127,6 @@ dependencies = [ "fastapi~=0.0", "flask[async]~=3.0", "pytest-cov~=6.0", - "pytest-rerunfailures~=15.0", - "pytest-xdist~=3.0", "pytest~=8.0", "testcontainers~=4.0", "uvicorn[standard]~=0.0", @@ -143,8 +136,6 @@ dependencies = [ "httpx~=0.0", "mock~=5.0", "pytest-cov~=6.0", - "pytest-rerunfailures~=15.0", - "pytest-xdist~=3.0", "pytest~=8.0", "uvicorn[standard]~=0.0", ] @@ -209,7 +200,7 @@ requires = ["hatch-vcs", "hatchling"] all = ["example", "format", "lint", "test", "typecheck"] docs = "mkdocs serve {args}" docs-build = "mkdocs build {args}" - example = "pytest --numprocesses=1 --ignore=examples/v2 examples/ {args}" + example = "pytest --ignore=examples/v2 examples/ {args}" format = "ruff format {args}" lint = "ruff check --output-format=full --show-fixes {args}" test = "pytest --ignore=tests/v2 tests/ {args}" @@ -247,7 +238,7 @@ requires = ["hatch-vcs", "hatchling"] [tool.hatch.envs.v2-test.scripts] all = ["test"] - test = "pytest --numprocesses=1 tests/v2 {args}" + test = "pytest tests/v2 {args}" [[tool.hatch.envs.v2-test.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.9"] @@ -259,7 +250,7 @@ requires = ["hatch-vcs", "hatchling"] [tool.hatch.envs.v2-example.scripts] all = ["example"] - example = "pytest --numprocesses=1 examples/v2 {args}" + example = "pytest examples/v2 {args}" [[tool.hatch.envs.v2-example.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.9"] @@ -287,12 +278,6 @@ requires = ["hatch-vcs", "hatchling"] "--cov-config=pyproject.toml", "--cov-report=xml", "--cov=pact", - # Xdist options - "--dist=worksteal", - "--numprocesses=2", - # Rerun options - "--rerun-except=assert", - "--reruns=3", ] asyncio_default_fixture_loop_scope = "session" filterwarnings = [ From fbd0e327cb230707e863013013397940a98efa2d Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 11 Aug 2025 13:30:21 +1000 Subject: [PATCH 0935/1376] chore(ci): use new examples Run each example in its own standalone venv. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 57 ++++++++++++-------------------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 91769ce40..9c1a2b049 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -121,39 +121,27 @@ jobs: on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.experimental }} - - services: - broker: - image: pactfoundation/pact-broker:latest@sha256:05b05a192c771b33ba67ffdf2d6829bb32600145ca8f154165187d318c8ee70f - ports: - - 9292:9292 - env: - # Basic auth credentials for the Broker - PACT_BROKER_ALLOW_PUBLIC_READ: 'true' - PACT_BROKER_BASIC_AUTH_USERNAME: pactbroker - PACT_BROKER_BASIC_AUTH_PASSWORD: pactbroker - # Database - PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite strategy: fail-fast: false matrix: os: + - macos-latest - ubuntu-latest + - windows-latest python-version: - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' - experimental: - - false + # Python 3.9 aren't supported on macos-latest (ARM) + exclude: + - os: macos-latest + python-version: '3.9' include: - # Run tests against the next Python version, but no need for the full list of OSes. - - os: ubuntu-latest - python-version: '3.14' - experimental: true + - os: macos-13 + python-version: '3.9' steps: - name: Checkout code @@ -169,26 +157,17 @@ jobs: - name: Install Python run: uv python install ${{ matrix.python-version }} - - name: Install Hatch - run: uv tool install hatch - - - name: Ensure broker is live + - name: Run examples + shell: bash run: | - i=0 - until curl -sSf http://localhost:9292/diagnostic/status/heartbeat; do - i=$((i+1)) - if [ $i -gt 120 ]; then - echo "Broker failed to start" - exit 1 - fi - sleep 1 - done - - - name: Run tests - run: hatch run example:example --broker-url=http://pactbroker:pactbroker@localhost:9292 --container - - - name: Examples (v2) - run: hatch run v2-example:example --broker-url=http://pactbroker:pactbroker@localhost:9292 + set -o errexit + set -o pipefail + + find examples -name pyproject.toml -print0 | + while IFS= read -r -d $'\0' file; do + cd "$(dirname "$file")" + uv run --python ${{ matrix.python-version }} --group test pytest + done - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' From 73f532b686ccb50e5ceb9e564b0d7be9fa02fff8 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 11 Aug 2025 13:33:23 +1000 Subject: [PATCH 0936/1376] chore: update protobuf examples So that they are in line with the updated Ruff linting rules. Signed-off-by: JP-Ellis --- examples/plugins/proto/person_pb2.py | 12 +- examples/plugins/proto/person_pb2.pyi | 140 ++++++++- examples/plugins/proto/person_pb2_grpc.py | 323 +++++++++++++++++---- examples/plugins/protobuf/__init__.py | 4 +- examples/plugins/protobuf/test_consumer.py | 5 +- examples/plugins/protobuf/test_provider.py | 5 +- 6 files changed, 407 insertions(+), 82 deletions(-) diff --git a/examples/plugins/proto/person_pb2.py b/examples/plugins/proto/person_pb2.py index 95741a653..3bf786c1f 100644 --- a/examples/plugins/proto/person_pb2.py +++ b/examples/plugins/proto/person_pb2.py @@ -1,10 +1,16 @@ # ruff: noqa: PGH004 # ruff: noqa -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE # source: person.proto # Protobuf Python Version: 6.31.0 -"""Generated protocol buffer code.""" +""" +Protocol buffer message and service definitions for the AddressBook pedagogical example. + +This module is auto-generated from the person.proto file using the protobuf compiler. It provides Python classes for all messages and services defined in the proto file, and is intended for use in educational and demonstration contexts. + +!!! note + + This file is generated code. Manual changes (except for documentation improvements) will be overwritten if the file is regenerated. +""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/examples/plugins/proto/person_pb2.pyi b/examples/plugins/proto/person_pb2.pyi index 0d1547245..a2cbccc96 100644 --- a/examples/plugins/proto/person_pb2.pyi +++ b/examples/plugins/proto/person_pb2.pyi @@ -1,3 +1,13 @@ +""" +Type stubs for protocol buffer messages and services for the AddressBook pedagogical example. + +This module is auto-generated from the person.proto file and provides type hints for all messages and services defined in the proto file. It is intended for use in educational and demonstration contexts, and helps with static analysis and editor support. + +!!! note + + This file is generated code. Manual changes (except for documentation improvements) will be overwritten if the file is regenerated. +""" + # ruff: noqa: PGH004 # ruff: noqa from collections.abc import Iterable as _Iterable @@ -14,8 +24,16 @@ from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper DESCRIPTOR: _descriptor.FileDescriptor class Person(_message.Message): + """ + Represents a person in the AddressBook example. + """ + __slots__ = ("email", "id", "name", "phones") class PhoneType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + """ + Enum for the type of phone number. + """ + __slots__ = () PHONE_TYPE_UNSPECIFIED: _ClassVar[Person.PhoneType] PHONE_TYPE_MOBILE: _ClassVar[Person.PhoneType] @@ -27,6 +45,10 @@ class Person(_message.Message): PHONE_TYPE_HOME: Person.PhoneType PHONE_TYPE_WORK: Person.PhoneType class PhoneNumber(_message.Message): + """ + Represents a phone number for a person. + """ + __slots__ = ("number", "type") NUMBER_FIELD_NUMBER: _ClassVar[int] TYPE_FIELD_NUMBER: _ClassVar[int] @@ -36,7 +58,16 @@ class Person(_message.Message): self, number: _Optional[str] = ..., type: _Optional[_Union[Person.PhoneType, str]] = ..., - ) -> None: ... + ) -> None: + """ + Create a new PhoneNumber instance. + + Args: + number: + The phone number as a string. + type: + The type of phone number (e.g., mobile, home, work). + """ NAME_FIELD_NUMBER: _ClassVar[int] ID_FIELD_NUMBER: _ClassVar[int] @@ -52,48 +83,135 @@ class Person(_message.Message): id: _Optional[int] = ..., email: _Optional[str] = ..., phones: _Optional[_Iterable[_Union[Person.PhoneNumber, _Mapping]]] = ..., - ) -> None: ... + ) -> None: + """ + Creates a new Person instance. + + Args: + name: + The person's name. + id: + The unique identifier for the person. + email: + The person's email address. + phones: + A list of phone numbers for the person. + """ class AddressBook(_message.Message): + """ + Represents an address book containing multiple people. + """ + __slots__ = ("people",) PEOPLE_FIELD_NUMBER: _ClassVar[int] people: _containers.RepeatedCompositeFieldContainer[Person] def __init__( - self, people: _Optional[_Iterable[_Union[Person, _Mapping]]] = ... - ) -> None: ... + self, + people: _Optional[_Iterable[_Union[Person, _Mapping]]] = ..., + ) -> None: + """ + Creates a new AddressBook instance. + + Args: + people: A list of Person objects in the address book. + """ class GetPersonRequest(_message.Message): + """ + Request message for retrieving a person by ID. + """ + __slots__ = ("person_id",) PERSON_ID_FIELD_NUMBER: _ClassVar[int] person_id: int - def __init__(self, person_id: _Optional[int] = ...) -> None: ... + def __init__(self, person_id: _Optional[int] = ...) -> None: + """ + Creates a new GetPersonRequest instance. + + Args: + person_id: + The unique identifier of the person to retrieve. + """ class GetPersonResponse(_message.Message): + """ + Response message containing a single person. + """ + __slots__ = ("person",) PERSON_FIELD_NUMBER: _ClassVar[int] person: Person - def __init__(self, person: _Optional[_Union[Person, _Mapping]] = ...) -> None: ... + def __init__(self, person: _Optional[_Union[Person, _Mapping]] = ...) -> None: + """ + Creates a new GetPersonResponse instance. + + Args: + person: + The Person object returned by the service. + """ class ListPeopleRequest(_message.Message): + """ + Request message for listing all people in the address book. + """ + __slots__ = () - def __init__(self) -> None: ... + def __init__(self) -> None: + """ + Creates a new ListPeopleRequest instance. + """ class ListPeopleResponse(_message.Message): + """ + Response message containing a list of people. + """ + __slots__ = ("people",) PEOPLE_FIELD_NUMBER: _ClassVar[int] people: _containers.RepeatedCompositeFieldContainer[Person] def __init__( - self, people: _Optional[_Iterable[_Union[Person, _Mapping]]] = ... - ) -> None: ... + self, + people: _Optional[_Iterable[_Union[Person, _Mapping]]] = ..., + ) -> None: + """ + Creates a new ListPeopleResponse instance. + + Args: + people: + The list of Person objects returned by the service. + """ class AddPersonRequest(_message.Message): + """ + Request message for adding a new person to the address book. + """ + __slots__ = ("person",) PERSON_FIELD_NUMBER: _ClassVar[int] person: Person - def __init__(self, person: _Optional[_Union[Person, _Mapping]] = ...) -> None: ... + def __init__(self, person: _Optional[_Union[Person, _Mapping]] = ...) -> None: + """ + Creates a new AddPersonRequest instance. + + Args: + person: + The Person object to add. + """ class AddPersonResponse(_message.Message): + """ + Response message confirming the addition of a person. + """ + __slots__ = ("person",) PERSON_FIELD_NUMBER: _ClassVar[int] person: Person - def __init__(self, person: _Optional[_Union[Person, _Mapping]] = ...) -> None: ... + def __init__(self, person: _Optional[_Union[Person, _Mapping]] = ...) -> None: + """ + Creates a new AddPersonResponse instance. + + Args: + person: + The Person object that was added. + """ diff --git a/examples/plugins/proto/person_pb2_grpc.py b/examples/plugins/proto/person_pb2_grpc.py index 5325331de..d613482c0 100644 --- a/examples/plugins/proto/person_pb2_grpc.py +++ b/examples/plugins/proto/person_pb2_grpc.py @@ -1,12 +1,20 @@ # ruff: noqa: PGH004 # ruff: noqa -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" +""" +Client and server classes for gRPC services defined in the person.proto file. +This module is generated by the protobuf compiler plugin and demonstrates how to use gRPC in Python. +It is intended for pedagogical purposes, showing how to implement and interact with gRPC services. + +!!! note + + This file is generated and should not be modified manually, except for documentation improvements. +""" + +from typing import Any import grpc -import warnings -from . import person_pb2 as person__pb2 +from examples.plugins.proto import person_pb2 as person__pb2 GRPC_GENERATED_VERSION = "1.73.1" GRPC_VERSION = grpc.__version__ @@ -32,27 +40,34 @@ class AddressBookServiceStub(object): - """The AddressBook service definition""" + """ + Stub client for the AddressBook gRPC service. - def __init__(self, channel): - """Constructor. + This class provides client-side methods for interacting with the AddressBook service. + It is typically instantiated with a gRPC channel and used to make remote procedure calls. + """ + + def __init__(self, channel: grpc.Channel) -> None: + """ + Initializes the AddressBookServiceStub. Args: - channel: A grpc.Channel. + channel: + The gRPC channel through which to make calls. """ - self.GetPerson = channel.unary_unary( + self.GetPerson = channel.unary_unary( # type: ignore[call-arg] "/person.AddressBookService/GetPerson", request_serializer=person__pb2.GetPersonRequest.SerializeToString, response_deserializer=person__pb2.GetPersonResponse.FromString, _registered_method=True, ) - self.ListPeople = channel.unary_unary( + self.ListPeople = channel.unary_unary( # type: ignore[call-arg] "/person.AddressBookService/ListPeople", request_serializer=person__pb2.ListPeopleRequest.SerializeToString, response_deserializer=person__pb2.ListPeopleResponse.FromString, _registered_method=True, ) - self.AddPerson = channel.unary_unary( + self.AddPerson = channel.unary_unary( # type: ignore[call-arg] "/person.AddressBookService/AddPerson", request_serializer=person__pb2.AddPersonRequest.SerializeToString, response_deserializer=person__pb2.AddPersonResponse.FromString, @@ -61,29 +76,98 @@ def __init__(self, channel): class AddressBookServiceServicer(object): - """The AddressBook service definition""" + """ + Server-side implementation base for the AddressBook gRPC service. - def GetPerson(self, request, context): - """Get a person by ID""" + This class should be subclassed to provide concrete implementations of the service methods. + Each method receives a request and a context, and should return the appropriate response. + """ + + def GetPerson( + self, + request: person__pb2.GetPersonRequest, + context: grpc.ServicerContext, + ) -> person__pb2.GetPersonResponse: + """ + Gets a person by their unique ID. + + Args: + request: + The request message containing the person's ID. + + context: + The context for the RPC call. + + Returns: + The response containing the person's details. + + Raises: + If the method is not implemented. + """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") - def ListPeople(self, request, context): - """List all people in the address book""" + def ListPeople( + self, + request: person__pb2.ListPeopleRequest, + context: grpc.ServicerContext, + ) -> person__pb2.ListPeopleResponse: + """ + Lists all people in the address book. + + Args: + request: + The request message (typically empty). + + context: + The context for the RPC call. + + Returns: + The response containing a list of people. + """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") - def AddPerson(self, request, context): - """Add a new person to the address book""" + def AddPerson( + self, + request: person__pb2.AddPersonRequest, + context: grpc.ServicerContext, + ) -> person__pb2.AddPersonResponse: + """ + Adds a new person to the address book. + + Args: + request: + The request message containing the new person's details. + + context: + The context for the RPC call. + + Returns: + The response confirming the addition. + """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") -def add_AddressBookServiceServicer_to_server(servicer, server): - rpc_method_handlers = { +def add_AddressBookServiceServicer_to_server( + servicer: AddressBookServiceServicer, + server: grpc.Server, +) -> None: + """ + Registers the AddressBookServiceServicer with a gRPC server. + + Args: + servicer: + The service implementation to add to the server. + + server: + The gRPC server to which the service will be added. + """ + rpc_method_handlers: dict[str, Any] = { "GetPerson": grpc.unary_unary_rpc_method_handler( servicer.GetPerson, request_deserializer=person__pb2.GetPersonRequest.FromString, @@ -101,32 +185,75 @@ def add_AddressBookServiceServicer_to_server(servicer, server): ), } generic_handler = grpc.method_handlers_generic_handler( - "person.AddressBookService", rpc_method_handlers + "person.AddressBookService", + rpc_method_handlers, ) server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers( - "person.AddressBookService", rpc_method_handlers + server.add_registered_method_handlers( # type: ignore[attr-defined] + "person.AddressBookService", + rpc_method_handlers, ) # This class is part of an EXPERIMENTAL API. class AddressBookService(object): - """The AddressBook service definition""" + """ + EXPERIMENTAL: Client for the AddressBook gRPC service using the experimental API. + + This class provides static methods for making calls to the AddressBook service using the experimental gRPC API. + """ @staticmethod def GetPerson( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( + request: person__pb2.GetPersonRequest, + target: str, + options: tuple[Any, ...] = tuple(), + channel_credentials: grpc.ChannelCredentials | None = None, + call_credentials: grpc.CallCredentials | None = None, + insecure: bool = False, + compression: grpc.Compression | None = None, + wait_for_ready: bool | None = None, + timeout: float | None = None, + metadata: dict[str, Any] | None = None, + ) -> person__pb2.GetPersonResponse: + """ + Makes an experimental unary call to GetPerson. + + Args: + request: + The request message containing the person's ID. + + target: + The server address. + + options: + Call options. + + channel_credentials: + Channel credentials. + + call_credentials: + Call credentials. + + insecure: + Whether to use an insecure channel. + + compression: + Compression option. + + wait_for_ready: + Wait for ready option. + + timeout: + Timeout for the call. + + metadata: + Metadata for the call. + + Returns: + The response containing the person's details. + """ + return grpc.experimental.unary_unary( # type: ignore[attr-defined] request, target, "/person.AddressBookService/GetPerson", @@ -145,18 +272,56 @@ def GetPerson( @staticmethod def ListPeople( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( + request: person__pb2.ListPeopleRequest, + target: str, + options: tuple[Any, ...] = tuple(), + channel_credentials: grpc.ChannelCredentials | None = None, + call_credentials: grpc.CallCredentials | None = None, + insecure: bool = False, + compression: grpc.Compression | None = None, + wait_for_ready: bool | None = None, + timeout: float | None = None, + metadata: dict[str, Any] | None = None, + ) -> person__pb2.ListPeopleResponse: + """ + Makes an experimental unary call to ListPeople. + + Args: + request: + The request message (typically empty). + + target: + The server address. + + options: + Call options. + + channel_credentials: + Channel credentials. + + call_credentials: + Call credentials. + + insecure: + Whether to use an insecure channel. + + compression: + Compression option. + + wait_for_ready: + Wait for ready option. + + timeout: + Timeout for the call. + + metadata: + Metadata for the call. + + + Returns: + The response containing a list of people. + """ + return grpc.experimental.unary_unary( # type: ignore[attr-defined] request, target, "/person.AddressBookService/ListPeople", @@ -175,18 +340,56 @@ def ListPeople( @staticmethod def AddPerson( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( + request: person__pb2.AddPersonRequest, + target: str, + options: tuple[Any, ...] = tuple(), + channel_credentials: grpc.ChannelCredentials | None = None, + call_credentials: grpc.CallCredentials | None = None, + insecure: bool = False, + compression: grpc.Compression | None = None, + wait_for_ready: bool | None = None, + timeout: float | None = None, + metadata: dict[str, Any] | None = None, + ) -> person__pb2.AddPersonResponse: + """ + Makes an experimental unary call to AddPerson. + + Args: + request: + The request message containing the new person's details. + + target: + The server address. + + options: + Call options. + + channel_credentials: + Channel credentials. + + call_credentials: + Call credentials. + + insecure: + Whether to use an insecure channel. + + compression: + Compression option. + + wait_for_ready: + Wait for ready option. + + timeout: + Timeout for the call. + + metadata: + Metadata for the call. + + + Returns: + The response confirming the addition. + """ + return grpc.experimental.unary_unary( # type: ignore[attr-defined] request, target, "/person.AddressBookService/AddPerson", diff --git a/examples/plugins/protobuf/__init__.py b/examples/plugins/protobuf/__init__.py index 13dbfbdbb..f256825a4 100644 --- a/examples/plugins/protobuf/__init__.py +++ b/examples/plugins/protobuf/__init__.py @@ -7,7 +7,7 @@ (protobuf) message serialization. For detailed information about Protocol Buffers, the generated files, and the -domain model used in this example, see the [`proto`][examples.v3.plugins.proto] +domain model used in this example, see the [`proto`][examples.plugins.proto] module documentation. ## Pact and the Plugin Ecosystem @@ -34,7 +34,7 @@ have a basic understanding of Pact and Protocol Buffers. """ -from ..proto.person_pb2 import AddressBook, Person +from examples.plugins.proto.person_pb2 import AddressBook, Person def address_book() -> AddressBook: diff --git a/examples/plugins/protobuf/test_consumer.py b/examples/plugins/protobuf/test_consumer.py index 681eadfcd..29c1eabfc 100644 --- a/examples/plugins/protobuf/test_consumer.py +++ b/examples/plugins/protobuf/test_consumer.py @@ -19,11 +19,10 @@ import pytest import requests +from examples.plugins.proto.person_pb2 import Person +from examples.plugins.protobuf import address_book from pact import Pact -from ..proto.person_pb2 import Person -from . import address_book - if TYPE_CHECKING: from collections.abc import Generator from pathlib import Path diff --git a/examples/plugins/protobuf/test_provider.py b/examples/plugins/protobuf/test_provider.py index e0bc5d5ee..f42e49faa 100644 --- a/examples/plugins/protobuf/test_provider.py +++ b/examples/plugins/protobuf/test_provider.py @@ -30,11 +30,10 @@ from fastapi.responses import Response from yarl import URL +from examples.plugins.proto.person_pb2 import AddressBook +from examples.plugins.protobuf import address_book from pact import Verifier -from ..proto.person_pb2 import AddressBook -from . import address_book - if TYPE_CHECKING: from collections.abc import Generator from pathlib import Path From d9ced725941c2530616e2a5be071d88f5bc85801 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 11 Aug 2025 14:14:01 +1000 Subject: [PATCH 0937/1376] chore(ci): cancel ci on PRs Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c1a2b049..fe8c76962 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref || github.run_id }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: contents: read From 3dde89d008e4b3c3184dab5e5e1afa81cb79c7a1 Mon Sep 17 00:00:00 2001 From: Kevin Rohan Vaz Date: Fri, 8 Aug 2025 11:36:58 +0530 Subject: [PATCH 0938/1376] fix(verifier): propagate branch In the vast majority of cases, the branch in the broker selector and in the publish options will be the same. If either option is set, the default gets propagated to the other. BREAKING CHANGE: If a branch is set through either `set_publish_options` or `provider_branch`, the value will be saved and used as a default for both in subsequent calls. Co-authored-by: JP-Ellis Co-authored-by: Kevin Rohan Vaz Signed-off-by: JP-Ellis --- src/pact/verifier.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/pact/verifier.py b/src/pact/verifier.py index 9a652f928..7fa57b705 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -166,6 +166,7 @@ def __init__(self, name: str, host: str | None = None) -> None: self._name = name self._host = host or "localhost" self._handle = pact_ffi.verifier_new_for_application() + self._branch: str | None = None # In order to provide a fluent interface, we remember some options which # are set using the same FFI method. In particular, we remember @@ -864,13 +865,19 @@ def set_publish_options( branch: Name of the branch used for verification. + + The first time a branch is set here or through + [`provider_branch`][pact.verifier.BrokerSelectorBuilder.provider_branch], + the value will be saved and use as a default for both. """ + if not self._branch and branch: + self._branch = branch pact_ffi.verifier_set_publish_options( self._handle, version, url, tags or [], - branch, + branch or self._branch, ) return self @@ -1375,8 +1382,14 @@ def provider_tags(self, *tags: str) -> Self: def provider_branch(self, branch: str) -> Self: """ Set the provider branch. + + The first time a branch is set here or through + [`set_publish_options`][pact.verifier.Verifier.set_publish_options], the + value will be saved and use as a default for both. """ self._provider_branch = branch + if not self._verifier._branch: # type: ignore # noqa: PGH003, SLF001 + self._verifier._branch = branch # type: ignore # noqa: PGH003, SLF001 return self def consumer_versions(self, *versions: str) -> Self: @@ -1410,7 +1423,7 @@ def build(self) -> Verifier: self._include_pending, self._include_wip_since, self._provider_tags or [], - self._provider_branch, + self._provider_branch or self._verifier._branch, # noqa: SLF001 self._consumer_versions or [], self._consumer_tags or [], ) From cc42d5eb09f764a3e9978ff1be74cfe195f607b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:43:24 +0000 Subject: [PATCH 0939/1376] chore(deps): update actions/checkout action to v5 (#1169) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 6 +++--- .github/workflows/build-ffi.yml | 6 +++--- .github/workflows/build.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index b957e3d57..7105b967d 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -49,7 +49,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -93,7 +93,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -135,7 +135,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 451b816c4..62795d18d 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -49,7 +49,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -93,7 +93,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -136,7 +136,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f347e555..bbdff97f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -104,7 +104,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8e72a1572..cc2b5c255 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index fb9bda257..bfa0ec8af 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Synchronize labels uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe8c76962..e42874ef8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 submodules: true @@ -145,7 +145,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -189,7 +189,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -221,7 +221,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -254,7 +254,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -283,7 +283,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -300,7 +300,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 From a358d5b9e35235bc374a54f18e5c74e79e284742 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 12 Aug 2025 10:13:34 +1000 Subject: [PATCH 0940/1376] chore: add vscode settings and extensions Signed-off-by: JP-Ellis --- .vscode/extensions.json | 17 +++++++++++++++++ .vscode/settings.json | 13 +++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..59194417f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,17 @@ +{ + "recommendations": [ + "bierner.github-markdown-preview", + "biomejs.biome", + "charliermarsh.ruff", + "davidanson.vscode-markdownlint", + "editorconfig.editorconfig", + "github.vscode-github-actions", + "github.vscode-pull-request-github", + "ms-python.debugpy", + "ms-python.mypy-type-checker", + "ms-python.python", + "redhat.vscode-yaml", + "tamasfe.even-better-toml", + "yzhang.markdown-all-in-one" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..bb00d41ff --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "python.testing.pytestArgs": [ + "--no-cov", + "--ignore=examples/v2", + "--ignore=tests/v2", + "examples/", + "pact-python-cli/tests/", + "pact-python-ffi/tests/", + "tests/" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} From 6665663ce8551f847efbec86204bb4bd87bc5ba2 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 12 Aug 2025 09:45:55 +1000 Subject: [PATCH 0941/1376] chore: add envrc Signed-off-by: JP-Ellis --- .envrc | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .envrc diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..86867d10b --- /dev/null +++ b/.envrc @@ -0,0 +1,11 @@ +# shellcheck shell=bash + +# If `.env` exists, load it +# This is useful to store secrets and making them available to the shell +# without risking exposing them. +dotenv_if_exists + +# For this to work, you will need to add: +# https://github.com/JP-Ellis/dotfiles/blob/c6b01b8cd633189920ded05b1201e4c32e9e597f/direnv/direnvrc#L26-L39 +# to your `direnvrc` file (usually located at `~/.config/direnv/direnvrc`) +layout hatch From fd37b80d3e89d388ee669594fe34febff4d49548 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 12 Aug 2025 11:01:34 +1000 Subject: [PATCH 0942/1376] chore(deps): switch to dependency groups The `[project.optional-dependencies]` header is used for _public_ dependency groups, but it was also a convenient way to keep track of dev dependencies. These were prefixed with `devel-` to make clear their purpose. Dependency groups are a recent addition which allows for dev dependencies to be specified without making it public in the same way that the optional dependencies were. Signed-off-by: JP-Ellis --- pact-python-cli/pyproject.toml | 22 ++--- pact-python-ffi/pyproject.toml | 24 ++--- pyproject.toml | 159 +++++++++++++++++---------------- 3 files changed, 99 insertions(+), 106 deletions(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 87e91f81d..f5a68cc74 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -48,16 +48,12 @@ requires-python = ">=3.9" pact-stub-service = "pact_cli:_exec" pactflow = "pact_cli:_exec" - [project.optional-dependencies] - # Linting and formatting tools use a more narrow specification to ensure - # developper consistency. All other dependencies are as above. - devel = [ - "pact-python-cli[devel-test]", - "pact-python-cli[devel-types]", - "ruff==0.12.8", - ] - devel-test = ["pytest-cov~=6.0", "pytest~=8.0"] - devel-types = ["mypy==1.17.1"] +[dependency-groups] +# Linting and formatting tools use a more narrow specification to ensure +# developper consistency. All other dependencies are as above. +dev = ["ruff==0.12.8", { include-group = "test" }, { include-group = "types" }] +test = ["pytest-cov~=6.0", "pytest~=8.0"] +types = ["mypy==1.17.1"] ################################################################################ ## Build System @@ -111,8 +107,8 @@ requires = ["hatch-vcs", "hatchling", "packaging"] "requests", "setuptools ; python_version >= '3.12'", ] - features = ["devel"] installer = "uv" + pre-install-commands = ["uv pip install --group dev -e ."] [tool.hatch.envs.default.scripts] all = ["format", "lint", "test", "typecheck"] @@ -126,8 +122,8 @@ requires = ["hatch-vcs", "hatchling", "packaging"] # Test environment for running unit tests. This automatically tests against all # supported Python versions. [tool.hatch.envs.test] - features = ["devel-test"] - installer = "uv" + installer = "uv" + pre-install-commands = ["uv pip install --group test -e ."] [[tool.hatch.envs.test.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.9"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index c77db1d4f..f553a5372 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,16 +41,10 @@ dependencies = ["cffi~=1.0"] "Homepage" = "https://pact.io" "Repository" = "https://github.com/pact-foundation/pact-python" - [project.optional-dependencies] - # Linting and formatting tools use a more narrow specification to ensure - # developper consistency. All other dependencies are as above. - devel = [ - "pact-python-ffi[devel-test]", - "pact-python-ffi[devel-types]", - "ruff==0.12.8", - ] - devel-test = ["pytest-cov~=6.0", "pytest~=8.0"] - devel-types = ["mypy==1.17.1", "typing-extensions~=4.0"] +[dependency-groups] +dev = ["ruff==0.12.8", { include-group = "test" }, { include-group = "types" }] +test = ["pytest-cov~=6.0", "pytest~=8.0"] +types = ["mypy==1.17.1", "typing-extensions~=4.0"] ################################################################################ ## Build System @@ -98,9 +92,9 @@ requires = ["hatch-vcs", "hatchling", "packaging", "cffi"] # Install dev dependencies in the default environment to simplify the developer # workflow. [tool.hatch.envs.default] - extra-dependencies = ["hatch-vcs", "hatchling", "packaging", "cffi"] - features = ["devel"] - installer = "uv" + extra-dependencies = ["hatch-vcs", "hatchling", "packaging", "cffi"] + installer = "uv" + pre-install-commands = ["uv pip install --group dev -e ."] # Update paths to ensure the shared library can be found # TODO: See if this can be overridden on a per-platform basis @@ -120,8 +114,8 @@ requires = ["hatch-vcs", "hatchling", "packaging", "cffi"] # Test environment for running unit tests. This automatically tests against all # supported Python versions. [tool.hatch.envs.test] - features = ["devel-test"] - installer = "uv" + installer = "uv" + pre-install-commands = ["uv pip install --group test -e ."] # Update paths to ensure the shared library can be found # TODO: See if this can be overridden on a per-platform basis diff --git a/pyproject.toml b/pyproject.toml index 094939d20..9f7ff965e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,75 +70,76 @@ dependencies = [ "six~=1.0", ] - # Linting and formatting tools use a more narrow specification to ensure - # developper consistency. All other dependencies are as above. - devel = [ - "pact-python-cli[devel]", - "pact-python-ffi[devel]", - "pact-python[devel-docs]", - "pact-python[devel-example]", - "pact-python[devel-test]", - "pact-python[devel-types]", - "ruff==0.12.8", - ] - devel-docs = [ - "mkdocs-github-admonitions-plugin~=0.0", - "mkdocs-literate-nav~=0.6", - "mkdocs-material[imaging]~=9.4", - "mkdocs-section-index~=0.3", - "mkdocs_gen_files~=0.5", - "mkdocstrings[python]~=0.23", - "mkdocs~=1.5", - "pathspec~=0.0", - ] - devel-example = [ - "fastapi~=0.0", - "flask[async]~=3.0", - "grpcio~=1.0", - "pact-python[devel-test]", - "protobuf~=6.0", - "pydantic~=2.0", - "pytest-cov~=6.0", - "pytest~=8.0", - "testcontainers~=4.0", - "uvicorn[standard]~=0.0", - ] - devel-test = [ - "aiohttp~=3.0", - "flask~=3.0", - "pact-python-cli", - "pytest-asyncio~=1.0", - "pytest-bdd~=8.0", - "pytest-cov~=6.0", - "pytest~=8.0", - "requests~=2.0", - "testcontainers~=4.0", - ] - devel-types = [ - "mypy==1.17.1", - "types-cffi~=1.0", - "types-grpcio~=1.0", - "types-protobuf~=6.0", - "types-requests~=2.0", - ] +[dependency-groups] +# Linting and formatting tools use a more narrow specification to ensure +# developper consistency. All other dependencies are as above. +dev = [ + "ruff==0.12.8", + { include-group = "docs" }, + { include-group = "example" }, + { include-group = "test" }, + { include-group = "types" }, + { include-group = "example-v2" }, + { include-group = "test-v2" }, +] - # Dependencies for v2 example and test environments - devel-example-v2 = [ - "fastapi~=0.0", - "flask[async]~=3.0", - "pytest-cov~=6.0", - "pytest~=8.0", - "testcontainers~=4.0", - "uvicorn[standard]~=0.0", - ] - devel-test-v2 = [ - "fastapi~=0.0", - "httpx~=0.0", - "mock~=5.0", - "pytest-cov~=6.0", - "pytest~=8.0", - "uvicorn[standard]~=0.0", - ] +docs = [ + "mkdocs-github-admonitions-plugin~=0.0", + "mkdocs-literate-nav~=0.6", + "mkdocs-material[recommended,git,imaging]~=9.0", + "mkdocs-section-index~=0.3", + "mkdocs_gen_files~=0.5", + "mkdocstrings[python]~=0.23", + "mkdocs~=1.5", + "pathspec~=0.0", +] +example = [ + "fastapi~=0.0", + "flask[async]~=3.0", + "grpcio~=1.0", + "protobuf~=6.0", + "pydantic~=2.0", + "pytest-cov~=6.0", + "pytest~=8.0", + "testcontainers~=4.0", + "uvicorn[standard]~=0.0", +] +test = [ + "aiohttp~=3.0", + "flask~=3.0", + "pact-python-cli", + "pytest-asyncio~=1.0", + "pytest-bdd~=8.0", + "pytest-cov~=6.0", + "pytest~=8.0", + "requests~=2.0", + "testcontainers~=4.0", +] +types = [ + "mypy==1.17.1", + "types-cffi~=1.0", + "types-grpcio~=1.0", + "types-protobuf~=6.0", + "types-requests~=2.0", +] + +# Dependencies for v2 example and test environments +example-v2 = [ + "fastapi~=0.0", + "flask[async]~=3.0", + "pytest-cov~=6.0", + "pytest~=8.0", + "testcontainers~=4.0", + "uvicorn[standard]~=0.0", +] +test-v2 = [ + "fastapi~=0.0", + "httpx~=0.0", + "mock~=5.0", + "pytest-cov~=6.0", + "pytest~=8.0", + "uvicorn[standard]~=0.0", +] [build-system] build-backend = "hatchling.build" @@ -184,11 +185,15 @@ requires = ["hatch-vcs", "hatchling"] # workflow. [tool.hatch.envs.default] extra-dependencies = ["hatchling", "hatch-vcs"] - features = ["devel"] installer = "uv" # This is require to get around an incompatibility between hatch and uv # See: https://github.com/pypa/hatch/issues/1639 - pre-install-commands = ["uv pip install -e .[devel]"] + # See: https://github.com/pypa/hatch/issues/1852 + pre-install-commands = [ + "uv pip install --group dev -e .", + "uv pip install --group dev -e ./pact-python-ffi", + "uv pip install --group dev -e ./pact-python-cli", + ] # # Update paths to ensure the shared library can be found # TODO: See if this can be overridden on a per-platform basis @@ -211,9 +216,8 @@ requires = ["hatch-vcs", "hatchling"] # Test environment for running unit tests. [tool.hatch.envs.test] - features = ["devel-test"] installer = "uv" - pre-install-commands = ["uv pip install -e ."] + pre-install-commands = ["uv pip install --group test -e ."] [[tool.hatch.envs.test.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.9"] @@ -221,9 +225,8 @@ requires = ["hatch-vcs", "hatchling"] # Test environment for running unit tests. This automatically tests against all # supported Python versions. [tool.hatch.envs.example] - features = ["devel-example"] installer = "uv" - pre-install-commands = ["uv pip install -e ."] + pre-install-commands = ["uv pip install --group example -e ."] [tool.hatch.envs.example.scripts] all = ["example"] @@ -232,9 +235,9 @@ requires = ["hatch-vcs", "hatchling"] python = ["3.10", "3.11", "3.12", "3.13", "3.9"] [tool.hatch.envs.v2-test] - features = ["v2", "devel-test-v2"] + features = ["v2"] installer = "uv" - pre-install-commands = ["uv pip install -e ."] + pre-install-commands = ["uv pip install --group test-v2 -e ."] [tool.hatch.envs.v2-test.scripts] all = ["test"] @@ -244,9 +247,9 @@ requires = ["hatch-vcs", "hatchling"] python = ["3.10", "3.11", "3.12", "3.13", "3.9"] [tool.hatch.envs.v2-example] - features = ["v2", "devel-example-v2"] + features = ["v2"] installer = "uv" - pre-install-commands = ["uv pip install -e ."] + pre-install-commands = ["uv pip install --group example-v2 -e ."] [tool.hatch.envs.v2-example.scripts] all = ["example"] From 9fb1b092bdfee50d21dda97e37d587f0a97895b8 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 12 Aug 2025 11:55:45 +1000 Subject: [PATCH 0943/1376] chore: replace yamlfix with yamlfmt Yamlfix has an issue with parsing the root `pyproject.toml` file. Signed-off-by: JP-Ellis --- .github/workflows/build-cli.yml | 3 +-- .github/workflows/build-ffi.yml | 3 +-- .github/workflows/build.yml | 3 +-- .github/workflows/test.yml | 8 ++++---- .pre-commit-config.yaml | 6 +++--- .yamlfmt.yml | 13 +++++++++++++ pyproject.toml | 9 --------- 7 files changed, 23 insertions(+), 22 deletions(-) create mode 100644 .yamlfmt.yml diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 7105b967d..55c9781da 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -116,8 +116,7 @@ jobs: name: Publish CLI wheels and sdist if: >- - github.event_name == 'push' && - startsWith(github.event.ref, 'refs/tags/pact-python-cli/') + github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/pact-python-cli/') runs-on: ubuntu-latest environment: name: pypi diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 62795d18d..20bc6c01d 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -117,8 +117,7 @@ jobs: name: Publish FFI wheels and sdist if: >- - github.event_name == 'push' && - startsWith(github.event.ref, 'refs/tags/pact-python-ffi/') + github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/pact-python-ffi/') runs-on: ubuntu-latest environment: name: pypi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bbdff97f3..0916e1201 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -86,8 +86,7 @@ jobs: name: Publish wheels and sdist if: >- - github.event_name == 'push' && - startsWith(github.event.ref, 'refs/tags/pact-python/') + github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/pact-python/') runs-on: ubuntu-latest environment: name: pypi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e42874ef8..f04b950a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,8 +48,8 @@ jobs: test: name: >- - Test Python ${{ matrix.python-version }} - on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} + Test Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, + 'windows-') && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} @@ -117,8 +117,8 @@ jobs: example: name: >- - Test Python Example ${{ matrix.python-version }} - on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} + Test Python Example ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' + || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index df9fa6c4c..8663fc51b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,10 +22,10 @@ repos: - id: mixed-line-ending - id: trailing-whitespace - - repo: https://github.com/lyz-code/yamlfix/ - rev: 1.17.0 + - repo: https://github.com/google/yamlfmt + rev: v0.17.2 hooks: - - id: yamlfix + - id: yamlfmt - repo: https://gitlab.com/bmares/check-json5 rev: v1.0.0 diff --git a/.yamlfmt.yml b/.yamlfmt.yml new file mode 100644 index 000000000..02062fbbd --- /dev/null +++ b/.yamlfmt.yml @@ -0,0 +1,13 @@ +--- +line_ending: lf + +formatter: + include_document_start: true + line_ending: lf + retain_line_breaks_single: true + max_line_length: 100 + drop_merge_tag: true + pad_line_comments: 2 + trim_trailing_whitespace: true + eof_newline: true + force_array_style: block diff --git a/pyproject.toml b/pyproject.toml index 9f7ff965e..49df4f8db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -382,15 +382,6 @@ exclude = """(?x)^( [tool.typos.default] extend-ignore-re = ['(?Rm)^.*(#|//| +## [pact-python/3.0.0a1] _2025-08-12_ + +### 🚀 Features + +- Create pact-python-cli package +- _(cli)_ Build abi-agnostic wheels +- _(ffi)_ Add standalone ffi package +- _(v3)_ [**breaking**] Remove pact.v3.ffi module + > `pact.v3.ffi` is removed, and to be replaced by `pact_ffi`. That is, `pact.v3.ffi.$fn` should be replaced by `pact_ffi.$fn`. +- [**breaking**] Prepare for v3 release + > This prepares for version 3. Pact Python v2 will be still accessible under `pact.v2` and all imports should be appropriate renamed. Everyone is encouraged to migrate to Pact Python v3. +- [**breaking**] Simplify `given` + > The signature of `Interaction.given` has been updated. The following changes are required: - Change `given("state", key="user_id", value=123)` to `given("state", user_id=123)`. This can take an arbitrary number of keyword arguments. If the key is not a valid Python keyword argument, use the dictionary input below. - Change `given("state", parameters={"user_id": 123})` to `given("state", {"user_id": 123})`. +- [**breaking**] Deserialize metadata values + > As the metadata values are now deserialised, the type of the metadata values may change. For example, setting metadata `user_id=123` will now pass `{"user_id": 123}` through to the function handler. Previously, this would have been `{"user_id": "123"}`. + +### 🐛 Bug Fixes + +- Matcher type variance +- With metadata function signature +- [**breaking**] Use correct datetime default format + > If you relied on the previous default (undocumented) behaviour, prefer specifying the format explicitly now: `match.datetime(regex="%Y-%m-%dT%H:%M:%S.%f%z")`. +- Handle empty state callback +- _(verifier)_ [**breaking**] Propagate branch + > If a branch is set through either `set_publish_options` or `provider_branch`, the value will be saved and used as a default for both in subsequent calls. + +### 🚜 Refactor + +- Functional state handler + +### 📚 Documentation + +- Update changelog for v2.3.3 +- _(blog)_ Fix v3 references +- Fix v3 references +- V3 review +- Update git cliff configuration + +### ⚙️ Miscellaneous Tasks + +- Update pre-commit hooks +- Use the new `pact_cli` package +- Remove packaging of pact cli +- _(ci)_ Incorporate tests of pact cli +- _(ci)_ Use new `pact-python/*` tags +- _(ci)_ Add build cli pipeline +- Exclude hatch_build from mypy checks +- _(ci)_ Narrow token permissions +- Remove macosx deployment target +- _(ci)_ Fix cli publish permissions +- Properly extract tag version +- Update gitignore +- _(ci)_ Fix core package build +- Split out dependencies and tests +- _(ci)_ Update labels +- _(ci)_ Fix labels +- _(tests)_ Re-organise tests +- Fix bad copy-paste in tests +- Log exceptions from apply_args +- Improve logging from apply_args +- _(examples)_ Start examples overhaul +- _(ci)_ Use new examples +- Update protobuf examples +- _(ci)_ Cancel ci on PRs +- Add vscode settings and extensions +- Add envrc +- Replace yamlfix with yamlfmt +- Remove deptry config +- Support pre and post release tags +- Fix typo + +### Contributors + +- @JP-Ellis +- @kevinrvaz + ## [2.3.3] _2025-07-17_ ### 🚀 Features @@ -1441,4 +1517,4 @@ All notable changes to this project will be documented in this file. - @matthewbalvanz-wf - @mefellows - + From 7009833eaba55b49acfc4ccf5345b66db4a468bb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 22:05:21 +0000 Subject: [PATCH 0949/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.35.4 (#1177) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f04b950a5..8b8e06635 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -288,7 +288,7 @@ jobs: fetch-depth: 0 - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@52bd719c2c91f9d676e2aa359fc8e0db8925e6d8 # v1.35.3 + uses: crate-ci/typos@a67079b4ae32e18c3f53d75368c52ce53b5fb56b # v1.35.4 pre-commit: name: Pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8663fc51b..0cc34347a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,7 +78,7 @@ repos: ) - repo: https://github.com/crate-ci/typos - rev: v1.35.3 + rev: v1.35.4 hooks: - id: typos From 3c592f6960e930154e7f45e1ab6e1ca595b8fef2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 10:37:59 +1000 Subject: [PATCH 0950/1376] chore(deps): update astral-sh/setup-uv action to v6.5.0 (#1178) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 55c9781da..395926469 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 20bc6c01d..263d4cc78 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0916e1201..8372644c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cc2b5c255..137bb2243 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b8e06635..f4757ff9f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -82,7 +82,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 with: enable-cache: true @@ -150,7 +150,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 with: enable-cache: true @@ -194,7 +194,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 with: enable-cache: true @@ -226,7 +226,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 with: enable-cache: true @@ -259,7 +259,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 with: enable-cache: true @@ -312,7 +312,7 @@ jobs: key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Set up uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 with: enable-cache: true cache-suffix: pre-commit From 7b666c154d1ef18ec8022794b4fb467ae90bbee2 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 14 Aug 2025 08:46:25 +1000 Subject: [PATCH 0951/1376] chore(ci): remove spelling check The spell checks are already covered through pre-commit. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f4757ff9f..a2cf3482e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,6 @@ jobs: - format - lint - typecheck - - spelling - pre-commit steps: @@ -276,20 +275,6 @@ jobs: working-directory: pact-python-cli run: hatch run typecheck - spelling: - name: Spell check - - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 0 - - - name: Spell Check Repo https://github.com/crate-ci/typos/commit/ - uses: crate-ci/typos@a67079b4ae32e18c3f53d75368c52ce53b5fb56b # v1.35.4 - pre-commit: name: Pre-commit From abb08731080f1cf88810790b50851a21407268f2 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 14 Aug 2025 08:43:35 +1000 Subject: [PATCH 0952/1376] docs: add mascot Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 6 +- README.md | 5 +- mascot.svg | 409 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 418 insertions(+), 2 deletions(-) create mode 100644 mascot.svg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0cc34347a..fff19ee03 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -75,12 +75,16 @@ repos: (?x)^( .github/PULL_REQUEST_TEMPLATE\.md | CHANGELOG.md - ) + )$ - repo: https://github.com/crate-ci/typos rev: v1.35.4 hooks: - id: typos + exclude: | + (?x)^( + mascot\.svg + )$ - repo: local hooks: diff --git a/README.md b/README.md index 921f0aa19..21ea381cf 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,10 @@
- Fast, easy and reliable testing for your APIs and microservices. + Pact Python Mascot + + Fast, easy and reliable testing for your APIs and microservices. +
diff --git a/mascot.svg b/mascot.svg new file mode 100644 index 000000000..f39ed0369 --- /dev/null +++ b/mascot.svg @@ -0,0 +1,409 @@ + + + + + + + + + + + + From a38889eb6011eca473b0c3693784f36ef6a2f2e5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 14 Aug 2025 09:53:23 +1000 Subject: [PATCH 0953/1376] docs: give mascot outline To help on dark background/dark mode. Signed-off-by: JP-Ellis --- mascot.svg | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mascot.svg b/mascot.svg index f39ed0369..de7717ad0 100644 --- a/mascot.svg +++ b/mascot.svg @@ -156,7 +156,7 @@ style="stop-color:#000000;stop-opacity:1" id="stop2556" /> Date: Fri, 15 Aug 2025 13:12:52 +1000 Subject: [PATCH 0954/1376] docs: set mascot width and height Without both of these, either GitHub's rendering or MkDocs' sizing of the mascot is incorrect. Signed-off-by: JP-Ellis --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 21ea381cf..1284d8e66 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,10 @@
- Pact Python Mascot + Pact Python Mascot Fast, easy and reliable testing for your APIs and microservices. From 36f962fc7e135f450e4504783ceb151f1e3bf5a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Aug 2025 06:02:36 +0000 Subject: [PATCH 0955/1376] chore(deps): update ruff to v0.12.9 (#1187) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fff19ee03..325651442 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.8 + rev: v0.12.9 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index f875e6f7d..6ae2ad4e0 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.9" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.12.8", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.12.9", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=6.0", "pytest~=8.0"] types = ["mypy==1.17.1"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 8b5dfbaa6..204112d7c 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -42,7 +42,7 @@ dependencies = ["cffi~=1.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.12.8", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.12.9", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=6.0", "pytest~=8.0"] types = ["mypy==1.17.1", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 6f35ab181..3bc96fcd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.12.8", + "ruff==0.12.9", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From 9500a0aaedc3216d35307970109b19d7ed206129 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 15 Aug 2025 16:07:50 +1000 Subject: [PATCH 0956/1376] chore(examples): minor improvements Minor improvements to the documentation and the example itself. For example, return a `201 CREATED` in response to a creation event. Signed-off-by: JP-Ellis --- examples/http/aiohttp_and_flask/consumer.py | 40 +++++++++---------- examples/http/aiohttp_and_flask/provider.py | 20 +++------- .../http/aiohttp_and_flask/test_consumer.py | 2 +- .../http/aiohttp_and_flask/test_provider.py | 3 +- 4 files changed, 27 insertions(+), 38 deletions(-) diff --git a/examples/http/aiohttp_and_flask/consumer.py b/examples/http/aiohttp_and_flask/consumer.py index 4f8e6004c..5c50aaefe 100644 --- a/examples/http/aiohttp_and_flask/consumer.py +++ b/examples/http/aiohttp_and_flask/consumer.py @@ -1,12 +1,12 @@ """ -Simple Consumer Implementation. +Aiohttp consumer example. This modules defines a simple [consumer](https://docs.pact.io/getting_started/terminology#service-consumer) -which will be tested with Pact in the [consumer -test][examples.http.aiohttp_and_flask.test_consumer]. As Pact is a -consumer-driven framework, the consumer defines the interactions which the -provider must then satisfy. +using the asynchronous [`aiohttp`][aiohttp] library which will be tested with +Pact in the [consumer test][examples.http.aiohttp_and_flask.test_consumer]. As +Pact is a consumer-driven framework, the consumer defines the interactions which +the provider must then satisfy. The consumer is the application which makes requests to another service (the provider) and receives a response to process. In this example, we have a simple @@ -72,16 +72,13 @@ def __post_init__(self) -> None: msg = "User must have a name" raise ValueError(msg) - if self.id < 0: + if self.id <= 0: msg = "User ID must be a positive integer" raise ValueError(msg) def __repr__(self) -> str: """ Return a string representation of the user. - - Returns: - The user's ID and name as a string. """ return f"User(id={self.id!r}, name={self.name!r})" @@ -101,14 +98,15 @@ def __init__(self, hostname: str, base_path: str | None = None) -> None: Args: hostname: - The base URL of the provider (must include scheme, e.g., 'http://'). + The base URL of the provider (must include scheme, e.g., + `http://`). base_path: - The base path for the provider's API endpoints. Defaults to '/'. + The base path for the provider's API endpoints. Defaults to `/`. Raises: ValueError: - If the hostname does not start with 'http://' or 'https://'. + If the hostname does not start with `http://` or 'https://'. """ if not hostname.startswith(("http://", "https://")): msg = "Invalid base URI" @@ -118,7 +116,7 @@ def __init__(self, hostname: str, base_path: str | None = None) -> None: if not self._base_path.endswith("/"): self._base_path += "/" - self._client = aiohttp.ClientSession( + self._session = aiohttp.ClientSession( base_url=self._hostname, timeout=aiohttp.ClientTimeout(total=5), ) @@ -153,9 +151,9 @@ def base_url(self) -> str: async def __aenter__(self) -> Self: """ - The client instance itself. + Begin an asynchronous context for the client. """ - await self._client.__aenter__() + await self._session.__aenter__() return self async def __aexit__( @@ -177,14 +175,14 @@ async def __aexit__( exc_tb: The traceback, if any. """ - await self._client.__aexit__(exc_type, exc_val, exc_tb) + await self._session.__aexit__(exc_type, exc_val, exc_tb) async def get_user(self, user_id: int) -> User: """ Fetch a user by ID from the provider. - This method demonstrates how a consumer fetches only the data it needs from - a provider, regardless of what else the provider may return. + This method demonstrates how a consumer fetches only the data it needs + from a provider, regardless of what else the provider may return. Args: user_id: @@ -198,7 +196,7 @@ async def get_user(self, user_id: int) -> User: If the server returns a non-2xx response or the request fails. """ logger.debug("Fetching user %s", user_id) - async with self._client.get(f"{self.base_path}users/{user_id}") as response: + async with self._session.get(f"{self.base_path}users/{user_id}") as response: response.raise_for_status() data: dict[str, Any] = await response.json() @@ -234,7 +232,7 @@ async def create_user( """ logger.debug("Creating user %s", name) async with ( - self._client.post( + self._session.post( f"{self.base_path}users", json={"name": name} ) as response, ): @@ -268,5 +266,5 @@ async def delete_user(self, uid: int | User) -> None: uid = uid.id logger.debug("Deleting user %s", uid) - async with self._client.delete(f"{self.base_path}users/{uid}") as response: + async with self._session.delete(f"{self.base_path}users/{uid}") as response: response.raise_for_status() diff --git a/examples/http/aiohttp_and_flask/provider.py b/examples/http/aiohttp_and_flask/provider.py index 65a9f874d..694f70de4 100644 --- a/examples/http/aiohttp_and_flask/provider.py +++ b/examples/http/aiohttp_and_flask/provider.py @@ -3,8 +3,8 @@ This modules defines a simple [provider](https://docs.pact.io/getting_started/terminology#service-provider) -which will be tested with Pact in the [provider -test][examples.http.aiohttp_and_flask.test_provider]. As Pact is a +implemented with [`flask`][flask] which will be tested with Pact in the +[provider test][examples.http.aiohttp_and_flask.test_provider]. As Pact is a consumer-driven framework, the consumer defines the contract which the provider must then satisfy. @@ -231,9 +231,6 @@ def get_user_by_id(uid: int) -> Response: """ Retrieve a user by their ID. - This endpoint demonstrates how a provider might expose user data to a - consumer. If the user is not found, a 404 error is returned. - Args: uid: The ID of the user to fetch. @@ -253,14 +250,11 @@ def get_user_by_id(uid: int) -> Response: @app.route("/users/", methods=["POST"]) -def create_user() -> Response: +def create_user() -> tuple[Response, int]: """ Create a new user in the system. - This endpoint accepts user data as JSON in the request body and adds a new - user to the fake database. The user ID is automatically assigned. This - example illustrates how a provider might handle resource creation and - validation. + The user ID is automatically assigned. Returns: A JSON response containing the created user data with HTTP 201 status @@ -286,7 +280,7 @@ def create_user() -> Response: admin=user.get("admin", False), ) UserDb.create(new_user) - return jsonify(new_user.to_dict()) + return jsonify(new_user.to_dict()), 201 @app.route("/users/", methods=["DELETE"]) @@ -294,9 +288,7 @@ def delete_user(uid: int) -> tuple[str | Response, int]: """ Delete a user by their ID. - This endpoint removes a user from the fake database. If the user does not - exist, a 404 error is returned. This demonstrates how a provider might - implement resource deletion and error handling. + If the user does not exist, a 404 error is returned. Args: uid: diff --git a/examples/http/aiohttp_and_flask/test_consumer.py b/examples/http/aiohttp_and_flask/test_consumer.py index d02d048e5..aea42c0b5 100644 --- a/examples/http/aiohttp_and_flask/test_consumer.py +++ b/examples/http/aiohttp_and_flask/test_consumer.py @@ -129,7 +129,7 @@ async def test_create_user(pact: Pact) -> None: pact.upon_receiving("A request to create a new user") .with_request("POST", "/users") .with_body(payload, content_type="application/json") - .will_respond_with(200) + .will_respond_with(201) .with_body(response, content_type="application/json") ) diff --git a/examples/http/aiohttp_and_flask/test_provider.py b/examples/http/aiohttp_and_flask/test_provider.py index 7f91e9f15..22ded9c63 100644 --- a/examples/http/aiohttp_and_flask/test_provider.py +++ b/examples/http/aiohttp_and_flask/test_provider.py @@ -37,10 +37,9 @@ ACTION_TYPE: TypeAlias = Literal["setup", "teardown"] logger = logging.getLogger(__name__) -_mock_user_db = None -@pytest.fixture +@pytest.fixture(scope="session") def app_server() -> str: """ Run the Flask server for provider verification. From e689b1902091e54c63027bec805ba5a6126841a4 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 15 Aug 2025 16:44:36 +1000 Subject: [PATCH 0957/1376] docs(examples): add requests and fastapi Add a brand new example featuring the Requests and FastAPI library. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 9 +- examples/README.md | 4 +- examples/http/README.md | 2 +- examples/http/aiohttp_and_flask/README.md | 2 +- examples/http/requests_and_fastapi/README.md | 91 ++++++ .../http/requests_and_fastapi/__init__.py | 1 + .../http/requests_and_fastapi/conftest.py | 35 +++ .../http/requests_and_fastapi/consumer.py | 262 ++++++++++++++++++ .../http/requests_and_fastapi/provider.py | 223 +++++++++++++++ .../http/requests_and_fastapi/pyproject.toml | 28 ++ .../requests_and_fastapi/test_consumer.py | 159 +++++++++++ .../requests_and_fastapi/test_provider.py | 237 ++++++++++++++++ mkdocs.yml | 4 +- 13 files changed, 1047 insertions(+), 10 deletions(-) create mode 100644 examples/http/requests_and_fastapi/README.md create mode 100644 examples/http/requests_and_fastapi/__init__.py create mode 100644 examples/http/requests_and_fastapi/conftest.py create mode 100644 examples/http/requests_and_fastapi/consumer.py create mode 100644 examples/http/requests_and_fastapi/provider.py create mode 100644 examples/http/requests_and_fastapi/pyproject.toml create mode 100644 examples/http/requests_and_fastapi/test_consumer.py create mode 100644 examples/http/requests_and_fastapi/test_provider.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a2cf3482e..027844106 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -162,11 +162,10 @@ jobs: set -o errexit set -o pipefail - find examples -name pyproject.toml -print0 | - while IFS= read -r -d $'\0' file; do - cd "$(dirname "$file")" - uv run --python ${{ matrix.python-version }} --group test pytest - done + while IFS= read -r -d $'\0' file <&3; do + cd "$(dirname "$file")" + uv run --python ${{ matrix.python-version }} --group test pytest + done 3< <(find "$(pwd)/examples" -name pyproject.toml -print0) - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' diff --git a/examples/README.md b/examples/README.md index 6f4223d40..40ccb91dd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,13 +10,13 @@ The code within the examples is intended to be well-documented and you are encou ### HTTP Examples -#### aiohttp and Flask +#### [aiohttp and Flask](./http/aiohttp_and_flask/README.md) - **Location**: `examples/http/aiohttp_and_flask/` - **Consumer**: aiohttp-based HTTP client - **Provider**: Flask-based HTTP server -#### requests and FastAPI +#### [requests and FastAPI](./http/requests_and_fastapi/README.md) - **Location**: `examples/http/requests_and_fastapi/` - **Consumer**: requests-based HTTP client diff --git a/examples/http/README.md b/examples/http/README.md index 913b50af0..4f97723ca 100644 --- a/examples/http/README.md +++ b/examples/http/README.md @@ -5,4 +5,4 @@ This directory contains examples of HTTP-based contract testing with Pact. ## Examples - [`aiohttp_and_flask/`](aiohttp_and_flask/) - Async aiohttp consumer with Flask provider -- [`requests_and_fastapi/`](requests_and_fastapi/) - requests consumer with FastAPI provider (🚧 planned) +- [`requests_and_fastapi/`](requests_and_fastapi/) - requests consumer with FastAPI provider diff --git a/examples/http/aiohttp_and_flask/README.md b/examples/http/aiohttp_and_flask/README.md index 89a137336..0fb792f79 100644 --- a/examples/http/aiohttp_and_flask/README.md +++ b/examples/http/aiohttp_and_flask/README.md @@ -37,7 +37,7 @@ Use the above links to view additional documentation within. ## Prerequisites - Python 3.9 or higher -- A dependency manager (uv recommended, pip also works) +- A dependency manager ([uv](https://docs.astral.sh/uv/) recommended, [pip](https://pip.pypa.io/en/stable/) also works) ## Running the Example diff --git a/examples/http/requests_and_fastapi/README.md b/examples/http/requests_and_fastapi/README.md new file mode 100644 index 000000000..547ae14f3 --- /dev/null +++ b/examples/http/requests_and_fastapi/README.md @@ -0,0 +1,91 @@ +# Example: requests Client and FastAPI Provider with Pact Contract Testing + +This example demonstrates contract testing between a synchronous [`requests`](https://docs.python-requests.org/en/latest/)-based client (consumer) and a [FastAPI](https://fastapi.tiangolo.com/) web server (provider). It is designed to be pedagogical, showing modern Python patterns, type hints, and best practices for contract-driven development. + +## Overview + +- [**Consumer**][examples.http.requests_and_fastapi.consumer]: Synchronous HTTP client using requests +- [**Provider**][examples.http.requests_and_fastapi.provider]: FastAPI web server +- [**Consumer Tests**][examples.http.requests_and_fastapi.test_consumer]: Contract definition and consumer testing with Pact +- [**Provider Tests**][examples.http.requests_and_fastapi.test_provider]: Provider verification against contracts + +Use the above links to view additional documentation within. + +## What This Example Demonstrates + +### Consumer Side + +- Synchronous HTTP client implementation with requests +- Consumer contract testing with Pact mock servers +- Handling different HTTP response scenarios (success, not found, etc.) +- Modern Python patterns and type hints + +### Provider Side + +- FastAPI web server with RESTful endpoints +- Provider verification against consumer contracts +- Provider state setup for different test scenarios +- Mock data management for testing + +### Testing Patterns + +- Independent consumer and provider testing +- Contract-driven development workflow +- Error handling and edge case testing +- Type safety with Python type hints + +## Pedagogical Context + +This example is intended for software engineers and engineering managers who want to understand: + +- How contract testing works in practice +- Why consumer-driven contracts are valuable +- How to structure Python code for clarity and testability +- The benefits of using requests and FastAPI for simple, modern HTTP services + +## Prerequisites + +- Python 3.9 or higher +- A dependency manager ([uv](https://docs.astral.sh/uv/) recommended, [pip](https://pip.pypa.io/en/stable/) also works) + +## Running the Example + +### Using uv (Recommended) + +We recommend using [uv](https://docs.astral.sh/uv/) to manage the virtual env and manage dependencies. The following command will automatically set up the virtual environment, install dependencies, and then execute the command within the virtual environment: + +```console +uv run --group test pytest +``` + +### Using pip + +If using the [`venv`][venv] module, the steps require are: + +1. Create and activate a virtual environment: + + ```console + python -m venv .venv + source .venv/bin/activate # On macOS/Linux + .venv\Scripts\activate # On Windows + ``` + +2. Install dependencies: + + ```console + pip install -U pip # Pip 25.1 is required + pip install --group test -e . + ``` + +3. Run tests: + + ```console + pytest + ``` + +## Related Documentation + +- [Pact Documentation](https://docs.pact.io/) +- [requests Documentation](https://docs.python-requests.org/) +- [FastAPI Documentation](https://fastapi.tiangolo.com/) +- [pytest Documentation](https://docs.pytest.org/) diff --git a/examples/http/requests_and_fastapi/__init__.py b/examples/http/requests_and_fastapi/__init__.py new file mode 100644 index 000000000..6e031999e --- /dev/null +++ b/examples/http/requests_and_fastapi/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/examples/http/requests_and_fastapi/conftest.py b/examples/http/requests_and_fastapi/conftest.py new file mode 100644 index 000000000..3ced5de4d --- /dev/null +++ b/examples/http/requests_and_fastapi/conftest.py @@ -0,0 +1,35 @@ +""" +Shared PyTest configuration. + +In order to run the examples, we need to run the Pact broker. In order to avoid +having to run the Pact broker manually, or repeating the same code in each +example, we define a PyTest fixture to run the Pact broker. + +We also define a `pact_dir` fixture to define the directory where the generated +Pact files will be stored. You are encouraged to have a look at these files +after the examples have been run. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +import pact_ffi + +EXAMPLE_DIR = Path(__file__).parent.resolve() + + +@pytest.fixture(scope="session") +def pacts_path() -> Path: + """Fixture for the Pact directory.""" + return EXAMPLE_DIR / "pacts" + + +@pytest.fixture(scope="session", autouse=True) +def _setup_pact_logging() -> None: + """ + Set up logging for the pact package. + """ + pact_ffi.log_to_stderr("INFO") diff --git a/examples/http/requests_and_fastapi/consumer.py b/examples/http/requests_and_fastapi/consumer.py new file mode 100644 index 000000000..a25392fab --- /dev/null +++ b/examples/http/requests_and_fastapi/consumer.py @@ -0,0 +1,262 @@ +""" +Requests consumer example. + +This module defines a simple +[consumer](https://docs.pact.io/getting_started/terminology#service-consumer) +using the synchronous [`requests`][requests] library which will be tested with +Pact in the [consumer test][examples.http.requests_and_fastapi.test_consumer]. +As Pact is a consumer-driven framework, the consumer defines the interactions +which the provider must then satisfy. + +The consumer is the application which makes requests to another service (the +provider) and receives a response to process. In this example, we have a simple +`User` class and the consumer fetches a user's information from a HTTP endpoint. + +This also showcases how Pact tests differ from merely testing adherence to an +OpenAPI specification. The Pact tests are more concerned with the practical use +of the API, rather than the formally defined specification. So you will see +below that as far as this consumer is concerned, the only information needed +from the provider is the user's ID, name, and creation date. This is despite the +provider having additional fields in the response. + +Note that the code in this module is agnostic of Pact (i.e., this would be your +production code). The `pact-python` dependency only appears in the tests. This +is because the consumer is not concerned with Pact, only the tests are. +""" + +from __future__ import annotations + +import logging +import sys +from dataclasses import dataclass +from datetime import datetime +from typing import TYPE_CHECKING, Any + +import requests + +if TYPE_CHECKING: + from types import TracebackType + + from typing_extensions import Self + +logger = logging.getLogger(__name__) + + +@dataclass() +class User: + """ + Represents a user as seen by the consumer. + + This class is intentionally minimal, including only the fields the consumer + actually uses. It may differ from the [provider's user + model][examples.http.requests_and_fastapi.provider.User], which could have + additional fields. This demonstrates the consumer-driven nature of contract + testing: the consumer defines what it needs, not what the provider exposes. + """ + + id: int + name: str + created_on: datetime + + def __post_init__(self) -> None: + """ + Validate the user data for contract and business logic. + + Ensures that the user has a non-empty name and a positive integer ID. + + Raises: + ValueError: If the name is empty or the ID is not positive. + """ + if not self.name: + msg = "User must have a name" + raise ValueError(msg) + + if self.id <= 0: + msg = "User ID must be a positive integer" + raise ValueError(msg) + + def __repr__(self) -> str: + """ + Return a string representation of the user. + """ + return f"User(id={self.id!r}, name={self.name!r})" + + +class UserClient: + """ + HTTP client for interacting with a user provider service. + + This class is a simple consumer that fetches user data from a provider over + HTTP. It demonstrates how to structure consumer code for use in contract + testing, keeping it independent of Pact or any contract testing framework. + """ + + def __init__(self, hostname: str, base_path: str | None = None) -> None: + """ + Initialise the user client. + + Args: + hostname: + The base URL of the provider (must include scheme, e.g., + `http://`). + + base_path: + The base path for the provider's API endpoints. Defaults to `/`. + + Raises: + ValueError: + If the hostname does not start with 'http://' or `https://`. + """ + if not hostname.startswith(("http://", "https://")): + msg = "Invalid base URI" + raise ValueError(msg) + self._hostname = hostname + self._base_path = base_path or "/" + if not self._base_path.endswith("/"): + self._base_path += "/" + + self._session = requests.Session() + logger.debug( + "Initialised UserClient with base URL: %s%s", + self.base_url, + self._base_path, + ) + + @property + def hostname(self) -> str: + """ + The hostname as a string. + + This includes the scheme. + """ + return self._hostname + + @property + def base_path(self) -> str: + """ + The base path as a string. + """ + return self._base_path + + @property + def base_url(self) -> str: + """ + The base URL as a string. + """ + return f"{self._hostname}{self._base_path}" + + def __enter__(self) -> Self: + """ + Begin the context for the client. + """ + self._session.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """ + Exit the context for the client. + + Args: + exc_type: + The exception type, if any. + + exc_val: + The exception value, if any. + + exc_tb: + The traceback, if any. + + """ + self._session.__exit__(exc_type, exc_val, exc_tb) + + def get_user(self, user_id: int) -> User: + """ + Fetch a user by ID from the provider. + + This method demonstrates how a consumer fetches only the data it needs + from a provider, regardless of what else the provider may return. + + Args: + user_id: + The ID of the user to fetch. + + Returns: + A `User` instance representing the fetched user. + + Raises: + requests.HTTPError: + If the server returns a non-2xx response or the request fails. + """ + logger.debug("Fetching user %s", user_id) + response = self._session.get(f"{self.hostname}{self.base_path}users/{user_id}") + response.raise_for_status() + data: dict[str, Any] = response.json() + + # Python < 3.11 don't support ISO 8601 offsets without a colon + if sys.version_info < (3, 11) and data["created_on"][-4:].isdigit(): + data["created_on"] = data["created_on"][:-2] + ":" + data["created_on"][-2:] + return User( + id=data["id"], + name=data["name"], + created_on=datetime.fromisoformat(data["created_on"]), + ) + + def create_user( + self, + *, + name: str, + ) -> User: + """ + Create a new user on the provider. + + Args: + name: + The name of the user to create. + + Returns: + A `User` instance representing the newly created user. + + Raises: + requests.HTTPError: + If the server returns a non-2xx response or the request fails. + """ + logger.debug("Creating user %s", name) + response = self._session.post( + f"{self.hostname}{self.base_path}users", + json={"name": name}, + ) + response.raise_for_status() + data = response.json() + + # Python < 3.11 don't support ISO 8601 offsets without a colon + if sys.version_info < (3, 11) and data["created_on"][-4:].isdigit(): + data["created_on"] = data["created_on"][:-2] + ":" + data["created_on"][-2:] + logger.debug("Created user %s", data["id"]) + return User( + id=data["id"], + name=data["name"], + created_on=datetime.fromisoformat(data["created_on"]), + ) + + def delete_user(self, uid: int | User) -> None: + """ + Delete a user by ID from the provider. + + Args: + uid: + The user ID (int) or a `User` instance to delete. + + Raises: + requests.HTTPError: + If the server returns a non-2xx response or the request fails. + """ + if isinstance(uid, User): + uid = uid.id + logger.debug("Deleting user %s", uid) + response = self._session.delete(f"{self.hostname}{self.base_path}users/{uid}") + response.raise_for_status() diff --git a/examples/http/requests_and_fastapi/provider.py b/examples/http/requests_and_fastapi/provider.py new file mode 100644 index 000000000..3e925afbf --- /dev/null +++ b/examples/http/requests_and_fastapi/provider.py @@ -0,0 +1,223 @@ +""" +FastAPI provider example. + +This modules defines a simple +[provider](https://docs.pact.io/getting_started/terminology#service-provider) +implemented with [`fastapi`](https://fastapi.tiangolo.com/) which will be tested +with Pact in the [provider +test][examples.http.requests_and_fastapi.test_provider]. As Pact is a +consumer-driven framework, the consumer defines the contract which the provider +must then satisfy. + +The provider is the application which receives requests from another service +(the consumer) and returns a response. In this example, we have a simple +endpoint which returns a user's information from a (fake) database. + +This also showcases how Pact tests differ from merely testing adherence to an +OpenAPI specification. The Pact tests are more concerned with the practical use +of the API, rather than the formally defined specification. The User class +defined here has additional fields which are not used by the consumer. Should +the provider later decide to add or remove fields, Pact's consumer-driven +testing will provide feedback on whether the consumer is compatible with the +provider's changes. + +Note that the code in this module is agnostic of Pact (i.e., this would be your +production code). The `pact-python` dependency only appears in the tests. This +is because the consumer is not concerned with Pact, only the tests are. +""" + +from __future__ import annotations + +import logging +from datetime import datetime, timezone +from typing import Any, ClassVar, Optional + +from fastapi import FastAPI, HTTPException, status +from pydantic import BaseModel, Field, field_validator + +logger = logging.getLogger(__name__) + + +class User(BaseModel): + """ + Represents a user in the provider system. + + This class models user data as it might exist in a real application. In a + provider context, the data model may contain more fields than are required + by any single consumer. This example demonstrates how a provider can serve + multiple consumers with different data needs, and how consumer-driven + contract testing (such as with Pact) helps ensure compatibility as the + provider evolves. + """ + + id: int + name: str + created_on: datetime = Field(default_factory=lambda: datetime.now(tz=timezone.utc)) + email: Optional[str] = None + ip_address: Optional[str] = None + hobbies: list[str] = Field(default_factory=list) + admin: bool = False + + @field_validator("id") + @classmethod + def validate_id(cls, value: int) -> int: + """ + Ensure the ID is a positive integer. + """ + if value <= 0: + msg = "ID must be a positive integer" + raise ValueError(msg) + return value + + @field_validator("name") + @classmethod + def validate_name(cls, value: str) -> str: + """ + Ensure the name is not empty. + """ + if not value: + msg = "Name must not be empty" + raise ValueError(msg) + return value + + +class UserDb: + """ + A simple in-memory user database abstraction for demonstration purposes. + + This class simulates a user database using a class-level dictionary. In a + real application, this would interface with a persistent database or + external user service. For testing, calls to this class can be mocked to + avoid the need for a real database. See the [test + suite][examples.http.requests_and_fastapi.test_provider] for an example. + """ + + _db: ClassVar[dict[int, User]] = {} + + @classmethod + def create(cls, user: User) -> None: + """ + Add a new user to the database. + + Args: + user: The User instance to add. + """ + cls._db[user.id] = user + + @classmethod + def update(cls, user: User) -> None: + """ + Update an existing user in the database. + + Args: + user: The User instance with updated data. + + Raises: + KeyError: If the user does not exist. + """ + if user.id not in cls._db: + msg = f"User with id {user.id} does not exist." + raise KeyError(msg) + cls._db[user.id] = user + + @classmethod + def delete(cls, user_id: int) -> None: + """ + Delete a user from the database by their ID. + + Args: + user_id: The ID of the user to delete. + + Raises: + KeyError: If the user does not exist. + """ + if user_id not in cls._db: + msg = f"User with id {user_id} does not exist." + raise KeyError(msg) + del cls._db[user_id] + + @classmethod + def get(cls, user_id: int) -> User | None: + """ + Retrieve a user by their ID. + + Args: + user_id: The ID of the user to retrieve. + + Returns: + The User instance if found, else None. + """ + return cls._db.get(user_id) + + @classmethod + def new_user_id(cls) -> int: + """ + Return a free user ID. + """ + return max(cls._db.keys(), default=0) + 1 + + +app = FastAPI() + + +@app.get("/users/{uid}") +async def get_user_by_id(uid: int) -> User: + """ + Retrieve a user by their ID. + + Args: + uid: + The user ID to retrieve. + + Returns: + A User instance representing the user with the given ID. + + Raises: + HTTPException: If the user is not found, a 404 error is raised. + """ + logger.debug("GET /users/%s", uid) + user = UserDb.get(uid) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + + +@app.post("/users/", status_code=status.HTTP_201_CREATED) +async def create_user(data: dict[str, Any]) -> User: + """ + Create a new user in the system. + """ + logger.debug("POST /users/") + + if not data or "name" not in data: + raise HTTPException(status_code=400, detail="Invalid JSON data") + + user = User( + id=UserDb.new_user_id(), + name=data["name"], + created_on=datetime.now(tz=timezone.utc), + email=data.get("email"), + ip_address=data.get("ip_address"), + hobbies=data.get("hobbies", []), + admin=data.get("admin", False), + ) + UserDb.create(user) + return user + + +@app.delete("/users/{uid}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_user(uid: int): # noqa: ANN201 + """ + Delete a user by their ID. + + Args: + uid: + The user ID to delete. + + Raises: + HTTPException: If the user is not found, a 404 error is raised. + """ + logger.debug("DELETE /users/%s", uid) + if UserDb.get(uid) is None: + raise HTTPException(status_code=404, detail="User not found") + UserDb.delete(uid) diff --git a/examples/http/requests_and_fastapi/pyproject.toml b/examples/http/requests_and_fastapi/pyproject.toml new file mode 100644 index 000000000..c03a74f95 --- /dev/null +++ b/examples/http/requests_and_fastapi/pyproject.toml @@ -0,0 +1,28 @@ +#:schema https://json.schemastore.org/pyproject.json +[project] +name = "example-requests-and-fastapi" + +description = "Example of using a requests client and FastAPI server with Pact Python" + +dependencies = ["requests~=2.0", "fastapi~=0.0", "typing-extensions~=4.0"] +requires-python = ">=3.9" +version = "1.0.0" + +[dependency-groups] + +test = ["pact-python", "pytest~=8.0", "uvicorn~=0.29"] +[tool.uv.sources] +pact-python = { path = "../../../" } + +[tool.ruff] +extend = "../../.ruff.toml" + +[tool.pytest] + + [tool.pytest.ini_options] + addopts = ["--import-mode=importlib"] + asyncio_default_fixture_loop_scope = "session" + + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" diff --git a/examples/http/requests_and_fastapi/test_consumer.py b/examples/http/requests_and_fastapi/test_consumer.py new file mode 100644 index 000000000..dd0621843 --- /dev/null +++ b/examples/http/requests_and_fastapi/test_consumer.py @@ -0,0 +1,159 @@ +""" +Consumer contract tests using Pact. + +This module demonstrates how to test a consumer (see +[`consumer.py`][examples.http.requests_and_fastapi.consumer]) against a mock +provider using Pact. The mock provider is set up by Pact to validate that the +consumer makes the expected requests and can handle the provider's responses. +Once validated, the contract can be published to a Pact Broker for use in +provider verification. + +For more information on consumer testing with Pact, see the [Pact Consumer +Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-consumer-pact-test) +section of the Pact documentation. +""" + +from __future__ import annotations + +import logging +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any + +import pytest +import requests + +from examples.http.requests_and_fastapi.consumer import UserClient +from pact import Pact, match + +if TYPE_CHECKING: + from collections.abc import Generator + from pathlib import Path + +logger = logging.getLogger(__name__) + + +@pytest.fixture +def pact(pacts_path: Path) -> Generator[Pact, None, None]: + """ + Set up a Pact mock provider for consumer tests. + + This fixture defines the consumer and provider, and sets up the mock + provider using Pact. Each test can then define the expected request and + response using the Pact DSL. This allows the consumer to be tested in + isolation from the real provider, ensuring that the contract is correct + before integration. + + Args: + pacts_path: + The path where the generated pact file will be written. + + Yields: + A Pact object for use in tests. + """ + pact = Pact("aiohttp-consumer", "flask-provider").with_specification("V4") + yield pact + pact.write_file(pacts_path) + + +def test_get_user(pact: Pact) -> None: + """ + Test the GET request for a user. + + This test defines the expected interaction for a GET request for a user. It + demonstrates how to use Pact to specify the expected request and response, + and how to verify that the consumer code can handle the response correctly. + """ + response: dict[str, object] = { + "id": match.int(123), + "name": match.str("Alice"), + "created_on": match.datetime(), + } + ( + pact.upon_receiving("A user request") + .given("the user exists", id=123, name="Alice") + .with_request("GET", "/users/123") + .will_respond_with(200) + .with_body(response, content_type="application/json") + ) + + with ( + pact.serve() as srv, + UserClient(str(srv.url)) as client, + ): + user = client.get_user(123) + assert user.name == "Alice" + + +def test_get_unknown_user(pact: Pact) -> None: + """ + Test the GET request for an unknown user. + + This test defines the expected interaction for a GET request for a user that + does not exist. It verifies that the consumer handles error responses as + expected. + """ + response = {"detail": "User not found"} + ( + pact.upon_receiving("A request for an unknown user") + .given("the user doesn't exist", id=123) + .with_request("GET", "/users/123") + .will_respond_with(404) + .with_body(response, content_type="application/json") + ) + + with ( + pact.serve() as srv, + UserClient(str(srv.url)) as client, + pytest.raises(requests.HTTPError), + ): + client.get_user(123) + + +def test_create_user(pact: Pact) -> None: + """ + Test the POST request for creating a new user. + + This test defines the expected interaction for a POST request to create a + new user. It demonstrates how to specify the request and response, and how + to verify that the consumer can handle the provider's response. This also + shows how Pact can support multiple requests and responses within a single + test case. + """ + payload: dict[str, Any] = {"name": "Bob"} + response: dict[str, Any] = { + "id": match.int(1000), + "name": "Bob", + "created_on": match.datetime(datetime.now(tz=timezone.utc)), + } + + ( + pact.upon_receiving("A request to create a new user") + .with_request("POST", "/users") + .with_body(payload, content_type="application/json") + .will_respond_with(201) + .with_body(response, content_type="application/json") + ) + + with pact.serve() as srv, UserClient(str(srv.url)) as client: + user = client.create_user(name="Bob") + assert user.id == 1000 + + +def test_delete_user(pact: Pact) -> None: + """ + Test the DELETE request for deleting a user. + + This test defines the expected interaction for a DELETE request to delete a + user. It demonstrates how to use Pact to specify the expected request and + response, and how to verify that the consumer code can handle the response + correctly. + """ + ( + pact.upon_receiving("A user deletion request") + .given("the user exists", id=124, name="Bob") + .with_request("DELETE", "/users/124") + .will_respond_with(204) + ) + + with pact.serve() as srv, UserClient(str(srv.url)) as client: + client.delete_user(124) diff --git a/examples/http/requests_and_fastapi/test_provider.py b/examples/http/requests_and_fastapi/test_provider.py new file mode 100644 index 000000000..a5e1cee9a --- /dev/null +++ b/examples/http/requests_and_fastapi/test_provider.py @@ -0,0 +1,237 @@ +""" +Provider contract tests using Pact. + +This module demonstrates how to test a FastAPI provider (see +[`provider.py`][examples.http.requests_and_fastapi.provider]) against a mock +consumer using Pact. The mock consumer replays the requests defined by the +consumer contract, and Pact validates that the provider responds as expected. + +These tests show how provider verification ensures that the provider remains +compatible with the consumer contract as the provider evolves. Provider state +management is handled by mocking the database and using provider state +endpoints. For more, see the [Pact Provider +Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-provider-pact-test) +section of the Pact documentation. +""" + +from __future__ import annotations + +import contextlib +import dataclasses +import logging +from datetime import datetime, timezone +from threading import Thread +from typing import TYPE_CHECKING, Any, Literal + +import pytest +import uvicorn + +import pact._util +from examples.http.requests_and_fastapi.provider import User, UserDb, app +from pact import Verifier + +if TYPE_CHECKING: + from pathlib import Path + + from typing_extensions import TypeAlias + + ACTION_TYPE: TypeAlias = Literal["setup", "teardown"] + +logger = logging.getLogger(__name__) + + +def start_fastapi_server(host: str, port: int) -> None: + uvicorn.run( + app, + host=host, + port=port, + ) + + +@pytest.fixture(scope="session") +def app_server() -> str: + """ + Run the FastAPI server for provider verification. + + Returns: + The base URL of the running FastAPI server. + """ + hostname = "localhost" + port = pact._util.find_free_port() # noqa: SLF001 + Thread( + target=uvicorn.run, + args=(app,), + kwargs={"host": hostname, "port": port}, + daemon=True, + ).start() + return f"http://{hostname}:{port}" + + +def test_provider(app_server: str, pacts_path: Path) -> None: + """ + Test the provider against the mock consumer contract. + + This test runs the Pact verifier against the FastAPI provider, using the + contract generated by the consumer tests. + + Provider state handlers are essential in Pact contract testing. They allow + the provider to be set up in a specific state before each interaction is + verified. For example, if a consumer expects a user to exist for a certain + request, the provider state handler ensures the database is populated + accordingly. This enables repeatable, isolated, and meaningful contract + verification, as each interaction can be tested in the correct context + without relying on global or persistent state. + + In this example, the state handlers `mock_user_exists` and + `mock_user_does_not_exist` are mapped to the states described in the + contract. They are responsible for setting up (and tearing down) the + in-memory database so that the provider can respond correctly to each + request defined by the consumer contract. + + For additional information on state handlers, see + [`Verifier.state_handler`][pact.verifier.Verifier.state_handler]. + """ + verifier = ( + Verifier("fastapi-provider") + .add_source(pacts_path) + .add_transport(url=app_server) + .state_handler( + { + "the user exists": mock_user_exists, + "the user doesn't exist": mock_user_does_not_exist, + }, + teardown=True, + ) + ) + + verifier.verify() + + +def default_mock_db() -> dict[int, User]: + """ + Standard in-memory database for provider state mocking. + + This function pre-populates a mock database with some default users. It is + used by the provider state handlers to ensure that the database is in the + correct state for each interaction. + + Returns: + A dictionary of user IDs to User objects for use in tests. + """ + return { + 1: User( + id=1, + name="Alice", + email="alice@example.com", + created_on=datetime(2020, 1, 1, 12, 0, 0, tzinfo=timezone.utc), + ip_address="1.2.3.4", + hobbies=["pact testing", "programming", "qa"], + admin=False, + ), + 2: User( + id=2, + name="Bob", + email=None, # Edge case: email is None + created_on=datetime(1999, 12, 31, 23, 59, 59, tzinfo=timezone.utc), + ip_address="", + hobbies=[], + admin=True, + ), + 10: User( + id=10, + name="Charlie", + email="charlie@example.com", + created_on=datetime(2025, 8, 8, 8, 8, 8, tzinfo=timezone.utc), + ip_address="3.4.5.6", + hobbies=[""], + admin=False, + ), + 42: User( + id=42, + name="Dana", + email="dana+test@example.com", + created_on=datetime(2022, 2, 22, 2, 22, 22, tzinfo=timezone.utc), + ip_address="255.255.255.255", + hobbies=["edge", "case", "testing"], + admin=False, + ), + } + + +def mock_user_exists( + action: Literal["setup", "teardown"], + parameters: dict[str, Any], +) -> None: + """ + Mock the provider state where a user exists. + + This handler sets up the provider so that a user with the given ID exists in + the database. Used by Pact to ensure the provider is in the correct state + for each interaction. + + Args: + action: + The action to perform, either "setup" or "teardown". + + parameters: + User information, including an `id` to guarantee presence in the + database. Additional fields may be provided to override defaults. + """ + logger.debug("mock_user_exists(%s, %r)", action, parameters) + + # We pre-populate the database with some data, and if a state requires + # some specific data, ensure the user is present. + db = default_mock_db() + user = db[uid] if (uid := parameters.get("id")) in db else next(iter(db.values())) + user = User(**{**dataclasses.asdict(user), **parameters}) + + if action == "setup": + UserDb.create(user) + return + + if action == "teardown": + with contextlib.suppress(KeyError): + UserDb.delete(user.id) + return + + msg = f"Unknown action: {action}" + raise ValueError(msg) + + +def mock_user_does_not_exist( + action: Literal["setup", "teardown"], + parameters: dict[str, Any], +) -> None: + """ + Mock the provider state where a user does not exist. + + This handler sets up the provider so that a user with the given ID does not + exist in the database. Used by Pact to ensure the provider is in the correct + state for each interaction. + + Args: + action: + The action to perform, either "setup" or "teardown". + + parameters: + User information, must contain an `id` to guarantee absence in the + database. + """ + logger.debug("mock_user_does_not_exist(%s, %r)", action, parameters) + + if "id" not in parameters: + msg = "State must contain an 'id' field to mock user non-existence" + raise ValueError(msg) + + uid = parameters["id"] + + if action == "setup": + if user := UserDb.get(uid): + UserDb.delete(user.id) + return + + if action == "teardown": + return + + msg = f"Unknown action: {action}" + raise ValueError(msg) diff --git a/mkdocs.yml b/mkdocs.yml index 19e1c472f..49c757a8b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,12 +27,14 @@ plugins: enable_inventory: true handlers: python: - import: + inventories: - https://docs.aiohttp.org/en/stable/objects.inv - https://docs.python.org/3/objects.inv + - https://fastapi.tiangolo.com/objects.inv - https://flask.palletsprojects.com/en/stable/objects.inv - https://googleapis.dev/python/protobuf/latest/objects.inv - https://grpc.github.io/grpc/python/objects.inv + - https://requests.readthedocs.io/en/latest/objects.inv options: # General allow_inspection: true From 470f981b18ea8aa001be1f449eabd0b129eb4ebd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Aug 2025 09:51:36 +0000 Subject: [PATCH 0958/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.2.0 (#1189) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 325651442..7d27ace14 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.1.4 + rev: v2.2.0 hooks: - id: biome-check From 1b667bb013a849ba0ea3faafd75d79eafd1c0d65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:08:20 +1000 Subject: [PATCH 0959/1376] chore(deps): update pypa/cibuildwheel action to v3.1.4 (#1194) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 395926469..d58e8cdee 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -98,7 +98,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 + uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 with: package-dir: pact-python-cli env: diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 263d4cc78..d96a9fbbc 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -98,7 +98,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 + uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 with: package-dir: pact-python-ffi env: From 4bec48508e3eb5c5ee9cc990ebd892624789c1ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:22:55 +0000 Subject: [PATCH 0960/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.35.5 (#1193) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d27ace14..dc12ae789 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,7 +78,7 @@ repos: )$ - repo: https://github.com/crate-ci/typos - rev: v1.35.4 + rev: v1.35.5 hooks: - id: typos exclude: | From 3447561d621361a264787057d033f064bff36b98 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:26:28 +0000 Subject: [PATCH 0961/1376] chore(deps): update taiki-e/install-action action to v2.58.17 (#1192) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index d58e8cdee..6a6e113a0 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -139,7 +139,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@1fd1160ee1f4387ce162a5a4f9c26ea274278b7f # v2.58.7 + uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index d96a9fbbc..a2c80438a 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@1fd1160ee1f4387ce162a5a4f9c26ea274278b7f # v2.58.7 + uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8372644c6..f9b4281a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -108,7 +108,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@1fd1160ee1f4387ce162a5a4f9c26ea274278b7f # v2.58.7 + uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17 with: tool: git-cliff,typos From d55c5b7a3df221867191505fc0bc7717053013d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 09:16:19 +1000 Subject: [PATCH 0962/1376] chore(deps): update codecov/codecov-action action to v5.5.0 (#1195) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 027844106..495d12ecf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -169,7 +169,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0 with: token: ${{ secrets.CODECOV_TOKEN }} flags: examples From b6fb1b8490a1e76a1df4849d0c62f1d0551f8908 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 09:25:54 +1000 Subject: [PATCH 0963/1376] chore(deps): update ruff to v0.12.10 (#1197) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc12ae789..d8d419fd8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.9 + rev: v0.12.10 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 6ae2ad4e0..da9ee0c7c 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.9" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.12.9", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.12.10", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=6.0", "pytest~=8.0"] types = ["mypy==1.17.1"] From df700ea0a3f3c861faa2eb5d575b6e0bc07934ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 23:27:39 +0000 Subject: [PATCH 0964/1376] chore(deps): update astral-sh/setup-uv action to v6.6.0 (#1196) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 6a6e113a0..22cdc7afe 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 + uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index a2c80438a..a4e2ba57b 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 + uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f9b4281a8..377080d6e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 + uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 137bb2243..173b687c4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 + uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 495d12ecf..6e6e5e14f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,7 +81,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 + uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 with: enable-cache: true @@ -149,7 +149,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 + uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 with: enable-cache: true @@ -192,7 +192,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 + uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 with: enable-cache: true @@ -224,7 +224,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 + uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 with: enable-cache: true @@ -257,7 +257,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 + uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 with: enable-cache: true @@ -296,7 +296,7 @@ jobs: key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Set up uv - uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # v6.5.0 + uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 with: enable-cache: true cache-suffix: pre-commit From dba8d1a22598d7dc7c51ddf98063b16f72771c07 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 21:12:01 +0000 Subject: [PATCH 0965/1376] chore(deps): update actions/upload-pages-artifact action to v4 (#1198) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 173b687c4..14d884b1a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -45,7 +45,7 @@ jobs: hatch run mkdocs build - name: Upload artifact - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 with: path: site From 5ecde1268dc98da230d8d40ba00c7cea61f0bf05 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 08:42:07 +0000 Subject: [PATCH 0966/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.2.2 (#1199) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d8d419fd8..a69c83b9c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.2.0 + rev: v2.2.2 hooks: - id: biome-check From 89c3c45fb4976b7fdc73d567c4668af581e3d90e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 04:16:06 +0000 Subject: [PATCH 0967/1376] chore(deps): update taiki-e/install-action action to v2.58.21 (#1200) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 22cdc7afe..bc7a004d6 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -139,7 +139,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17 + uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index a4e2ba57b..7139c4591 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17 + uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 377080d6e..46686da8b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -108,7 +108,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17 + uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21 with: tool: git-cliff,typos From 103aa13104e0269bd63dbe4031e2de4809fee6d5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 26 Aug 2025 18:43:23 +1000 Subject: [PATCH 0968/1376] chore: remove reference count checks Signed-off-by: JP-Ellis --- pact-python-ffi/src/pact_ffi/__init__.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index abb665d07..06a8305bc 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -86,7 +86,6 @@ from __future__ import annotations -import gc import inspect import json import logging @@ -6199,13 +6198,6 @@ def with_binary_body( RuntimeError: If the body could not be modified. """ - if len(gc.get_referrers(body)) == 0: - warnings.warn( - "Make sure to assign the body to a variable to avoid having the byte array" - " modified.", - UserWarning, - stacklevel=3, - ) success: bool = lib.pactffi_with_binary_body( interaction._ref, part.value, @@ -6266,13 +6258,6 @@ def with_binary_file( RuntimeError: If the body could not be set. """ - if len(gc.get_referrers(body)) == 0: - warnings.warn( - "Make sure to assign the body to a variable to avoid having the byte array" - " modified.", - UserWarning, - stacklevel=3, - ) success: bool = lib.pactffi_with_binary_file( interaction._ref, part.value, From ad2da2b0b4f1134c3dc9c16417655480c2bc3748 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 26 Aug 2025 18:48:36 +1000 Subject: [PATCH 0969/1376] chore: store hatch venv in .venv Signed-off-by: JP-Ellis --- pact-python-cli/pyproject.toml | 2 ++ pact-python-ffi/pyproject.toml | 2 ++ pyproject.toml | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index da9ee0c7c..33a8236ef 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -108,6 +108,7 @@ requires = ["hatch-vcs", "hatchling", "packaging"] "setuptools ; python_version >= '3.12'", ] installer = "uv" + path = ".venv" pre-install-commands = ["uv pip install --group dev -e ."] [tool.hatch.envs.default.scripts] @@ -123,6 +124,7 @@ requires = ["hatch-vcs", "hatchling", "packaging"] # supported Python versions. [tool.hatch.envs.test] installer = "uv" + path = ".venv/test" pre-install-commands = ["uv pip install --group test -e ."] [[tool.hatch.envs.test.matrix]] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 204112d7c..72da52305 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -94,6 +94,7 @@ requires = ["hatch-vcs", "hatchling", "packaging", "cffi"] [tool.hatch.envs.default] extra-dependencies = ["hatch-vcs", "hatchling", "packaging", "cffi"] installer = "uv" + path = ".venv" pre-install-commands = ["uv pip install --group dev -e ."] # Update paths to ensure the shared library can be found @@ -115,6 +116,7 @@ requires = ["hatch-vcs", "hatchling", "packaging", "cffi"] # supported Python versions. [tool.hatch.envs.test] installer = "uv" + path = ".venv/test" pre-install-commands = ["uv pip install --group test -e ."] # Update paths to ensure the shared library can be found diff --git a/pyproject.toml b/pyproject.toml index 3bc96fcd8..a2fa9426e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -186,6 +186,7 @@ requires = ["hatch-vcs", "hatchling"] [tool.hatch.envs.default] extra-dependencies = ["hatchling", "hatch-vcs"] installer = "uv" + path = ".venv" # This is require to get around an incompatibility between hatch and uv # See: https://github.com/pypa/hatch/issues/1639 # See: https://github.com/pypa/hatch/issues/1852 @@ -217,6 +218,7 @@ requires = ["hatch-vcs", "hatchling"] # Test environment for running unit tests. [tool.hatch.envs.test] installer = "uv" + path = ".venv/test" pre-install-commands = ["uv pip install --group test -e ."] [[tool.hatch.envs.test.matrix]] @@ -226,6 +228,7 @@ requires = ["hatch-vcs", "hatchling"] # supported Python versions. [tool.hatch.envs.example] installer = "uv" + path = ".venv/example" pre-install-commands = ["uv pip install --group example -e ."] [tool.hatch.envs.example.scripts] @@ -237,6 +240,7 @@ requires = ["hatch-vcs", "hatchling"] [tool.hatch.envs.v2-test] features = ["v2"] installer = "uv" + path = ".venv/v2-test" pre-install-commands = ["uv pip install --group test-v2 -e ."] [tool.hatch.envs.v2-test.scripts] @@ -249,6 +253,7 @@ requires = ["hatch-vcs", "hatchling"] [tool.hatch.envs.v2-example] features = ["v2"] installer = "uv" + path = ".venv/v2-example" pre-install-commands = ["uv pip install --group example-v2 -e ."] [tool.hatch.envs.v2-example.scripts] From 5a9c6280857eaa1a44ecfbe362738246f7f0eff8 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 26 Aug 2025 18:51:12 +1000 Subject: [PATCH 0970/1376] chore: update mismatch repr The mismatches are simple enough that the representation can be made Python-parseable. Signed-off-by: JP-Ellis --- src/pact/error.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/pact/error.py b/src/pact/error.py index c19d6a55f..da0bd3bf1 100644 --- a/src/pact/error.py +++ b/src/pact/error.py @@ -191,7 +191,9 @@ def __repr__(self) -> str: """ Information-rich string representation of the GenericMismatch. """ - return f"" + return "GenericMismatch({})".format( + ", ".join(f"{key}={value!r}" for key, value in self._data.items()) + ) def __str__(self) -> str: """ @@ -255,7 +257,7 @@ def __repr__(self) -> str: """ Information-rich string representation of the MissingRequest. """ - return "".format( + return "MissingRequest({})".format( ", ".join([ f"method={self.method!r}", f"path={self.path!r}", @@ -328,7 +330,7 @@ def __repr__(self) -> str: """ Information-rich string representation of the RequestNotFound. """ - return "".format( + return "RequestNotFound({})".format( ", ".join([ f"method={self.method!r}", f"path={self.path!r}", @@ -403,7 +405,7 @@ def __repr__(self) -> str: """ Information-rich string representation of the RequestMismatch. """ - return "".format( + return "RequestMismatch({})".format( ", ".join([ f"method={self.method!r}", f"path={self.path!r}", @@ -465,7 +467,12 @@ def __repr__(self) -> str: """ Information-rich string representation of the MethodMismatch. """ - return f"" + return "MethodMismatch({})".format( + ", ".join([ + f"expected={self.expected!r}", + f"actual={self.actual!r}", + ]) + ) def __str__(self) -> str: """ @@ -529,7 +536,7 @@ def __repr__(self) -> str: """ Information-rich string representation of the PathMismatch. """ - return "".format( + return "PathMismatch({})".format( ", ".join([ f"expected={self.expected!r}", f"actual={self.actual!r}", @@ -603,7 +610,7 @@ def __repr__(self) -> str: """ Information-rich string representation of the StatusMismatch. """ - return "".format( + return "StatusMismatch({})".format( ", ".join([ f"expected={self.expected!r}", f"actual={self.actual!r}", @@ -694,7 +701,7 @@ def __repr__(self) -> str: """ Information-rich string representation of the QueryMismatch. """ - return "".format( + return "QueryMismatch({})".format( ", ".join([ f"parameter={self.parameter!r}", f"expected={self.expected!r}", @@ -776,7 +783,7 @@ def __repr__(self) -> str: """ Information-rich string representation of the HeaderMismatch. """ - return "".format( + return "HeaderMismatch({})".format( ", ".join([ f"key={self.key!r}", f"expected={self.expected!r}", @@ -884,7 +891,7 @@ def __repr__(self) -> str: """ Information-rich string representation of the BodyTypeMismatch. """ - return "".format( + return "BodyTypeMismatch({})".format( ", ".join([ f"expected={self.expected!r}", f"actual={self.actual!r}", @@ -973,7 +980,7 @@ def __repr__(self) -> str: """ Information-rich string representation of the BodyMismatch. """ - return "".format( + return "BodyMismatch({})".format( ", ".join([ f"path={self.path!r}", f"expected={self.expected!r}", @@ -1055,7 +1062,7 @@ def __repr__(self) -> str: """ Information-rich string representation of the MetadataMismatch. """ - return "".format( + return "MetadataMismatch({})".format( ", ".join([ f"key={self.key!r}", f"expected={self.expected!r}", @@ -1104,8 +1111,8 @@ def __repr__(self) -> str: """ Information-rich string representation of the MismatchesError. """ - return "".format( - [f"{m!r}" for m in self._mismatches], + return "MismatchesError({})".format( + ", ".join(f"{m!r}" for m in self._mismatches) ) def __str__(self) -> str: From 84a74d873096e43572a5a4bef2b9b12d2efcc6a5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 26 Aug 2025 19:27:35 +1000 Subject: [PATCH 0971/1376] chore: save mismatches before exiting the server Signed-off-by: JP-Ellis --- src/pact/pact.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pact/pact.py b/src/pact/pact.py index f2a2a704f..29d64b090 100644 --- a/src/pact/pact.py +++ b/src/pact/pact.py @@ -640,6 +640,7 @@ def __init__( # noqa: PLR0913 self._handle: None | pact_ffi.PactServerHandle = None self._raises = raises self._verbose = verbose + self._mismatches: list[Mismatch] | None = None @property def port(self) -> int | None: @@ -702,6 +703,9 @@ def mismatches(self) -> list[Mismatch]: RuntimeError: If the server is not running. """ + if self._mismatches is not None: + return self._mismatches + if not self._handle: msg = "The server is not running." raise RuntimeError(msg) @@ -796,6 +800,7 @@ def __exit__( logger.error(msg) if self._raises: raise MismatchesError(*self.mismatches) + self._mismatches = self.mismatches self._handle = None def __truediv__(self, other: str | object) -> URL: From a169bfae0d2267f9c262bb4a6e45ffa68bddbec9 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 26 Aug 2025 19:32:21 +1000 Subject: [PATCH 0972/1376] chore(examples): remove old http example Signed-off-by: JP-Ellis --- examples/v2/tests/v3/test_00_consumer.py | 184 -------- .../v2/tests/v3/test_01_fastapi_provider.py | 401 ------------------ 2 files changed, 585 deletions(-) delete mode 100644 examples/v2/tests/v3/test_00_consumer.py delete mode 100644 examples/v2/tests/v3/test_01_fastapi_provider.py diff --git a/examples/v2/tests/v3/test_00_consumer.py b/examples/v2/tests/v3/test_00_consumer.py deleted file mode 100644 index 06583f772..000000000 --- a/examples/v2/tests/v3/test_00_consumer.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -HTTP consumer test using Pact Python v3. - -This module demonstrates how to write a consumer test using Pact Python's -upcoming version 3. Pact, being a consumer-driven testing tool, requires that -the consumer define the expected interactions with the provider. - -In this example, the consumer defined in `src/consumer.py` is tested against a -mock provider. The mock provider is set up by Pact and is used to ensure that -the consumer is making the expected requests to the provider. Once these -interactions are validated, the contracts can be published to a Pact Broker -where they can be re-run against the provider to ensure that the provider is -compliant with the contract. - -A good source for understanding the consumer tests is the [Pact Consumer Test -section](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-consumer-pact-test) -of the Pact documentation. -""" - -import json -from collections.abc import Generator -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -import pytest -import requests - -from examples.v2.src.consumer import UserConsumer -from pact import Pact, match - - -@pytest.fixture -def pact(pacts_path: Path) -> Generator[Pact, None, None]: - """ - Set up the Pact fixture. - - This fixture configures the Pact instance for the consumer test. It defines - where the pact file will be written and the consumer and provider names. - This fixture also sets the Pact specification to `V4` (the latest version). - - The use of `yield` allows this function to return the Pact instance to be - used in the test cases, and then for this function to continue running after - the test cases have completed. This is useful for writing the pact file - after the test cases have run. - - Yields: - The Pact instance for the consumer tests. - """ - pact = Pact("v3_http_consumer", "v3_http_provider") - yield pact.with_specification("V4") - pact.write_file(pacts_path) - - -def test_get_existing_user(pact: Pact) -> None: - """ - Retrieve an existing user's details. - - This test defines the expected interaction for a GET request to retrieve - user information. It sets up the expected request and response from the - provider and verifies that the response status code is 200. - - When setting up the expected response, the consumer should only define what - it needs from the provider (as opposed to the full schema). Should the - provider later decide to add or remove fields, Pact's consumer-driven - approach will ensure that interaction is still valid. - - The use of the `given` method allows the consumer to define the state of the - provider before the interaction. In this case, the provider is in a state - where the user exists and can be retrieved. By contrast, the same HTTP - request with a different `given` state is expected to return a 404 status - code as shown in - [`test_get_non_existent_user`](#test_get_non_existent_user). - """ - expected: dict[str, Any] = { - "id": 123, - "name": "Verna Hampton", - "created_on": match.datetime( - # Python datetime objects are automatically formatted - datetime.now(tz=timezone.utc), - format="%Y-%m-%dT%H:%M:%S%z", - ), - } - ( - pact.upon_receiving("a request for user information") - .given("user exists") - .with_request(method="GET", path="/users/123") - .will_respond_with(200) - .with_body(expected) - ) - - with pact.serve() as srv: - client = UserConsumer(str(srv.url)) - user = client.get_user(123) - assert user.id == 123 - assert user.name == "Verna Hampton" - - -def test_get_non_existent_user(pact: Pact) -> None: - """ - Test the GET request for retrieving user information. - - This test defines the expected interaction for a GET request to retrieve - user information when that user does not exist in the provider's database. - It is the counterpart to the - [`test_get_existing_user`](#test_get_existing_user) and showcases how the - same request can have different responses based on the provider's state. - - It is up to the specific use case to determine whether negative scenarios - should be tested, and to what extent. Certain common negative scenarios - include testing for non-existent resources, unauthorized access attempts may - be useful to ensure that the consumer handles these cases correctly; but it - is generally infeasible to test all possible negative scenarios. - """ - expected_response_code = 404 - ( - pact.upon_receiving("a request for user information") - .given("user doesn't exists") - .with_request(method="GET", path="/users/2") - .will_respond_with(404) - ) - - with pact.serve() as srv: - response = requests.get(f"{srv.url}/users/2", timeout=5) - - assert response.status_code == expected_response_code - - -def test_create_user(pact: Pact) -> None: - """ - Test the POST request for creating a new user. - - This test defines the expected interaction for a POST request to create - a new user. It sets up the expected request and response from the provider, - including the request body and headers, and verifies that the response - status code is 200 and the response body matches the expected user data. - """ - body = {"name": "Verna Hampton"} - expected_response: dict[str, Any] = { - "id": 124, - "name": "Verna Hampton", - "created_on": match.datetime( - # Python datetime objects are automatically formatted - datetime.now(tz=timezone.utc), - format="%Y-%m-%dT%H:%M:%S%z", - ), - } - - ( - pact.upon_receiving("a request to create a new user") - .given("the specified user doesn't exist") - .with_request(method="POST", path="/users/") - .with_body(json.dumps(body), content_type="application/json") - .will_respond_with(status=200) - .with_body(content_type="application/json", body=expected_response) - ) - - with pact.serve() as srv: - client = UserConsumer(str(srv.url)) - user = client.create_user(name="Verna Hampton") - assert user.id > 0 - assert user.name == "Verna Hampton" - assert user.created_on - - -def test_delete_request_to_delete_user(pact: Pact) -> None: - """ - Test the DELETE request for deleting a user. - - This test defines the expected interaction for a DELETE request to delete - a user. It sets up the expected request and response from the provider, - including the request body and headers, and verifies that the response - status code is 200 and the response body matches the expected user data. - """ - ( - pact.upon_receiving("a request for deleting user") - .given("user is present in DB") - .with_request(method="DELETE", path="/users/124") - .will_respond_with(204) - ) - - with pact.serve() as srv: - client = UserConsumer(str(srv.url)) - client.delete_user(124) diff --git a/examples/v2/tests/v3/test_01_fastapi_provider.py b/examples/v2/tests/v3/test_01_fastapi_provider.py deleted file mode 100644 index d105b722a..000000000 --- a/examples/v2/tests/v3/test_01_fastapi_provider.py +++ /dev/null @@ -1,401 +0,0 @@ -""" -Test the FastAPI provider with Pact. - -This module demonstrates how to write a provider test using Pact Python's -upcoming version 3. Pact, being a consumer-driven testing tool, requires that -the provider respond to the requests defined by the consumer. The consumer -defines the expected interactions with the provider, and the provider is -expected to respond with the expected responses. - -This module tests the FastAPI provider defined in `src/fastapi.py` against the -mock consumer. The mock consumer is set up by Pact and will replay the requests -defined by the consumers. Pact will then validate that the provider responds -with the expected responses. - -The provider will be expected to be in a given state in order to respond to -certain requests. For example, when fetching a user's information, the provider -will need to have a user with the given ID in the database. In order to avoid -side effects, the provider's database calls are mocked out using functionalities -from `unittest.mock`. - -Note that Pact requires that the provider be running on an accessible URL. This -means that FastAPI's [`TestClient`][fastapi.testclient.TestClient] cannot be used -to test the provider. Instead, the provider is run in a separate thread using -Python's [`Thread`][threading.Thread] class. -""" - -from __future__ import annotations - -import contextlib -import time -from datetime import datetime, timezone -from threading import Thread -from typing import TYPE_CHECKING, Any -from unittest.mock import MagicMock - -import pytest -import uvicorn -from yarl import URL - -import examples.v2.src.fastapi -from examples.v2.src.fastapi import User -from pact import Verifier - -if TYPE_CHECKING: - from collections.abc import Generator - -PROVIDER_URL = URL("http://localhost:8000") - - -class Server(uvicorn.Server): - """ - Custom server class to run the FastAPI server in a separate thread. - - Thanks to [this StackOverflow - answer](https://stackoverflow.com/a/64521239/1573761) for this solution. - """ - - def install_signal_handlers(self) -> None: - """ - Prevent the server from installing signal handlers. - - This is required to run the FastAPI server in a separate process. The - default behaviour of `uvicorn.Server` is to install signal handlers which - would interfere with the signal handlers of the main process. - """ - - @contextlib.contextmanager - def run_in_thread(self) -> Generator[str, None, None]: - """ - Run the FastAPI server in a separate thread. - - This method runs the FastAPI server in a separate thread and yields the - URL of the server. The server is started in a separate thread to allow the - tests to run in the main thread. - """ - thread = Thread(target=self.run) - thread.start() - try: - while not self.started: - time.sleep(0.01) - yield f"http://{self.config.host}:{self.config.port}" - finally: - self.should_exit = True - thread.join() - - -@pytest.fixture(scope="session") -def server() -> Generator[str, None, None]: - server = Server(uvicorn.Config("examples.v2.src.fastapi:app", host="localhost")) - with server.run_in_thread() as url: - yield url - - -def test_provider(server: str, pacts_path: Path) -> None: - """ - Test the FastAPI provider with Pact. - - This function performs all of the provider testing. It runs as follows: - - 1. The FastAPI server is started in a separate process. A small wait time - is added to allow the server to start up before the tests begin. - 2. The Verifier is created and configured. - - 1. The `set_info` method tells Pact the names of provider to be tested. - Pact will automatically discover all the consumers that have - contracts with this provider. - - The `url` parameter is used to specify the base URL of the provider - against which the tests will be run. - 2. The `add_source` method adds the directory where the pact files are - stored. In a more typical setup, this would in fact be a Pact Broker - URL. - 3. The `set_state` method defines the endpoint on the provider that - will be called to set the provider into the correct state before the - tests begin. At present, this is the only way to set the provider - into the correct state; however, future version of Pact Python - intend to provide a more Pythonic way to do this. - - 3. The `verify` method is called to run the tests. This will run all the - tests defined in the pact files and verify that the provider responds - correctly to each request. More specifically, for each interaction, it - will perform the following steps: - - 1. The provider state(s) are by sending a POST request to the - provider's `_pact/callback` endpoint. - 2. Pact impersonates the consumer by sending a request to the provider. - 3. The provider handles the request and sends a response back to Pact. - 4. Pact validates the response against the expected response defined in - the pact file. - 5. If `teardown` is set to `True` for `set_state`, Pact will send a - `teardown` action to the provider to reset the provider state(s). - - Pact will output the results of the tests to the console and verify that the - provider is compliant with the contract. If the provider is not compliant, - the tests will fail and the output will show which interactions failed and - why. - """ - verifier = ( - Verifier("v3_http_provider") - .add_transport(url=server) - .add_source(f"{pacts_path}/v3_http_consumer-v3_http_provider.json") - .state_handler(provider_state_handler, teardown=True) - ) - verifier.verify() - - -def provider_state_handler( - state: str, - action: str, - parameters: dict[str, Any] | None = None, # noqa: ARG001 -) -> None: - """ - Handler for the provider state callback. - - For Pact to be able to correctly test compliance with the contract, the - internal state of the provider needs to be set up correctly. For example, if - the consumer expects a user to exist in the database, the provider needs to - have a user with the given ID in the database. - - Naïvely, this can be achieved by setting up the database with the correct - data for the test, but this can be slow and error-prone, and requires - standing up additional infrastructure. The alternative showcased here is to - mock the relevant calls to the database so as to avoid any side effects. The - `unittest.mock` library is used to achieve this as part of the `setup` - action. - - The added benefit of using this approach is that the mock can subsequently - be inspected to ensure that the correct calls were made to the database. For - example, asserting that the correct user ID was retrieved from the database. - These checks are performed as part of the `teardown` action. This action can - also be used to reset the mock, or in the case were a real database is used, - to clean up any side effects. - - This example showcases how a _full_ provider state handler can be - implemented. The handler can also be specified through a mapping of provider - states to functions. See the documentation of the - [`state_handler`][pact.v3.Verifier.state_handler] method for more details. - - Args: - action: - One of `setup` or `teardown`. Determines whether the provider state - should be set up or torn down. - - state: - The name of the state to set up or tear down. - - parameters: - A dictionary of parameters to pass to the state handler. This is - not used in this example, but is included for completeness. - - Returns: - A dictionary containing the result of the action. - """ - if action == "setup": - { - "user doesn't exists": mock_user_doesnt_exist, - "user exists": mock_user_exists, - "the specified user doesn't exist": mock_post_request_to_create_user, - "user is present in DB": mock_delete_request_to_delete_user, - }[state]() - - if action == "teardown": - { - "user doesn't exists": verify_user_doesnt_exist_mock, - "user exists": verify_user_exists_mock, - "the specified user doesn't exist": verify_mock_post_request_to_create_user, - "user is present in DB": verify_mock_delete_request_to_delete_user, - }[state]() - - -def mock_user_doesnt_exist() -> None: - """ - Mock the database for the user doesn't exist state. - """ - mock_db = MagicMock() - mock_db.get.return_value = None - examples.v2.src.fastapi.FAKE_DB = mock_db - - -def mock_user_exists() -> None: - """ - Mock the database for the user exists state. - - You may notice that the return value here differs from the consumer's - expected response. This is because the consumer's expected response is - guided by what the consumer uses. - - By using consumer-driven contracts and testing the provider against the - consumer's contract, we can ensure that the provider is what the consumer - needs. This allows the provider to safely evolve their API (by both adding - and removing fields) without fear of breaking the interactions with the - consumers. - """ - mock_db = MagicMock() - mock_db.get.return_value = User( - id=123, - name="Verna Hampton", - email="verna@example.com", - created_on=datetime.now(tz=timezone.utc), - ip_address="10.1.2.3", - hobbies=["hiking", "swimming"], - admin=False, - ) - examples.v2.src.fastapi.FAKE_DB = mock_db - - -def mock_post_request_to_create_user() -> None: - """ - Mock the database for the post request to create a user. - - While the `FAKE_DB` is a dictionary in this example, one should imagine that - this is a real database. In this instance, we are replacing the calls to the - database with a local dictionary to avoid side effects; thereby eliminating - the need to stand up a real database for the tests. - - The added benefit of using this approach is that the mock can subsequently - be inspected to ensure that the correct calls were made to the database. For - example, asserting that the correct user ID was retrieved from the database. - These checks are performed as part of the `teardown` action. This action can - also be used to reset the mock, or in the case were a real database is used, - to clean up any side effects. - """ - local_db: dict[int, User] = {} - - def local_setitem(key: int, value: User) -> None: - local_db[key] = value - - def local_getitem(key: int) -> User: - return local_db[key] - - mock_db = MagicMock() - mock_db.__len__.return_value = 124 - mock_db.__setitem__.side_effect = local_setitem - mock_db.__getitem__.side_effect = local_getitem - examples.v2.src.fastapi.FAKE_DB = mock_db - - -def mock_delete_request_to_delete_user() -> None: - """ - Mock the database for the delete request to delete a user. - - As with the `mock_post_request_to_create_user` function, we are using a - local dictionary to avoid side effects. This function replaces the calls to - the database with a local dictionary to avoid side effects. - """ - local_db = { - 123: User( - id=123, - name="Verna Hampton", - email="verna@example.com", - created_on=datetime.now(tz=timezone.utc), - ip_address="10.1.2.3", - hobbies=["hiking", "swimming"], - admin=False, - ), - 124: User( - id=124, - name="Jane Doe", - email="jane@example.com", - created_on=datetime.now(tz=timezone.utc), - ip_address="10.1.2.5", - hobbies=["running", "dancing"], - admin=False, - ), - } - - def local_delitem(key: int) -> None: - del local_db[key] - - def local_contains(key: int) -> bool: - return key in local_db - - mock_db = MagicMock() - mock_db.__delitem__.side_effect = local_delitem - mock_db.__contains__.side_effect = local_contains - examples.v2.src.fastapi.FAKE_DB = mock_db - - -def verify_user_doesnt_exist_mock() -> None: - """ - Verify the mock calls for the 'user doesn't exist' state. - - This function checks that the mock for `FAKE_DB.get` was called, verifies - that it returned `None`, and ensures that it was called with an integer - argument. It then resets the mock for future tests. - """ - if TYPE_CHECKING: - # During setup, the `FAKE_DB` is replaced with a MagicMock object. - # We need to inform the type checker that this has happened. - examples.v2.src.fastapi.FAKE_DB = MagicMock() - - assert len(examples.v2.src.fastapi.FAKE_DB.mock_calls) == 1 - - examples.v2.src.fastapi.FAKE_DB.get.assert_called_once() - args, kwargs = examples.v2.src.fastapi.FAKE_DB.get.call_args - assert len(args) == 1 - assert isinstance(args[0], int) - assert kwargs == {} - - examples.v2.src.fastapi.FAKE_DB.reset_mock() - - -def verify_user_exists_mock() -> None: - """ - Verify the mock calls for the 'user exists' state. - - This function checks that the mock for `FAKE_DB.get` was called, verifies - that it returned the expected user data, and ensures that it was called with - the integer argument `1`. It then resets the mock for future tests. - """ - if TYPE_CHECKING: - examples.v2.src.fastapi.FAKE_DB = MagicMock() - - assert len(examples.v2.src.fastapi.FAKE_DB.mock_calls) == 1 - - examples.v2.src.fastapi.FAKE_DB.get.assert_called_once() - args, kwargs = examples.v2.src.fastapi.FAKE_DB.get.call_args - assert len(args) == 1 - assert isinstance(args[0], int) - assert kwargs == {} - - examples.v2.src.fastapi.FAKE_DB.reset_mock() - - -def verify_mock_post_request_to_create_user() -> None: - if TYPE_CHECKING: - examples.v2.src.fastapi.FAKE_DB = MagicMock() - - assert len(examples.v2.src.fastapi.FAKE_DB.mock_calls) == 3 - - examples.v2.src.fastapi.FAKE_DB.__getitem__.assert_called_once() - args, kwargs = examples.v2.src.fastapi.FAKE_DB.__getitem__.call_args - assert len(args) == 1 - assert isinstance(args[0], int) - assert kwargs == {} - - examples.v2.src.fastapi.FAKE_DB.__len__.assert_called_once() - args, kwargs = examples.v2.src.fastapi.FAKE_DB.__len__.call_args - assert len(args) == 0 - assert kwargs == {} - - examples.v2.src.fastapi.FAKE_DB.reset_mock() - - -def verify_mock_delete_request_to_delete_user() -> None: - if TYPE_CHECKING: - examples.v2.src.fastapi.FAKE_DB = MagicMock() - - assert len(examples.v2.src.fastapi.FAKE_DB.mock_calls) == 2 - - examples.v2.src.fastapi.FAKE_DB.__delitem__.assert_called_once() - args, kwargs = examples.v2.src.fastapi.FAKE_DB.__delitem__.call_args - assert len(args) == 1 - assert isinstance(args[0], int) - assert kwargs == {} - - examples.v2.src.fastapi.FAKE_DB.__contains__.assert_called_once() - args, kwargs = examples.v2.src.fastapi.FAKE_DB.__contains__.call_args - assert len(args) == 1 - assert isinstance(args[0], int) - assert kwargs == {} From c781455170d0bdae356c77e6b0aac36a7a308ffd Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 26 Aug 2025 19:44:24 +1000 Subject: [PATCH 0973/1376] feat(ffi): upgrade lib to 0.4.28 Signed-off-by: JP-Ellis --- pact-python-ffi/src/pact_ffi/__init__.py | 498 +++++++++++------------ 1 file changed, 249 insertions(+), 249 deletions(-) diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index 06a8305bc..794e6649d 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -418,7 +418,7 @@ class InteractionHandle: Handle to a HTTP Interaction. [Rust - `InteractionHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/struct.InteractionHandle.html) + `InteractionHandle`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/mock_server/handles/struct.InteractionHandle.html) """ def __init__(self, ref: int) -> None: @@ -877,7 +877,7 @@ class PactHandle: Handle to a Pact. [Rust - `PactHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/struct.PactHandle.html) + `PactHandle`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/mock_server/handles/struct.PactHandle.html) """ def __init__(self, ref: int) -> None: @@ -1512,7 +1512,7 @@ class VerifierHandle: """ Handle to a Verifier. - [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/verifier/handle/struct.VerifierHandle.html) + [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/verifier/handle/struct.VerifierHandle.html) """ def __init__(self, ref: cffi.FFI.CData) -> None: @@ -1548,7 +1548,7 @@ class ExpressionValueType(Enum): """ Expression Value Type. - [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/expressions/enum.ExpressionValueType.html) + [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/models/expressions/enum.ExpressionValueType.html) """ UNKNOWN = lib.ExpressionValueType_Unknown @@ -1575,7 +1575,7 @@ class GeneratorCategory(Enum): """ Generator Category. - [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/generators/enum.GeneratorCategory.html) + [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/models/generators/enum.GeneratorCategory.html) """ METHOD = lib.GeneratorCategory_METHOD @@ -1603,7 +1603,7 @@ class InteractionPart(Enum): """ Interaction Part. - [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/handles/enum.InteractionPart.html) + [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/mock_server/handles/enum.InteractionPart.html) """ REQUEST = lib.InteractionPart_Request @@ -1649,7 +1649,7 @@ class MatchingRuleCategory(Enum): """ Matching Rule Category. - [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) + [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) """ METHOD = lib.MatchingRuleCategory_METHOD @@ -1678,7 +1678,7 @@ class PactSpecification(Enum): """ Pact Specification. - [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/models/pact_specification/enum.PactSpecification.html) + [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/models/pact_specification/enum.PactSpecification.html) """ UNKNOWN = lib.PactSpecification_Unknown @@ -1731,7 +1731,7 @@ class _StringResult(Enum): """ Internal enum from Pact FFI. - [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/mock_server/enum.StringResult.html) + [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/mock_server/enum.StringResult.html) """ FAILED = lib.StringResult_Failed @@ -1896,7 +1896,7 @@ def version() -> str: """ Return the version of the pact_ffi library. - [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_version) + [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_version) Returns: The version of the pact_ffi library as a string, in the form of `x.y.z`. @@ -1916,7 +1916,7 @@ def init(log_env_var: str) -> None: tracing subscriber. [Rust - `pactffi_init`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_init) + `pactffi_init`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_init) # Safety @@ -1933,7 +1933,7 @@ def init_with_log_level(level: str = "INFO") -> None: tracing subscriber. [Rust - `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_init_with_log_level) + `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_init_with_log_level) Args: level: @@ -1953,7 +1953,7 @@ def enable_ansi_support() -> None: On non-Windows platforms, this function is a no-op. [Rust - `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_enable_ansi_support) + `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_enable_ansi_support) # Safety @@ -1971,7 +1971,7 @@ def log_message( Log using the shared core logging facility. [Rust - `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_message) + `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_log_message) This is useful for callers to have a single set of logs. @@ -2001,7 +2001,7 @@ def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: Get an iterator over mismatches. [Rust - `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_get_iter) + `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatches_get_iter) """ raise NotImplementedError @@ -2010,7 +2010,7 @@ def mismatches_delete(mismatches: Mismatches) -> None: """ Delete mismatches. - [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_delete) + [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatches_delete) """ raise NotImplementedError @@ -2019,7 +2019,7 @@ def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: """ Get the next mismatch from a mismatches iterator. - [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_iter_next) + [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatches_iter_next) Returns a null pointer if no mismatches remain. """ @@ -2030,7 +2030,7 @@ def mismatches_iter_delete(iter: MismatchesIterator) -> None: """ Delete a mismatches iterator when you're done with it. - [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatches_iter_delete) + [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatches_iter_delete) """ raise NotImplementedError @@ -2039,7 +2039,7 @@ def mismatch_to_json(mismatch: Mismatch) -> str: """ Get a JSON representation of the mismatch. - [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_to_json) + [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatch_to_json) """ raise NotImplementedError @@ -2048,7 +2048,7 @@ def mismatch_type(mismatch: Mismatch) -> str: """ Get the type of a mismatch. - [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_type) + [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatch_type) """ raise NotImplementedError @@ -2057,7 +2057,7 @@ def mismatch_summary(mismatch: Mismatch) -> str: """ Get a summary of a mismatch. - [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_summary) + [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatch_summary) """ raise NotImplementedError @@ -2066,7 +2066,7 @@ def mismatch_description(mismatch: Mismatch) -> str: """ Get a description of a mismatch. - [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_description) + [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatch_description) """ raise NotImplementedError @@ -2075,7 +2075,7 @@ def mismatch_ansi_description(mismatch: Mismatch) -> str: """ Get an ANSI-compatible description of a mismatch. - [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mismatch_ansi_description) + [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatch_ansi_description) """ raise NotImplementedError @@ -2085,7 +2085,7 @@ def get_error_message(length: int = 1024) -> str | None: Provide the error message from `LAST_ERROR` to the calling C code. [Rust - `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_get_error_message) + `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_get_error_message) This function should be called after any other function in the pact_matching FFI indicates a failure with its own error message, if the caller wants to @@ -2139,7 +2139,7 @@ def log_to_stdout(level_filter: LevelFilter) -> int: """ Convenience function to direct all logging to stdout. - [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_stdout) + [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_log_to_stdout) """ raise NotImplementedError @@ -2149,7 +2149,7 @@ def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: Convenience function to direct all logging to stderr. [Rust - `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_stderr) + `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_log_to_stderr) Args: level_filter: @@ -2174,7 +2174,7 @@ def log_to_file(file_name: str, level_filter: LevelFilter) -> int: Convenience function to direct all logging to a file. [Rust - `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_file) + `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_log_to_file) # Safety @@ -2188,7 +2188,7 @@ def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: """ Convenience function to direct all logging to a task local memory buffer. - [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_log_to_buffer) + [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_log_to_buffer) Raises: RuntimeError: @@ -2206,7 +2206,7 @@ def logger_init() -> None: """ Initialize the FFI logger with no sinks. - [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_init) + [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_logger_init) This initialized logger does nothing until `pactffi_logger_apply` has been called. @@ -2228,7 +2228,7 @@ def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: Attach an additional sink to the thread-local logger. [Rust - `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_attach_sink) + `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_logger_attach_sink) This logger does nothing until `pactffi_logger_apply` has been called. @@ -2256,7 +2256,7 @@ def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: any other log function). - `-3`: The sink specifier was not UTF-8 encoded. - `-4`: The sink type specified is not a known type (known types: "stdout", - "stderr", or "file /some/path"). + "stderr", "buffer", or "file /some/path"). - `-5`: No file path was specified in a file-type sink specification. - `-6`: Opening a sink to the specified file path failed (check permissions). @@ -2275,7 +2275,7 @@ def logger_apply() -> int: Apply the previously configured sinks and levels to the program. [Rust - `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_logger_apply) + `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_logger_apply) If no sinks have been setup, will set the log level to info and the target to standard out. @@ -2291,7 +2291,7 @@ def fetch_log_buffer(log_id: str) -> str: Fetch the in-memory logger buffer contents. [Rust - `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_fetch_log_buffer) + `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_fetch_log_buffer) This will only have any contents if the `buffer` sink has been configured to log to. The contents will be allocated on the heap and will need to be freed @@ -2317,7 +2317,7 @@ def parse_pact_json(json: str) -> Pact: Parses the provided JSON into a Pact model. [Rust - `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_parse_pact_json) + `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_parse_pact_json) The returned Pact model must be freed with the `pactffi_pact_model_delete` function when no longer needed. @@ -2334,7 +2334,7 @@ def pact_model_delete(pact: Pact) -> None: """ Frees the memory used by the Pact model. - [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_model_delete) + [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_model_delete) """ raise NotImplementedError @@ -2344,7 +2344,7 @@ def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: Returns an iterator over all the interactions in the Pact. [Rust - `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_model_interaction_iterator) + `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_model_interaction_iterator) The iterator will have to be deleted using the `pactffi_pact_interaction_iter_delete` function. The iterator will contain a @@ -2363,7 +2363,7 @@ def pact_spec_version(pact: Pact) -> PactSpecification: """ Returns the Pact specification enum that the Pact is for. - [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_spec_version) + [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_spec_version) """ raise NotImplementedError @@ -2372,7 +2372,7 @@ def pact_interaction_delete(interaction: PactInteraction) -> None: """ Frees the memory used by the Pact interaction model. - [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_delete) + [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_delete) """ raise NotImplementedError @@ -2381,7 +2381,7 @@ def async_message_new() -> AsynchronousMessage: """ Get a mutable pointer to a newly-created default message on the heap. - [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_new) + [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_new) # Safety @@ -2398,7 +2398,7 @@ def async_message_delete(message: AsynchronousMessage) -> None: """ Destroy the `AsynchronousMessage` being pointed to. - [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_delete) + [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_delete) """ lib.pactffi_async_message_delete(message._ptr) @@ -2408,7 +2408,7 @@ def async_message_get_contents(message: AsynchronousMessage) -> MessageContents Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. [Rust - `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents) + `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_contents) If the message contents are missing, this function will return `None`. """ @@ -2427,7 +2427,7 @@ def async_message_generate_contents( contents as would be received by the consumer. [Rust - `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_generate_contents) + `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_generate_contents) If the message contents are missing, this function will return `None`. """ @@ -2441,7 +2441,7 @@ def async_message_get_contents_str(message: AsynchronousMessage) -> str: """ Get the message contents of an `AsynchronousMessage` in string form. - [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_str) + [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_contents_str) # Safety @@ -2468,7 +2468,7 @@ def async_message_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_contents_str) + `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_set_contents_str) - `message` - the message to set the contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -2496,7 +2496,7 @@ def async_message_get_contents_length(message: AsynchronousMessage) -> int: Get the length of the contents of a `AsynchronousMessage`. [Rust - `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_length) + `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_contents_length) # Safety @@ -2515,7 +2515,7 @@ def async_message_get_contents_bin(message: AsynchronousMessage) -> str: Get the contents of an `AsynchronousMessage` as bytes. [Rust - `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_contents_bin) + `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_contents_bin) # Safety @@ -2542,7 +2542,7 @@ def async_message_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_contents_bin) + `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_set_contents_bin) * `message` - the message to set the contents for * `contents` - pointer to contents to copy from @@ -2569,7 +2569,7 @@ def async_message_get_description(message: AsynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_description) + `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_description) Raises: RuntimeError: @@ -2589,7 +2589,7 @@ def async_message_set_description( """ Write the `description` field on the `AsynchronousMessage`. - [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_set_description) + [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_set_description) # Safety @@ -2614,7 +2614,7 @@ def async_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_provider_state) + `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_provider_state) Raises: RuntimeError: @@ -2633,7 +2633,7 @@ def async_message_get_provider_state_iter( """ Get an iterator over provider states. - [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) + [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) # Safety @@ -2648,7 +2648,7 @@ def consumer_get_name(consumer: Consumer) -> str: r""" Get a copy of this consumer's name. - [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_consumer_get_name) + [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_consumer_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -2694,7 +2694,7 @@ def pact_get_consumer(pact: Pact) -> Consumer: `pactffi_pact_consumer_delete` when no longer required. [Rust - `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_get_consumer) + `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_get_consumer) # Errors @@ -2708,7 +2708,7 @@ def pact_consumer_delete(consumer: Consumer) -> None: """ Frees the memory used by the Pact consumer. - [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_consumer_delete) + [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_consumer_delete) """ raise NotImplementedError @@ -2724,7 +2724,7 @@ def message_contents_delete(contents: MessageContents) -> None: Deleting a message content which is associated with an interaction will result in undefined behaviour. - [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_delete) + [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_delete) """ lib.pactffi_message_contents_delete(contents._ptr) @@ -2733,7 +2733,7 @@ def message_contents_get_contents_str(contents: MessageContents) -> str | None: """ Get the message contents in string form. - [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_str) + [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_get_contents_str) If the message has no contents or contain invalid UTF-8 characters, this function will return `None`. @@ -2753,7 +2753,7 @@ def message_contents_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_set_contents_str) + `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_set_contents_str) * `contents` - the message contents to set the contents for * `contents_str` - pointer to contents to copy from. Must be a valid @@ -2780,7 +2780,7 @@ def message_contents_get_contents_length(contents: MessageContents) -> int: """ Get the length of the message contents. - [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_length) + [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_get_contents_length) If the message has not contents, this function will return 0. """ @@ -2792,7 +2792,7 @@ def message_contents_get_contents_bin(contents: MessageContents) -> bytes | None Get the contents of a message as a pointer to an array of bytes. [Rust - `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_contents_bin) + `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_get_contents_bin) If the message has no contents, this function will return `None`. """ @@ -2815,7 +2815,7 @@ def message_contents_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_set_contents_bin) + `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_set_contents_bin) * `message` - the message contents to set the contents for * `contents_bin` - pointer to contents to copy from @@ -2844,7 +2844,7 @@ def message_contents_get_metadata_iter( Get an iterator over the metadata of a message. [Rust - `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) + `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) # Safety @@ -2873,7 +2873,7 @@ def message_contents_get_matching_rule_iter( Get an iterator over the matching rules for a category of a message. [Rust - `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) + `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -2915,7 +2915,7 @@ def request_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP request. - [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) + [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -2952,7 +2952,7 @@ def response_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP response. - [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) + [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -2990,7 +2990,7 @@ def message_contents_get_generators_iter( Get an iterator over the generators for a category of a message. [Rust - `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_contents_get_generators_iter) + `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_get_generators_iter) # Safety @@ -3016,7 +3016,7 @@ def request_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP request. [Rust - `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_request_contents_get_generators_iter) + `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_request_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -3041,7 +3041,7 @@ def response_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP response. [Rust - `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_contents_get_generators_iter) + `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_response_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -3066,7 +3066,7 @@ def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: any generator. [Rust - `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_parse_matcher_definition) + `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_parse_matcher_definition) The following are examples of matching rule definitions: @@ -3102,7 +3102,7 @@ def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: using the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_error) + `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matcher_definition_error) """ raise NotImplementedError @@ -3116,7 +3116,7 @@ def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_value) + `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matcher_definition_value) Note that different expressions values can have types other than a string. Use `pactffi_matcher_definition_value_type` to get the actual type of the @@ -3130,7 +3130,7 @@ def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: """ Frees the memory used by the result of parsing the matching definition expression. - [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_delete) + [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matcher_definition_delete) """ raise NotImplementedError @@ -3143,7 +3143,7 @@ def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Ge NULL pointer, otherwise returns the generator as a pointer. [Rust - `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_generator) + `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matcher_definition_generator) The generator pointer will be a valid pointer as long as `pactffi_matcher_definition_delete` has not been called on the definition. @@ -3162,7 +3162,7 @@ def matcher_definition_value_type( If there was an error parsing the expression, it will return Unknown. [Rust - `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_value_type) + `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matcher_definition_value_type) """ raise NotImplementedError @@ -3171,7 +3171,7 @@ def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: """ Free the iterator when you're done using it. - [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_iter_delete) + [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_iter_delete) """ raise NotImplementedError @@ -3186,7 +3186,7 @@ def matcher_definition_iter( `pactffi_matching_rule_iter_delete` function once done with it. [Rust - `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matcher_definition_iter) + `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matcher_definition_iter) If there was an error parsing the expression, this function will return a NULL pointer. @@ -3202,7 +3202,7 @@ def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: deleted but will be cleaned up when the iterator is deleted. [Rust - `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_iter_next) + `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_iter_next) Will return a NULL pointer when the iterator has advanced past the end of the list. @@ -3224,7 +3224,7 @@ def matching_rule_id(rule_result: MatchingRuleResult) -> int: Return the ID of the matching rule. [Rust - `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_id) + `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_id) The ID corresponds to the following rules: @@ -3270,7 +3270,7 @@ def matching_rule_value(rule_result: MatchingRuleResult) -> str: pointer. [Rust - `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_value) + `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_value) The associated values for the rules are: @@ -3318,7 +3318,7 @@ def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: Will return a NULL pointer if the matching rule result was a reference. [Rust - `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_pointer) + `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_pointer) # Safety @@ -3336,7 +3336,7 @@ def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: structure. I.e., [Rust - `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_reference_name) + `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_reference_name) ```json { @@ -3363,7 +3363,7 @@ def validate_datetime(value: str, format: str) -> None: Validates the date/time value against the date/time format string. [Rust - `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_validate_datetime) + `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_validate_datetime) Raises: ValueError: @@ -3390,7 +3390,7 @@ def generator_to_json(generator: Generator) -> str: Get the JSON form of the generator. [Rust - `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_to_json) + `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generator_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -3413,7 +3413,7 @@ def generator_generate_string(generator: Generator, context_json: str) -> str: function). [Rust - `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_generate_string) + `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generator_generate_string) If anything goes wrong, it will return a NULL pointer. """ @@ -3436,7 +3436,7 @@ def generator_generate_integer(generator: Generator, context_json: str) -> int: should be the values returned from the Provider State callback function). [Rust - `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generator_generate_integer) + `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generator_generate_integer) If anything goes wrong or the generator is not a type that can generate an integer value, it will return a zero value. @@ -3452,7 +3452,7 @@ def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_delete) + `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generators_iter_delete) """ lib.pactffi_generators_iter_delete(iter._ptr) @@ -3462,7 +3462,7 @@ def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePa Get the next path and generator out of the iterator, if possible. [Rust - `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_next) + `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generators_iter_next) The returned pointer must be deleted with `pactffi_generator_iter_pair_delete`. @@ -3482,7 +3482,7 @@ def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: Free a pair of key and value returned from `pactffi_generators_iter_next`. [Rust - `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generators_iter_pair_delete) + `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generators_iter_pair_delete) """ lib.pactffi_generators_iter_pair_delete(pair._ptr) @@ -3491,7 +3491,7 @@ def sync_http_new() -> SynchronousHttp: """ Get a mutable pointer to a newly-created default interaction on the heap. - [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_new) + [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_new) # Safety @@ -3509,7 +3509,7 @@ def sync_http_delete(interaction: SynchronousHttp) -> None: Destroy the `SynchronousHttp` interaction being pointed to. [Rust - `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_delete) + `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_delete) """ lib.pactffi_sync_http_delete(interaction) @@ -3519,7 +3519,7 @@ def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: Get the request of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request) + `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_request) # Safety @@ -3539,7 +3539,7 @@ def sync_http_get_request_contents(interaction: SynchronousHttp) -> str | None: Get the request contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents) + `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_request_contents) Note that this function will return `None` if either the body is missing or is `null`. @@ -3559,7 +3559,7 @@ def sync_http_set_request_contents( Sets the request contents of the interaction. [Rust - `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_request_contents) + `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_set_request_contents) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -3587,7 +3587,7 @@ def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: Get the length of the request contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) + `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) This function will return 0 if the body is missing. """ @@ -3599,7 +3599,7 @@ def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes | Get the request contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) + `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) Note that this function will return `None` if either the body is missing or is `null`. @@ -3623,7 +3623,7 @@ def sync_http_set_request_contents_bin( Sets the request contents of the interaction as an array of bytes. [Rust - `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) + `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from @@ -3650,7 +3650,7 @@ def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: Get the response of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response) + `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_response) # Safety @@ -3670,7 +3670,7 @@ def sync_http_get_response_contents(interaction: SynchronousHttp) -> str | None: Get the response contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents) + `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_response_contents) Note that this function will return `None` if either the body is missing or is `null`. @@ -3690,7 +3690,7 @@ def sync_http_set_response_contents( Sets the response contents of the interaction. [Rust - `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_response_contents) + `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_set_response_contents) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -3718,7 +3718,7 @@ def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: Get the length of the response contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) + `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) This function will return 0 if the body is missing. """ @@ -3730,7 +3730,7 @@ def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes | Get the response contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) + `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) Note that this function will return `None` if either the body is missing or is `null`. @@ -3754,7 +3754,7 @@ def sync_http_set_response_contents_bin( Sets the response contents of the `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) + `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from @@ -3781,7 +3781,7 @@ def sync_http_get_description(interaction: SynchronousHttp) -> str: Get a copy of the description. [Rust - `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_description) + `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_description) Raises: RuntimeError: @@ -3799,7 +3799,7 @@ def sync_http_set_description(interaction: SynchronousHttp, description: str) -> Write the `description` field on the `SynchronousHttp`. [Rust - `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_set_description) + `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_set_description) # Safety @@ -3824,7 +3824,7 @@ def sync_http_get_provider_state( Get a copy of the provider state at the given index from this interaction. [Rust - `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_provider_state) + `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_provider_state) # Safety @@ -3850,7 +3850,7 @@ def sync_http_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) + `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) # Safety @@ -3879,7 +3879,7 @@ def pact_interaction_as_synchronous_http( longer required. [Rust - `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) + `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) # Safety This function is safe as long as the interaction pointer is a valid pointer. @@ -3901,7 +3901,7 @@ def pact_interaction_as_asynchronous_message( no longer required. [Rust - `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) + `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) Note that if the interaction is a V3 `Message`, it will be converted to a V4 `AsynchronousMessage` before being returned. @@ -3926,7 +3926,7 @@ def pact_interaction_as_synchronous_message( no longer required. [Rust - `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) + `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) # Safety This function is safe as long as the interaction pointer is a valid pointer. @@ -3941,7 +3941,7 @@ def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> Asynchronous Get the next asynchronous message from the iterator. [Rust - `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_async_message_iter_next) + `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_async_message_iter_next) Raises: StopIteration: @@ -3958,7 +3958,7 @@ def pact_async_message_iter_delete(iter: PactAsyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_async_message_iter_delete) + `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_async_message_iter_delete) """ lib.pactffi_pact_async_message_iter_delete(iter._ptr) @@ -3968,7 +3968,7 @@ def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMes Get the next synchronous request/response message from the V4 pact. [Rust - `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_message_iter_next) + `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_sync_message_iter_next) Raises: StopIteration: @@ -3985,7 +3985,7 @@ def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) + `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) """ lib.pactffi_pact_sync_message_iter_delete(iter._ptr) @@ -3995,7 +3995,7 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: Get the next synchronous HTTP request/response interaction from the V4 pact. [Rust - `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_http_iter_next) + `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_sync_http_iter_next) Raises: StopIteration: @@ -4012,7 +4012,7 @@ def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) + `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) """ lib.pactffi_pact_sync_http_iter_delete(iter._ptr) @@ -4022,7 +4022,7 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction Get the next interaction from the pact. [Rust - `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_iter_next) + `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_iter_next) Raises: StopIteration: @@ -4040,7 +4040,7 @@ def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_interaction_iter_delete) + `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_iter_delete) """ lib.pactffi_pact_interaction_iter_delete(iter._ptr) @@ -4050,7 +4050,7 @@ def matching_rule_to_json(rule: MatchingRule) -> str: Get the JSON form of the matching rule. [Rust - `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rule_to_json) + `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -4067,7 +4067,7 @@ def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_delete) + `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rules_iter_delete) """ lib.pactffi_matching_rules_iter_delete(iter._ptr) @@ -4079,7 +4079,7 @@ def matching_rules_iter_next( Get the next path and matching rule out of the iterator, if possible. [Rust - `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_next) + `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rules_iter_next) The returned pointer must be deleted with `pactffi_matching_rules_iter_pair_delete`. @@ -4101,7 +4101,7 @@ def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) + `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) """ lib.pactffi_matching_rules_iter_pair_delete(pair._ptr) @@ -4111,7 +4111,7 @@ def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: Get the next value from the iterator. [Rust - `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_iter_next) + `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_iter_next) # Safety @@ -4132,7 +4132,7 @@ def provider_state_iter_delete(iter: ProviderStateIterator) -> None: Delete the iterator. [Rust - `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_iter_delete) + `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_iter_delete) """ lib.pactffi_provider_state_iter_delete(iter._ptr) @@ -4142,7 +4142,7 @@ def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadata Get the next key and value out of the iterator, if possible. [Rust - `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_iter_next) + `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_metadata_iter_next) The returned pointer must be deleted with `pactffi_message_metadata_pair_delete`. @@ -4168,7 +4168,7 @@ def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: Free the metadata iterator when you're done using it. [Rust - `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_iter_delete) + `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_metadata_iter_delete) """ lib.pactffi_message_metadata_iter_delete(iter._ptr) @@ -4178,7 +4178,7 @@ def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_message_metadata_pair_delete) + `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_metadata_pair_delete) """ lib.pactffi_message_metadata_pair_delete(pair._ptr) @@ -4188,7 +4188,7 @@ def provider_get_name(provider: Provider) -> str: Get a copy of this provider's name. [Rust - `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_get_name) + `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -4234,7 +4234,7 @@ def pact_get_provider(pact: Pact) -> Provider: `pactffi_pact_provider_delete` when no longer required. [Rust - `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_get_provider) + `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_get_provider) # Errors @@ -4249,7 +4249,7 @@ def pact_provider_delete(provider: Provider) -> None: Frees the memory used by the Pact provider. [Rust - `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_provider_delete) + `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_provider_delete) """ raise NotImplementedError @@ -4259,7 +4259,7 @@ def provider_state_get_name(provider_state: ProviderState) -> str | None: Get the name of the provider state as a string. [Rust - `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_get_name) + `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_get_name) Raises: RuntimeError: @@ -4279,7 +4279,7 @@ def provider_state_get_param_iter( Get an iterator over the params of a provider state. [Rust - `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_get_param_iter) + `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_get_param_iter) # Safety @@ -4307,7 +4307,7 @@ def provider_state_param_iter_next( Get the next key and value out of the iterator, if possible. [Rust - `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_iter_next) + `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_param_iter_next) # Safety @@ -4328,7 +4328,7 @@ def provider_state_delete(provider_state: ProviderState) -> None: Free the provider state when you're done using it. [Rust - `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_delete) + `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_delete) """ raise NotImplementedError @@ -4338,7 +4338,7 @@ def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: Free the provider state param iterator when you're done using it. [Rust - `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_iter_delete) + `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_param_iter_delete) """ lib.pactffi_provider_state_param_iter_delete(iter._ptr) @@ -4348,7 +4348,7 @@ def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: Free a pair of key and value. [Rust - `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_provider_state_param_pair_delete) + `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_param_pair_delete) """ lib.pactffi_provider_state_param_pair_delete(pair._ptr) @@ -4358,7 +4358,7 @@ def sync_message_new() -> SynchronousMessage: Get a mutable pointer to a newly-created default message on the heap. [Rust - `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_new) + `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_new) # Safety @@ -4376,7 +4376,7 @@ def sync_message_delete(message: SynchronousMessage) -> None: Destroy the `Message` being pointed to. [Rust - `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_delete) + `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_delete) """ lib.pactffi_sync_message_delete(message._ptr) @@ -4386,7 +4386,7 @@ def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: Get the request contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) + `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) # Safety @@ -4413,7 +4413,7 @@ def sync_message_set_request_contents_str( Sets the request contents of the message. [Rust - `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) + `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) - `message` - the message to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -4441,7 +4441,7 @@ def sync_message_get_request_contents_length(message: SynchronousMessage) -> int Get the length of the request contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) + `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) # Safety @@ -4460,7 +4460,7 @@ def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes: Get the request contents of a `SynchronousMessage` as a bytes. [Rust - `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) + `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) # Safety @@ -4487,7 +4487,7 @@ def sync_message_set_request_contents_bin( Sets the request contents of the message as an array of bytes. [Rust - `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) + `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) * `message` - the message to set the request contents for * `contents` - pointer to contents to copy from @@ -4514,7 +4514,7 @@ def sync_message_get_request_contents(message: SynchronousMessage) -> MessageCon Get the request contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_request_contents) + `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_request_contents) # Safety @@ -4541,7 +4541,7 @@ def sync_message_generate_request_contents( contents as would be received by the consumer. [Rust - `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_generate_request_contents) + `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_generate_request_contents) Raises: RuntimeError: @@ -4559,7 +4559,7 @@ def sync_message_get_number_responses(message: SynchronousMessage) -> int: Get the number of response messages in the `SynchronousMessage`. [Rust - `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_number_responses) + `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_number_responses) If the message is null, this function will return 0. """ @@ -4574,7 +4574,7 @@ def sync_message_get_response_contents_str( Get the response contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) + `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) # Safety @@ -4607,7 +4607,7 @@ def sync_message_set_response_contents_str( with default values. [Rust - `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) + `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response. @@ -4639,7 +4639,7 @@ def sync_message_get_response_contents_length( Get the length of the response contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) + `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) # Safety @@ -4661,7 +4661,7 @@ def sync_message_get_response_contents_bin( Get the response contents of a `SynchronousMessage` as bytes. [Rust - `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) + `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) # Safety @@ -4692,7 +4692,7 @@ def sync_message_set_response_contents_bin( responses will be padded with default values. [Rust - `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) + `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response @@ -4723,7 +4723,7 @@ def sync_message_get_response_contents( Get the response contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_response_contents) + `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_response_contents) # Safety @@ -4752,7 +4752,7 @@ def sync_message_generate_response_contents( received by the consumer. [Rust - `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_generate_response_contents) + `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_generate_response_contents) Raises: RuntimeError: @@ -4770,7 +4770,7 @@ def sync_message_get_description(message: SynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_description) + `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_description) Raises: RuntimeError: @@ -4788,7 +4788,7 @@ def sync_message_set_description(message: SynchronousMessage, description: str) Write the `description` field on the `SynchronousMessage`. [Rust - `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_set_description) + `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_set_description) # Safety @@ -4813,7 +4813,7 @@ def sync_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_provider_state) + `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_provider_state) # Safety @@ -4839,7 +4839,7 @@ def sync_message_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) + `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) # Safety @@ -4861,7 +4861,7 @@ def string_delete(string: OwnedString) -> None: Delete a string previously returned by this FFI. [Rust - `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_string_delete) + `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_string_delete) """ lib.pactffi_string_delete(string._ptr) @@ -4876,7 +4876,7 @@ def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: the mock server is returned. [Rust - `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server) + `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_create_mock_server) * `pact_str` - Pact JSON * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:80) @@ -4912,7 +4912,7 @@ def get_tls_ca_certificate() -> OwnedString: Fetch the CA Certificate used to generate the self-signed certificate. [Rust - `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_get_tls_ca_certificate) + `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_get_tls_ca_certificate) **NOTE:** The string for the result is allocated on the heap, and will have to be freed by the caller using pactffi_string_delete. @@ -4933,7 +4933,7 @@ def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) - operating system. The port of the mock server is returned. [Rust - `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server_for_pact) + `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_create_mock_server_for_pact) * `pact` - Handle to a Pact model created with created with `pactffi_new_pact`. @@ -4976,7 +4976,7 @@ def create_mock_server_for_transport( Create a mock server for the provided Pact handle and transport. [Rust - `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_create_mock_server_for_transport) + `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_create_mock_server_for_transport) Args: pact: @@ -5038,7 +5038,7 @@ def mock_server_matched(mock_server_handle: PactServerHandle) -> bool: if any request has not been successfully matched, or the method panics. [Rust - `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_matched) + `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mock_server_matched) """ return lib.pactffi_mock_server_matched(mock_server_handle._ref) @@ -5050,7 +5050,7 @@ def mock_server_mismatches( External interface to get all the mismatches from a mock server. [Rust - `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_mismatches) + `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mock_server_mismatches) # Errors @@ -5077,7 +5077,7 @@ def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: and cleanup any memory allocated for it. [Rust - `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_cleanup_mock_server) + `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_cleanup_mock_server) Args: mock_server_handle: @@ -5106,7 +5106,7 @@ def write_pact_file( directory to write the file to is passed as the second parameter. [Rust - `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_write_pact_file) + `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_write_pact_file) Args: mock_server_handle: @@ -5158,7 +5158,7 @@ def mock_server_logs(mock_server_handle: PactServerHandle) -> str: started. [Rust - `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_mock_server_logs) + `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mock_server_logs) Raises: RuntimeError: @@ -5182,7 +5182,7 @@ def generate_datetime_string(format: str) -> StringResult: string needs to be freed with the `pactffi_string_delete` function [Rust - `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generate_datetime_string) + `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generate_datetime_string) # Safety @@ -5199,7 +5199,7 @@ def check_regex(regex: str, example: str) -> bool: Checks that the example string matches the given regex. [Rust - `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_check_regex) + `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_check_regex) # Safety @@ -5218,7 +5218,7 @@ def generate_regex_value(regex: str) -> StringResult: `pactffi_string_delete` function. [Rust - `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_generate_regex_value) + `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generate_regex_value) # Safety @@ -5233,7 +5233,7 @@ def free_string(s: str) -> None: [DEPRECATED] Frees the memory allocated to a string by another function. [Rust - `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_free_string) + `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_free_string) This function is deprecated. Use `pactffi_string_delete` instead. @@ -5255,7 +5255,7 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle: Creates a new Pact model and returns a handle to it. [Rust - `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_pact) + `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_new_pact) Args: consumer_name: @@ -5296,7 +5296,7 @@ def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: will result in that interaction being replaced with the new one. [Rust - `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_interaction) + `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_new_interaction) Args: pact: @@ -5324,7 +5324,7 @@ def new_message_interaction(pact: PactHandle, description: str) -> InteractionHa will result in that interaction being replaced with the new one. [Rust - `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_message_interaction) + `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_new_message_interaction) Args: pact: @@ -5355,7 +5355,7 @@ def new_sync_message_interaction( will result in that interaction being replaced with the new one. [Rust - `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_new_sync_message_interaction) + `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_new_sync_message_interaction) Args: pact: @@ -5380,7 +5380,7 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> None: Sets the description for the Interaction. [Rust - `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_upon_receiving) + `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_upon_receiving) This function @@ -5421,7 +5421,7 @@ def given(interaction: InteractionHandle, description: str) -> None: Adds a provider state to the Interaction. [Rust - `pactffi_given`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given) + `pactffi_given`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_given) Args: interaction: @@ -5448,7 +5448,7 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> Non used with V4 interactions. [Rust - `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_interaction_test_name) + `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_interaction_test_name) Args: interaction: @@ -5495,7 +5495,7 @@ def given_with_param( be parsed as JSON. [Rust - `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given_with_param) + `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_given_with_param) Args: interaction: @@ -5537,7 +5537,7 @@ def given_with_params( with a `value` key. [Rust - `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_given_with_params) + `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_given_with_params) Args: interaction: @@ -5576,7 +5576,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None Configures the request for the Interaction. [Rust - `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_request) + `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_request) Args: interaction: @@ -5590,7 +5590,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md) which allows regex patterns. For examples: ```json @@ -5625,7 +5625,7 @@ def with_query_parameter_v2( Configures a query parameter for the Interaction. [Rust - `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_query_parameter_v2) + `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_query_parameter_v2) To setup a query parameter with multiple values, you can either call this function multiple times with a different index value: @@ -5662,7 +5662,7 @@ def with_query_parameter_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md) If you want the matching rules to apply to all values (and not just the one with the given index), make sure to set the value to be an array. @@ -5714,7 +5714,7 @@ def with_query_parameter_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -5736,7 +5736,7 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None: Sets the specification version for a given Pact model. [Rust - `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_specification) + `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_specification) Args: pact: @@ -5760,7 +5760,7 @@ def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: Fetches the Pact specification version for the given Pact model. [Rust - `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_handle_get_pact_spec_version) + `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_handle_get_pact_spec_version) Args: handle: @@ -5782,18 +5782,18 @@ def with_pact_metadata( Sets the additional metadata on the Pact file. Common uses are to add the client library details such as the name and - version Returns false if the interaction or Pact can't be modified (i.e. the - mock server for it has already started) + version. Returns false if the interaction or Pact can't be modified (i.e. + the mock server for it has already started) or the namespace is readonly. [Rust - `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_pact_metadata) + `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_pact_metadata) Args: pact: Handle to a Pact model namespace: - The top level metadat key to set any key values on + the top level metadata key to set any key values on name: The key to set @@ -5852,7 +5852,7 @@ def with_metadata( ``` See - [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md) # Note @@ -5901,7 +5901,7 @@ def with_header_v2( r""" Configures a header for the Interaction. - [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_header_v2) + [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_header_v2) To setup a header with multiple values, you can either call this function multiple times with a different index value: @@ -5939,7 +5939,7 @@ def with_header_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -5961,7 +5961,7 @@ def with_header_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -5993,7 +5993,7 @@ def set_header( and generators can not be configured with it. [Rust - `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_header) + `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_set_header) If matching rules are required to be set, use `pactffi_with_header_v2`. @@ -6031,7 +6031,7 @@ def response_status(interaction: InteractionHandle, status: int) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_status) + `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_response_status) Args: interaction: @@ -6055,7 +6055,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_response_status_v2) + `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_response_status_v2) To include matching rules for the status (only statusCode or integer really makes sense to use), include the matching rule JSON format with the value as @@ -6074,7 +6074,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -6085,7 +6085,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -6109,7 +6109,7 @@ def with_body( Adds the body for the interaction. [Rust - `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_body) + `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_body) Returns false if the interaction or Pact can't be modified (i.e. the mock server for it has already started) @@ -6144,7 +6144,7 @@ def with_body( body: The body contents. For JSON payloads, matching rules can be embedded in the body. See - [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.22/rust/pact_ffi/IntegrationJson.md). + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -6171,7 +6171,7 @@ def with_binary_body( Adds the body for the interaction. [Rust - `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_binary_body) + `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_binary_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -6232,7 +6232,7 @@ def with_binary_file( already started) [Rust - `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_binary_file) + `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_binary_file) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -6279,7 +6279,7 @@ def with_matching_rules( Add matching rules to the interaction. [Rust - `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_matching_rules) + `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_matching_rules) This function can be called multiple times, in which case the matching rules will be merged. @@ -6317,7 +6317,7 @@ def with_generators( Add generators to the interaction. [Rust - `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_generators) + `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_generators) This function can be called multiple times, in which case the generators will be combined (provide they don't clash). @@ -6365,7 +6365,7 @@ def with_multipart_file_v2( # noqa: PLR0913 already started) or an error occurs. [Rust - `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_multipart_file_v2) + `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_multipart_file_v2) This function can be called multiple times. In that case, each subsequent call will be appended to the existing multipart body as a new part. @@ -6419,7 +6419,7 @@ def with_multipart_file( already started) or an error occurs. [Rust - `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_with_multipart_file) + `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_multipart_file) * `interaction` - Interaction handle to set the body for. * `part` - Request or response part. @@ -6456,7 +6456,7 @@ def set_key(interaction: InteractionHandle, key: str | None) -> None: Sets the key attribute for the interaction. [Rust - `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_key) + `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_set_key) Args: interaction: @@ -6484,7 +6484,7 @@ def set_pending(interaction: InteractionHandle, *, pending: bool) -> None: Mark the interaction as pending. [Rust - `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_pending) + `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_set_pending) Args: interaction: @@ -6508,7 +6508,7 @@ def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> Add a comment to the interaction. [Rust - `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_set_comment) + `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_set_comment) Args: interaction: @@ -6542,7 +6542,7 @@ def add_text_comment(interaction: InteractionHandle, comment: str) -> None: Add a text comment to the interaction. [Rust - `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_add_text_comment) + `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_add_text_comment) Args: interaction: @@ -6572,7 +6572,7 @@ def pact_handle_get_async_message_iter(pact: PactHandle) -> PactAsyncMessageIter `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6598,7 +6598,7 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6624,7 +6624,7 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: `pactffi_pact_sync_http_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) + `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) # Safety @@ -6650,7 +6650,7 @@ def pact_handle_write_file( External interface to write out the pact file. [Rust - `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_pact_handle_write_file) + `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_handle_write_file) This function should be called if all the consumer tests have passed. @@ -6694,7 +6694,7 @@ def free_pact_handle(pact: PactHandle) -> None: Delete a Pact handle and free the resources used by it. [Rust - `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_free_pact_handle) + `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_free_pact_handle) Raises: RuntimeError: @@ -6714,7 +6714,7 @@ def verify(args: str) -> int: """ External interface to verifier a provider. - [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verify) + [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verify) * `args` - the same as the CLI interface, except newline delimited @@ -6746,7 +6746,7 @@ def verifier_new_for_application() -> VerifierHandle: to set the required values and enable it. [Rust - `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_new_for_application) + `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_new_for_application) """ result: cffi.FFI.CData = lib.pactffi_verifier_new_for_application( b"pact-python", @@ -6759,7 +6759,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None: """ Shutdown the verifier and release all resources. - [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_shutdown) + [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_shutdown) """ lib.pactffi_verifier_shutdown(handle._ref) @@ -6776,7 +6776,7 @@ def verifier_set_provider_info( # noqa: PLR0913 Set the provider details for the Pact verifier. [Rust - `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_provider_info) + `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_provider_info) Args: handle: @@ -6822,7 +6822,7 @@ def verifier_add_provider_transport( Adds a new transport for the given provider. [Rust - `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_provider_transport) + `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_add_provider_transport) Args: handle: @@ -6863,7 +6863,7 @@ def verifier_set_filter_info( Set the filters for the Pact verifier. [Rust - `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_filter_info) + `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_filter_info) Set filters to narrow down the interactions to verify. @@ -6899,7 +6899,7 @@ def verifier_set_provider_state( Set the provider state URL for the Pact verifier. [Rust - `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_provider_state) + `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_provider_state) Args: handle: @@ -6934,7 +6934,7 @@ def verifier_set_verification_options( Set the options used by the verifier when calling the provider. [Rust - `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_verification_options) + `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_verification_options) Args: handle: @@ -6969,7 +6969,7 @@ def verifier_set_coloured_output( Enables or disables coloured output using ANSI escape codes. [Rust - `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_coloured_output) + `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_coloured_output) By default, coloured output is enabled. @@ -6998,7 +6998,7 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> Enables or disables if no pacts are found to verify results in an error. [Rust - `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) + `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) Args: handle: @@ -7031,7 +7031,7 @@ def verifier_set_publish_options( Set the options used when publishing verification results to the Broker. [Rust - `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_publish_options) + `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_publish_options) Args: handle: @@ -7074,7 +7074,7 @@ def verifier_set_consumer_filters( Set the consumer filters for the Pact verifier. [Rust - `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_set_consumer_filters) + `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_consumer_filters) """ lib.pactffi_verifier_set_consumer_filters( handle._ref, @@ -7092,7 +7092,7 @@ def verifier_add_custom_header( Adds a custom header to be added to the requests made to the provider. [Rust - `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_custom_header) + `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_add_custom_header) """ lib.pactffi_verifier_add_custom_header( handle._ref, @@ -7106,7 +7106,7 @@ def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: Adds a Pact file as a source to verify. [Rust - `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_file_source) + `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_add_file_source) """ lib.pactffi_verifier_add_file_source(handle._ref, file.encode("utf-8")) @@ -7118,7 +7118,7 @@ def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> Non All pacts from the directory that match the provider name will be verified. [Rust - `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_add_directory_source) + `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_add_directory_source) # Safety @@ -7140,7 +7140,7 @@ def verifier_url_source( Adds a URL as a source to verify. [Rust - `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_url_source) + `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_url_source) Args: handle: @@ -7180,7 +7180,7 @@ def verifier_broker_source( Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_broker_source) + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_broker_source) This will fetch all the pact files from the broker that match the provider name. @@ -7228,7 +7228,7 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) + `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) This will fetch all the pact files from the broker that match the provider name and the consumer version selectors (See [Consumer Version @@ -7307,7 +7307,7 @@ def verifier_execute(handle: VerifierHandle) -> None: """ Runs the verification. - (https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_execute) + (https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_execute) Raises: RuntimeError: @@ -7327,7 +7327,7 @@ def verifier_cli_args() -> str: string. [Rust - `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_cli_args) + `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_cli_args) The purpose is to then be able to use in other languages which wrap the FFI library, to implement the same CLI functionality automatically without @@ -7383,7 +7383,7 @@ def verifier_logs(handle: VerifierHandle) -> OwnedString: Extracts the logs for the verification run. [Rust - `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_logs) + `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_logs) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` @@ -7405,7 +7405,7 @@ def verifier_logs_for_provider(provider_name: str) -> OwnedString: Extracts the logs for the verification run for the provider name. [Rust - `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_logs_for_provider) + `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_logs_for_provider) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` @@ -7427,7 +7427,7 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: Extracts the standard output for the verification run. [Rust - `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_output) + `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_output) Args: handle: @@ -7454,7 +7454,7 @@ def verifier_json(handle: VerifierHandle) -> OwnedString: Extracts the verification result as a JSON document. [Rust - `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_verifier_json) + `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_json) Raises: RuntimeError: @@ -7482,7 +7482,7 @@ def using_plugin( otherwise you will have plugin processes left running. [Rust - `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_using_plugin) + `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_using_plugin) Args: pact: @@ -7525,7 +7525,7 @@ def cleanup_plugins(pact: PactHandle) -> None: zero). [Rust - `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_cleanup_plugins) + `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_cleanup_plugins) """ lib.pactffi_cleanup_plugins(pact._ref) @@ -7544,7 +7544,7 @@ def interaction_contents( format of the JSON contents. [Rust - `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_interaction_contents) + `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_interaction_contents) Args: interaction: @@ -7604,7 +7604,7 @@ def matches_string_value( function once it is no longer required. [Rust - `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_string_value) + `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_string_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string @@ -7635,7 +7635,7 @@ def matches_u64_value( function once it is no longer required. [Rust - `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_u64_value) + `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_u64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7665,7 +7665,7 @@ def matches_i64_value( function once it is no longer required. [Rust - `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_i64_value) + `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_i64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7695,7 +7695,7 @@ def matches_f64_value( function once it is no longer required. [Rust - `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_f64_value) + `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_f64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7725,7 +7725,7 @@ def matches_bool_value( function once it is no longer required. [Rust - `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_bool_value) + `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_bool_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get, 0 == false and 1 == true @@ -7757,7 +7757,7 @@ def matches_binary_value( # noqa: PLR0913 function once it is no longer required. [Rust - `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_binary_value) + `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_binary_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7792,7 +7792,7 @@ def matches_json_value( function once it is no longer required. [Rust - `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.22/pact_ffi/?search=pactffi_matches_json_value) + `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_json_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string From b95b5699e879ac3ff71758dc1a02c126eae91f24 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 26 Aug 2025 10:26:49 +0000 Subject: [PATCH 0974/1376] docs: update changelog for pact-python-ffi/0.4.28.0 --- pact-python-ffi/CHANGELOG.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/pact-python-ffi/CHANGELOG.md b/pact-python-ffi/CHANGELOG.md index 43d2a406b..2a5e8b0f1 100644 --- a/pact-python-ffi/CHANGELOG.md +++ b/pact-python-ffi/CHANGELOG.md @@ -8,6 +8,38 @@ Note that this _only_ includes changes to the Python FFI interface. For changes +## [pact-python-ffi/0.4.28.0] _2025-08-26_ + +### 🚀 Features + +- _(v3)_ [**breaking**] Remove pact.v3.ffi module + > `pact.v3.ffi` is removed, and to be replaced by `pact_ffi`. That is, `pact.v3.ffi.$fn` should be replaced by `pact_ffi.$fn`. +- _(ffi)_ Upgrade lib to 0.4.28 + +### 🐛 Bug Fixes + +- Allow none in with_metadata + +### 📚 Documentation + +- Update changelog for pact-python-ffi/0.4.22.0 +- _(ffi)_ Fix old references to pact.v3.ffi +- V3 review +- Update git cliff configuration + +### ⚙️ Miscellaneous Tasks + +- _(ffi)_ Cleanup build script +- Ignore extensions +- Split out dependencies and tests +- Support pre and post release tags +- Remove reference count checks +- Store hatch venv in .venv + +### Contributors + +- @JP-Ellis + ## [pact-python-ffi/0.4.22.0] _2025-07-29_ ### 🚀 Features @@ -22,4 +54,4 @@ Note that this _only_ includes changes to the Python FFI interface. For changes - @JP-Ellis - + From 30077207b3aef3bfc0233e52fe8d23d67a8051e2 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Aug 2025 15:02:31 +1000 Subject: [PATCH 0975/1376] chore: fix sub-project git cliff config Signed-off-by: JP-Ellis --- cliff.toml | 2 +- pact-python-cli/cliff.toml | 4 +++- pact-python-ffi/cliff.toml | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cliff.toml b/cliff.toml index 915aa319e..1301c49d4 100644 --- a/cliff.toml +++ b/cliff.toml @@ -1,5 +1,5 @@ #:schema https://json.schemastore.org/any.json -# git-cliff ~ default configuration file +# git-cliff configuration file # https://git-cliff.org/docs/configuration [changelog] diff --git a/pact-python-cli/cliff.toml b/pact-python-cli/cliff.toml index f0d003821..67031d710 100644 --- a/pact-python-cli/cliff.toml +++ b/pact-python-cli/cliff.toml @@ -1,5 +1,5 @@ #:schema https://json.schemastore.org/any.json -# git-cliff ~ default configuration file +# git-cliff configuration file # https://git-cliff.org/docs/configuration [changelog] @@ -107,6 +107,8 @@ filter_commits = false topo_order = false # sort the commits inside sections by oldest/newest order sort_commits = "oldest" +# Only include the current directory (relative to the .git directory) +include_paths = ["pact-python-ffi/"] [remote.github] owner = "pact-foundation" diff --git a/pact-python-ffi/cliff.toml b/pact-python-ffi/cliff.toml index 83d720234..afc3a7863 100644 --- a/pact-python-ffi/cliff.toml +++ b/pact-python-ffi/cliff.toml @@ -107,6 +107,8 @@ filter_commits = false topo_order = false # sort the commits inside sections by oldest/newest order sort_commits = "oldest" +# Only include the current directory (relative to the .git directory) +include_paths = ["pact-python-ffi/"] [remote.github] owner = "pact-foundation" From 8bcc06ee0ffe9507123c7f205b3f6e268f2dde1a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Aug 2025 16:01:45 +1000 Subject: [PATCH 0976/1376] chore: hide import from traceback The `__import__` redefinition only inspects the import and prints a warning and otherwise does not interfere with the import process. Signed-off-by: JP-Ellis --- src/pact/generate/__init__.py | 1 + src/pact/match/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/pact/generate/__init__.py b/src/pact/generate/__init__.py index 724de7d4c..1f798de4b 100644 --- a/src/pact/generate/__init__.py +++ b/src/pact/generate/__init__.py @@ -72,6 +72,7 @@ def __import__( # noqa: N807 warn users when they import functions directly from this module. This is done to avoid shadowing built-in types and functions. """ + __tracebackhide__ = True if name == "pact.generate" and len(set(fromlist) - {"Matcher"}) > 0: warnings.warn( "Avoid `from pact.generate import `. " diff --git a/src/pact/match/__init__.py b/src/pact/match/__init__.py index 75ec6f4e1..afef67b43 100644 --- a/src/pact/match/__init__.py +++ b/src/pact/match/__init__.py @@ -130,6 +130,7 @@ def __import__( # noqa: N807 users when they import functions directly from this module. This is done to avoid shadowing built-in types and functions. """ + __tracebackhide__ = True if name == "pact.match" and len(set(fromlist) - {"Matcher"}) > 0: warnings.warn( "Avoid `from pact.match import `. " From bdd8c9de6fc9dc0b3eeb7adf576a970d464b6dc8 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 28 Aug 2025 12:43:34 +1000 Subject: [PATCH 0977/1376] chore(ffi): clean up data directory Signed-off-by: JP-Ellis --- pact-python-ffi/hatch_build.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pact-python-ffi/hatch_build.py b/pact-python-ffi/hatch_build.py index ead8dc4c2..0c9cd3d11 100644 --- a/pact-python-ffi/hatch_build.py +++ b/pact-python-ffi/hatch_build.py @@ -87,6 +87,8 @@ def clean(self, versions: list[str]) -> None: # noqa: ARG002 # Cleanup the Pact FFI library for lib in PKG_DIR.glob("*pact_ffi.*"): lib.unlink() + # Cleanup the data directory + shutil.rmtree(PKG_DIR / "data", ignore_errors=True) def initialize( self, From 66ddf128b69ee95be8c27ccae65929b55ca81f91 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 28 Aug 2025 12:45:18 +1000 Subject: [PATCH 0978/1376] fix(ffi): make version dynamic The version was always meant to be dynamic, but had to be fixed initially to bootstrap the process. This was inadvertently not removed. Signed-off-by: JP-Ellis --- pact-python-ffi/pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 72da52305..017d4c8f2 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -3,11 +3,10 @@ description = "Python bindings for the Pact FFI library" name = "pact-python-ffi" -# dynamic = ["version"] +dynamic = ["version"] keywords = ["pact", "ffi", "pact-python", "contract-testing"] license = "MIT" readme = "README.md" -version = "0.4.22.0" authors = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] maintainers = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] From fb48cfdde9647cf25c4a00f752647ba00455ec97 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 28 Aug 2025 13:01:35 +1000 Subject: [PATCH 0979/1376] chore: fix flask integer coercion Signed-off-by: JP-Ellis --- tests/test_match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_match.py b/tests/test_match.py index f44e2aea5..a2bfb03cc 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -95,7 +95,7 @@ def redirect() -> NoReturn: if __name__ == "__main__": app = Flask(__name__) - @app.route("/path/to/") + @app.route("/path/to/") def hello_world(test_id: int) -> Response: random_regex_matches = "1-8 digits: 12345678, 1-8 random letters abcdefgh" response = make_response({ From 1b948f36b4cddb4ba7c507eed0b5c3765a7680d2 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 27 Aug 2025 16:07:59 +1000 Subject: [PATCH 0980/1376] chore: add v3 matching rules test Fixes: #1179 Signed-off-by: JP-Ellis --- .../test_v3_matching_rules.py | 437 ++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 tests/compatibility_suite/test_v3_matching_rules.py diff --git a/tests/compatibility_suite/test_v3_matching_rules.py b/tests/compatibility_suite/test_v3_matching_rules.py new file mode 100644 index 000000000..c8099c8ed --- /dev/null +++ b/tests/compatibility_suite/test_v3_matching_rules.py @@ -0,0 +1,437 @@ +"""V3 matching rules tests.""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import TYPE_CHECKING, Any +from urllib.parse import parse_qs + +import pytest +import requests +from pytest_bdd import ( + given, + parsers, + scenario, + then, + when, +) + +from pact.pact import Pact +from tests.compatibility_suite.util import ( + FIXTURES_ROOT, + parse_horizontal_table, +) +from tests.compatibility_suite.util.interaction_definition import ( + InteractionDefinition, +) + +if TYPE_CHECKING: + from collections.abc import Callable + + from pact.error import Mismatch + +TEST_PACT_FILE_DIRECTORY = Path(Path(__file__).parent / "pacts") +EXT_TO_CONTENT_TYPE = { + "jpg": "image/jpeg", + "pdf": "application/pdf", + "json": "application/json", +} + +logger = logging.getLogger(__name__) + + +@pytest.fixture +def pact() -> Pact: + return Pact( + "v3-matching-rules-consumer", + "v3-matching-rules-provider", + ).with_specification("V3") + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a Boolean matcher (negative case)", +) +def test_supports_a_boolean_matcher_negative_case() -> None: + """Supports a Boolean matcher (negative case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a Boolean matcher (positive case)", +) +def test_supports_a_boolean_matcher_positive_case() -> None: + """Supports a Boolean matcher (positive case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a ContentType matcher (negative case)", +) +def test_supports_a_contenttype_matcher_negative_case() -> None: + """Supports a ContentType matcher (negative case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a ContentType matcher (positive case)", +) +def test_supports_a_contenttype_matcher_positive_case() -> None: + """Supports a ContentType matcher (positive case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a Date and Time matcher (negative case)", +) +def test_supports_a_date_and_time_matcher_negative_case() -> None: + """Supports a Date and Time matcher (negative case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a Date and Time matcher (positive case)", +) +def test_supports_a_date_and_time_matcher_positive_case() -> None: + """Supports a Date and Time matcher (positive case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a Values matcher (negative case, final type is wrong)", +) +def test_supports_a_values_matcher_negative_case_final_type_is_wrong() -> None: + """Supports a Values matcher (negative case, final type is wrong).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a Values matcher (positive case, ignores missing and additional keys)", +) +def test_values_matcher_positive_case_missing_and_additional_keys() -> None: + """Supports a Values matcher (ignores missing and additional keys).""" + + +@pytest.mark.skip( + reason="Waiting on an upstream change in FFI and/or Compatibility Suite" +) +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a decimal type matcher " + "where it is acceptable to coerce values from string form", +) +def test_decimal_matcher_coerce_string_form() -> None: + """Supports a decimal type matcher string form.""" + + +@pytest.mark.skip( + reason="Waiting on an upstream change in FFI and/or Compatibility Suite" +) +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a decimal type matcher, " + "must have significant digits after the decimal point (negative case)", +) +def test_decimal_matcher_significant_digits_negative() -> None: + """Supports a decimal type matcher with decimal digits (negative case).""" + + +@pytest.mark.skip( + reason="Waiting on an upstream change in FFI and/or Compatibility Suite" +) +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a integer type matcher, " + "no digits after the decimal point (negative case)", +) +def test_integer_matcher_no_decimal_digits_negative() -> None: + """Tests integer matcher with no decimal digits (negative case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a minmax type matcher (negative case)", +) +def test_supports_a_minmax_type_matcher_negative_case() -> None: + """Supports a minmax type matcher (negative case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a minmax type matcher (positive case)", +) +def test_supports_a_minmax_type_matcher_positive_case() -> None: + """Supports a minmax type matcher (positive case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a null matcher (positive case)", +) +def test_supports_a_null_matcher_positive_case() -> None: + """Supports a null matcher (positive case).""" + + +@pytest.mark.skip( + reason="Waiting on an upstream change in FFI and/or Compatibility Suite" +) +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a number type matcher (negative case)", +) +def test_supports_a_number_type_matcher_negative_case() -> None: + """Supports a number type matcher (negative case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a number type matcher (positive case)", +) +def test_supports_a_number_type_matcher_positive_case() -> None: + """Supports a number type matcher (positive case).""" + + +@pytest.mark.skip( + reason="Waiting on an upstream change in FFI and/or Compatibility Suite" +) +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports a number type matcher " + "where it is acceptable to coerce values from string form", +) +def test_number_type_matcher_coerce_string_form() -> None: + """Tests number type matcher coerce string form.""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports an decimal type matcher, " + "must have significant digits after the decimal point (positive case)", +) +def test_decimal_matcher_significant_digits_positive() -> None: + """Tests decimal matcher with significant digits (positive case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports an equality matcher to reset cascading rules", +) +def test_supports_an_equality_matcher_to_reset_cascading_rules() -> None: + """Supports an equality matcher to reset cascading rules.""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports an include matcher (negative case)", +) +def test_supports_an_include_matcher_negative_case() -> None: + """Supports an include matcher (negative case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports an include matcher (positive case)", +) +def test_supports_an_include_matcher_positive_case() -> None: + """Supports an include matcher (positive case).""" + + +@pytest.mark.skip( + reason="Waiting on an upstream change in FFI and/or Compatibility Suite" +) +@scenario( + "definition/features/V3/matching_rules.feature", + ( + "Supports an integer type matcher " + "where it is acceptable to coerce values from string form" + ), +) +def test_integer_type_matcher_coerce_string_form() -> None: + """Supports an integer type matcher coerce string form.""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports an integer type matcher, " + "no digits after the decimal point (positive case)", +) +def test_integer_type_matcher_no_decimal_digits_positive() -> None: + """Tests integer type matcher no decimal digits (positive case).""" + + +@scenario( + "definition/features/V3/matching_rules.feature", + "Supports an null matcher (negative case)", +) +def test_supports_an_null_matcher_negative_case() -> None: + """Supports an null matcher (negative case).""" + + +################################################################################ +## Given +################################################################################ + + +@given( + parsers.re( + r"^(" + r"a request is received with the following:|" + r"the following requests are received:" + r")$" + ), + target_fixture="request_calls", +) +def a_request_is_received_with_the_following( + datatable: list[list[str]], +) -> list[Callable[[str], requests.Response]]: + """A request is received with the following:.""" + data = parse_horizontal_table(datatable) + assert len(data) > 0, "Expected at least one row in the table" + + body: Any + request_calls: list[Callable[[str], requests.Response]] = [] + for row in data: + content_type = row.pop("content type", None) + + if body := row.pop("body", None): + if body.startswith("JSON: "): + content_type = content_type or "application/json" + body = body.replace("JSON: ", "") + elif body.startswith("file: "): + content_type = ( + content_type or EXT_TO_CONTENT_TYPE[body.rsplit(".")[-1].lower()] + ) + body = (FIXTURES_ROOT / body.replace("file: ", "")).read_bytes() + + query: dict[str, list[str]] = ( + parse_qs(s) if (s := row.pop("query", None)) else {} + ) + headers = ( + dict(s.split(": ") for s in hs.strip("'").split("; ")) + if (hs := row.pop("headers", None)) + else {} + ) + + # Ignore description field + row.pop("desc", None) + + if row: + msg = f"Unexpected extra columns in table: {row!r}" + raise ValueError(msg) + + logger.debug( + "Configured POST request: %r", + { + "body": body, + "content_type": content_type, + "query": query, + "headers": headers, + }, + ) + + request_calls.append( + lambda url, # type: ignore[misc] + body=body, + content_type=content_type, + headers=headers, + query=query: requests.post( + url, + body, + timeout=2, + headers={ + **({"Content-Type": content_type} if content_type else {}), + **(headers), + }, + params=query, + ) + ) + + return request_calls + + +@given("an expected request configured with the following:") +def an_expected_request_configured_with( + pact: Pact, + datatable: list[list[str]], +) -> None: + """An expected request configured with.""" + data = parse_horizontal_table(datatable) + assert len(data) == 1, "Expected exactly one row in the table" + + interaction = InteractionDefinition( + method="POST", + path="/", + **data[0], # type: ignore[arg-type] + ) + interaction.add_to_pact(pact, "a matching rules request") + + +################################################################################ +## When +################################################################################ + + +@when( + parsers.re("the (request is|requests are) compared to the expected one"), + target_fixture="mismatches", +) +def the_request_is_compared_to_the_expected_one( + pact: Pact, + request_calls: list[Callable[[str], requests.Response]], +) -> list[Mismatch]: + """The request is compared to the expected one.""" + with pact.serve(raises=False) as srv: + for f in request_calls: + f(str(srv.url)) + + return srv.mismatches + + +################################################################################ +## Then +################################################################################ + + +@then( + parsers.re(r"the comparison should (?P(NOT )?)be OK"), + converters={"negated": lambda s: s == "NOT "}, +) +def the_comparison_should_be_ok( + negated: bool, # noqa: FBT001 + mismatches: list[Mismatch], +) -> None: + """The comparison should be OK.""" + if negated: + assert len(mismatches) > 0 + else: + assert len(mismatches) == 0 + + +@then( + parsers.re( + r"the mismatches will contain a mismatch " + r'with error "(?P[^"]+)" -> "(?P[^"]+)"' + ) +) +def the_mismatches_will_contain_a_mismatch_with_error( + path: str, + message: str, + mismatches: list[Mismatch], +) -> None: + """The mismatches will contain a mismatch with error.""" + logger.info("Searching for mismatch with path=%r, error=%r", path, message) + for mismatch in mismatches: + for submismatch in getattr(mismatch, "mismatches", []): + logger.info("Checking submismatch: %r", submismatch) + if ( + (s_path := getattr(submismatch, "path", None)) + and path == s_path + and (s_message := getattr(submismatch, "mismatch", None)) + and message in s_message + ): + logger.info("Found matching submismatch: %r", submismatch) + return + + msg = f"Mismatch not found: path={path!r}, error={message!r}" + raise AssertionError(msg) From 371f4a21ba364fe33d223b280ea8c701d483b330 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 28 Aug 2025 12:47:38 +1000 Subject: [PATCH 0981/1376] chore: add v4 matching rules tests Fixes: #1180 Signed-off-by: JP-Ellis --- .../test_v4_matching_rules.py | 493 ++++++++++++++++++ 1 file changed, 493 insertions(+) create mode 100644 tests/compatibility_suite/test_v4_matching_rules.py diff --git a/tests/compatibility_suite/test_v4_matching_rules.py b/tests/compatibility_suite/test_v4_matching_rules.py new file mode 100644 index 000000000..608869462 --- /dev/null +++ b/tests/compatibility_suite/test_v4_matching_rules.py @@ -0,0 +1,493 @@ +"""V4 matching rules tests.""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import TYPE_CHECKING, Any +from urllib.parse import parse_qs + +import pytest +import requests +from pytest_bdd import ( + given, + parsers, + scenario, + then, + when, +) + +from pact import Pact, Verifier +from tests.compatibility_suite.util import ( + FIXTURES_ROOT, + parse_horizontal_table, +) +from tests.compatibility_suite.util.interaction_definition import ( + InteractionDefinition, +) +from tests.compatibility_suite.util.provider import Provider + +if TYPE_CHECKING: + from collections.abc import Callable + + from pact.error import Mismatch + +TEST_PACT_FILE_DIRECTORY = Path(Path(__file__).parent / "pacts") +EXT_TO_CONTENT_TYPE = { + "jpg": "image/jpeg", + "pdf": "application/pdf", + "json": "application/json", +} + +logger = logging.getLogger(__name__) + + +@pytest.fixture +def pact() -> Pact: + return Pact( + "v4-matching-rules-consumer", + "v4-matching-rules-provider", + ).with_specification("V4") + + +@pytest.fixture +def verifier() -> Verifier: + return Verifier("v4-matching-rules-provider") + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports a ArrayContains matcher (negative case)", +) +def test_supports_a_arraycontains_matcher_negative_case() -> None: + """Supports a ArrayContains matcher (negative case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports a EachValue matcher (negative case)", +) +def test_supports_a_eachvalue_matcher_negative_case() -> None: + """Supports a EachValue matcher (negative case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports a not empty matcher (negative case 2, types are different)", +) +def test_supports_a_not_empty_matcher_negative_case_2_types_are_different() -> None: + """Supports a not empty matcher (negative case 2, types are different).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports a not empty matcher (negative case)", +) +def test_supports_a_not_empty_matcher_negative_case() -> None: + """Supports a not empty matcher (negative case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports a not empty matcher (positive case)", +) +def test_supports_a_not_empty_matcher_positive_case() -> None: + """Supports a not empty matcher (positive case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports a not empty matcher with binary data (negative case)", +) +def test_supports_a_not_empty_matcher_with_binary_data_negative_case() -> None: + """Supports a not empty matcher with binary data (negative case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports a not empty matcher with binary data (positive case)", +) +def test_supports_a_not_empty_matcher_with_binary_data_positive_case() -> None: + """Supports a not empty matcher with binary data (positive case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports a semver matcher (negative case)", +) +def test_supports_a_semver_matcher_negative_case() -> None: + """Supports a semver matcher (negative case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports a semver matcher (positive case)", +) +def test_supports_a_semver_matcher_positive_case() -> None: + """Supports a semver matcher (positive case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports a status code matcher (negative case)", +) +def test_supports_a_status_code_matcher_negative_case() -> None: + """Supports a status code matcher (negative case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports a status code matcher (positive case)", +) +def test_supports_a_status_code_matcher_positive_case() -> None: + """Supports a status code matcher (positive case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports an ArrayContains matcher (positive case)", +) +def test_supports_an_arraycontains_matcher_positive_case() -> None: + """Supports an ArrayContains matcher (positive case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports an EachKey matcher (negative case)", +) +def test_supports_an_eachkey_matcher_negative_case() -> None: + """Supports an EachKey matcher (negative case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports an EachKey matcher (positive case)", +) +def test_supports_an_eachkey_matcher_positive_case() -> None: + """Supports an EachKey matcher (positive case).""" + + +@scenario( + "definition/features/V4/matching_rules.feature", + "Supports an EachValue matcher (positive case)", +) +def test_supports_an_eachvalue_matcher_positive_case() -> None: + """Supports an EachValue matcher (positive case).""" + + +################################################################################ +## Given +################################################################################ + + +@given( + parsers.re( + r"^(" + r"a request is received with the following:|" + r"the following requests are received:" + r")$" + ), + target_fixture="request_calls", +) +def a_request_is_received_with_the_following( + datatable: list[list[str]], +) -> list[Callable[[str], requests.Response]]: + """A request is received with the following:.""" + data = parse_horizontal_table(datatable) + assert len(data) > 0, "Expected at least one row in the table" + + body: Any + request_calls: list[Callable[[str], requests.Response]] = [] + for row in data: + content_type = row.pop("content type", None) + + if body := row.pop("body", None): + if body.startswith("JSON: "): + content_type = content_type or "application/json" + body = body.replace("JSON: ", "") + elif body.startswith("file: "): + content_type = ( + content_type or EXT_TO_CONTENT_TYPE[body.rsplit(".")[-1].lower()] + ) + body = (FIXTURES_ROOT / body.replace("file: ", "")).read_bytes() + elif body == "EMPTY": + body = None + + query: dict[str, list[str]] = ( + parse_qs(s) if (s := row.pop("query", None)) else {} + ) + headers = ( + dict(s.split(": ") for s in hs.strip("'").split("; ")) + if (hs := row.pop("headers", None)) + else {} + ) + + # Ignore description field + row.pop("desc", None) + + if row: + msg = f"Unexpected extra columns in table: {row!r}" + raise ValueError(msg) + + logger.debug( + "Configured POST request: %r", + { + "body": body, + "content_type": content_type, + "query": query, + "headers": headers, + }, + ) + + request_calls.append( + lambda url, # type: ignore[misc] + body=body, + content_type=content_type, + headers=headers, + query=query: requests.post( + url, + body, + timeout=2, + headers={ + **({"Content-Type": content_type} if content_type else {}), + **(headers), + }, + params=query, + ) + ) + + return request_calls + + +@given("an expected request configured with the following:") +def an_expected_request_configured_with( + pact: Pact, + datatable: list[list[str]], +) -> None: + """An expected request configured with.""" + data = parse_horizontal_table(datatable) + assert len(data) == 1, "Expected exactly one row in the table" + + interaction = InteractionDefinition( + method="POST", + path="/", + **data[0], # type: ignore[arg-type] + ) + interaction.add_to_pact(pact, "a matching rules request") + + +@given("an expected response configured with the following:") +def an_expected_response_configured_with_the_following( + pact: Pact, + datatable: list[list[str]], + tmp_path: Path, + verifier: Verifier, +) -> None: + """An expected response configured with the following.""" + data = parse_horizontal_table(datatable) + assert len(data) == 1, "Expected exactly one row in the table" + row = data[0] + + interaction = InteractionDefinition( + method="POST", + path="/", + status=row["status"], + response_matching_rules=row["matching rules"], + ) + interaction.add_to_pact(pact, "a matching rules response") + (tmp_path / "pacts").mkdir(exist_ok=True, parents=True) + pact.write_file(tmp_path / "pacts") + + with ( + tmp_path + / "pacts" + / "v4-matching-rules-consumer-v4-matching-rules-provider.json" + ).open( + "r", + encoding="utf-8", + ) as f: + for line in f: + logger.info("Pact file: %s", line.rstrip()) + + verifier.add_source(tmp_path / "pacts") + + +@given( + parsers.re(r"a status (?P\d{3}) response is received"), + target_fixture="provider", +) +def a_response_is_received( + status_code: str, + verifier: Verifier, +) -> Provider: + """A response is received.""" + provider = Provider() + interaction = InteractionDefinition( + method="POST", + path="/", + response=status_code, + ) + provider.add_interaction(interaction) + verifier.add_transport(url=provider.url) + return provider + + +################################################################################ +## When +################################################################################ + + +@when( + "the response is compared to the expected one", + target_fixture="verifier_result", +) +def the_response_is_compared_to_the_expected_one( + provider: Provider, + verifier: Verifier, +) -> tuple[Verifier, Exception | None]: + """The response is compared to the expected one.""" + with provider: + try: + verifier.verify() + except Exception as e: # noqa: BLE001 + return verifier, e + return verifier, None + + +@when( + parsers.re(r"the (request is|requests are) compared to the expected one"), + target_fixture="mismatches", +) +def the_request_is_compared_to_the_expected_one( + pact: Pact, + request_calls: list[Callable[[str], requests.Response]], +) -> list[Mismatch]: + """The request is compared to the expected one.""" + with pact.serve(raises=False) as srv: + for f in request_calls: + f(str(srv.url)) + + return srv.mismatches + + +################################################################################ +## Then +################################################################################ + + +@then( + parsers.re(r"the comparison should (?P(NOT )?)be OK"), + converters={"negated": lambda s: s == "NOT "}, +) +def the_comparison_should_be_ok( + negated: bool, # noqa: FBT001 + mismatches: list[Mismatch], +) -> None: + """The comparison should be OK.""" + if negated: + assert len(mismatches) > 0 + else: + assert len(mismatches) == 0 + + +@then( + parsers.re( + r"the mismatches will contain a mismatch " + r'with error "(?P[^"]+)" -> "(?P.+)"$' + ) +) +def the_mismatches_will_contain_a_mismatch_with_error( + path: str, + message: str, + mismatches: list[Mismatch], +) -> None: + """The mismatches will contain a mismatch with error.""" + # To account for slight differences in wording between implementations + # we map some expected values here. + path, message = { + ( + "$", + "Expected [] (0 bytes) to not be empty", + ): ("/", "Expected body Present(28058 bytes, image/jpeg) but was empty"), + ( + "$.actions", + 'Variant at index 1 ({\\"href\\":\\"http://api.x.io/orders/42/items\\",' + '\\"method\\":\\"DELETE\\",\\"name\\":\\"delete-item\\",' + '\\"title\\":\\"Delete Item\\"}) was not found in the actual list', + ): ( + "$.actions", + 'Variant at index 1 ({"href":"http://api.x.io/orders/42/items",' + '"method":"DELETE","name":"delete-item","title":"Delete Item"}) was ' + "not found in the actual list", + ), + ( + "$.two", + "Type mismatch: Expected 'b' (String) " + 'to be the same type as [\\"b\\"] (Array)', + ): ( + "$.two", + "Type mismatch: Expected 'b' (String) " + 'to be the same type as ["b"] (Array)', + ), + }.get((path, message), (path, message)) + logger.info("Searching for mismatch with path=%r, error=%r", path, message) + for mismatch in mismatches: + for submismatch in getattr(mismatch, "mismatches", []): + logger.info("Checking submismatch: %r", submismatch) + if ( + (s_path := getattr(submismatch, "path", None)) + and path == s_path + and (s_message := getattr(submismatch, "mismatch", None)) + and message in s_message + ): + logger.info("Found matching submismatch: %r", submismatch) + return + + msg = f"Mismatch not found: path={path!r}, error={message!r}" + raise AssertionError(msg) + + +@then( + parsers.re(r"the response comparison should (?P(NOT )?)be OK"), + converters={"negated": lambda s: s == "NOT "}, +) +def the_response_comparison_should_be_maybe_ok( + negated: bool, # noqa: FBT001 + verifier_result: tuple[Verifier, Exception | None], +) -> None: + """The response comparison should maybe be OK.""" + _, result = verifier_result + if negated: + assert result is not None + else: + assert result is None + + +@then( + parsers.re( + r'the response mismatches will contain a "(?P[^"]+)" mismatch ' + r'with error "(?P.+)"$' + ) +) +def the_response_mismatches_will_contain_a_mismatch_with_error( + mismatch_type: str, + message: str, + verifier_result: tuple[Verifier, Exception | None], +) -> None: + """The response mismatches will contain a mismatch with error.""" + mismatch_type = {"status": "StatusMismatch"}[mismatch_type] + + verifier, mismatches = verifier_result + assert mismatches is not None, "Expected mismatches to be present" + for error in verifier.results["errors"]: + if (mismatch := error.get("mismatch")) and ( + mismatches := mismatch.get("mismatches") + ): + for submismatch in mismatches: + if submismatch.get( + "type" + ) == mismatch_type and message in submismatch.get("mismatch", ""): + logger.info("Found matching submismatch: %r", submismatch) + return + msg = f"Mismatch {mismatch_type!r} not found with error={message!r}" + raise AssertionError(msg) From 0c04faab55ac08bdff8bd303d8970709554ba38e Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Fri, 29 Aug 2025 03:22:17 +0000 Subject: [PATCH 0982/1376] docs: update changelog for pact-python-ffi/0.4.28.1 --- pact-python-ffi/CHANGELOG.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pact-python-ffi/CHANGELOG.md b/pact-python-ffi/CHANGELOG.md index 2a5e8b0f1..59d698a79 100644 --- a/pact-python-ffi/CHANGELOG.md +++ b/pact-python-ffi/CHANGELOG.md @@ -8,6 +8,25 @@ Note that this _only_ includes changes to the Python FFI interface. For changes +## [pact-python-ffi/0.4.28.1] _2025-08-28_ + +### 🐛 Bug Fixes + +- _(ffi)_ Make version dynamic + +### 📚 Documentation + +- Update changelog for pact-python-ffi/0.4.28.0 + +### ⚙️ Miscellaneous Tasks + +- Fix sub-project git cliff config +- _(ffi)_ Clean up data directory + +### Contributors + +- @JP-Ellis + ## [pact-python-ffi/0.4.28.0] _2025-08-26_ ### 🚀 Features @@ -54,4 +73,4 @@ Note that this _only_ includes changes to the Python FFI interface. For changes - @JP-Ellis - + From 1ea8bd95ae1b7554846e801dc56efcaccdc431b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 03:55:55 +0000 Subject: [PATCH 0983/1376] chore(deps): update ruff to v0.12.11 (#1214) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a69c83b9c..ae88a94d0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.10 + rev: v0.12.11 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 33a8236ef..fb4c9bdd4 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.9" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.12.10", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.12.11", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=6.0", "pytest~=8.0"] types = ["mypy==1.17.1"] From 358dd497b8819820a6afcacc4c4d8ffde37e9155 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 03:56:52 +0000 Subject: [PATCH 0984/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.35.6 (#1213) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ae88a94d0..ed7f6b621 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,7 +78,7 @@ repos: )$ - repo: https://github.com/crate-ci/typos - rev: v1.35.5 + rev: v1.35.6 hooks: - id: typos exclude: | From 6e0ec98e8110e648cdb4c2c0ac1122351fe79adf Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 29 Aug 2025 13:21:18 +1000 Subject: [PATCH 0985/1376] chore(ci): add publish as completion dependency This should _not_ result in failures though, as the `if` only checks for cancelled or failed dependencies (implicitly allowing skipped ones). Signed-off-by: JP-Ellis --- .github/workflows/build-cli.yml | 1 + .github/workflows/build-ffi.yml | 1 + .github/workflows/build.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index bc7a004d6..04a56bd42 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -36,6 +36,7 @@ jobs: needs: - build-sdist - build-wheels + - publish steps: - name: Failed diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 7139c4591..c4b9db3f3 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -36,6 +36,7 @@ jobs: needs: - build-sdist - build-wheels + - publish steps: - name: Failed diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 46686da8b..f64836e78 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,6 +35,7 @@ jobs: runs-on: ubuntu-latest needs: - build + - publish steps: - name: Failed From cf7282eea66435a5f60cdebecf7fc013323c6e32 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 29 Aug 2025 13:22:53 +1000 Subject: [PATCH 0986/1376] refactor(ci): if statement Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e6e5e14f..24a891e9b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,8 +42,10 @@ jobs: steps: - name: Failed run: exit 1 - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, - 'skipped') + if: | + contains(needs.*.result, 'failure') + || contains(needs.*.result, 'cancelled') + || contains(needs.*.result, 'skipped') test: name: >- From 8f7874408f51b73a8ff8e1880ee43b5568a11845 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 1 Sep 2025 16:59:05 +1000 Subject: [PATCH 0987/1376] chore(tests): add generators to interaction defn Signed-off-by: JP-Ellis --- tests/compatibility_suite/util/__init__.py | 2 +- .../util/interaction_definition.py | 29 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/tests/compatibility_suite/util/__init__.py b/tests/compatibility_suite/util/__init__.py index 1111c0a43..440cfc262 100644 --- a/tests/compatibility_suite/util/__init__.py +++ b/tests/compatibility_suite/util/__init__.py @@ -280,7 +280,7 @@ def parse_headers(headers: str) -> MultiDict[str]: return MultiDict(kvs) -def parse_matching_rules(matching_rules: str) -> str: +def parse_rules(matching_rules: str) -> str: """ Parse the matching rules. diff --git a/tests/compatibility_suite/util/interaction_definition.py b/tests/compatibility_suite/util/interaction_definition.py index a16d8e5eb..1b91e89a0 100644 --- a/tests/compatibility_suite/util/interaction_definition.py +++ b/tests/compatibility_suite/util/interaction_definition.py @@ -24,7 +24,7 @@ from tests.compatibility_suite.util import ( FIXTURES_ROOT, parse_headers, - parse_matching_rules, + parse_rules, truncate, ) @@ -290,11 +290,13 @@ def __init__( self.headers: MultiDict[str] = MultiDict() self.body: InteractionBody | None = None self.matching_rules: str | None = None + self.generators: str | None = None # Response properties self.response_headers: MultiDict[str] = MultiDict() self.response_body: InteractionBody | None = None self.response_matching_rules: str | None = None + self.response_generators: str | None = None self.update(metadata=metadata, **kwargs) @@ -373,7 +375,7 @@ def _update_shared( return kwargs - def _update_request(self, **kwargs: str) -> dict[str, str]: + def _update_request(self, **kwargs: str) -> dict[str, str]: # noqa: C901 """ Update the request properties of the interaction. @@ -389,6 +391,7 @@ def _update_request(self, **kwargs: str) -> dict[str, str]: - `body`: Request body. - `content_type`: Request content type. - `matching_rules`: Request matching rules. + - `generators`: Request generators. """ if method := kwargs.pop("method", None): @@ -425,7 +428,10 @@ def _update_request(self, **kwargs: str) -> dict[str, str]: if matching_rules := ( kwargs.pop("matching_rules", None) or kwargs.pop("matching rules", None) ): - self.matching_rules = parse_matching_rules(matching_rules) + self.matching_rules = parse_rules(matching_rules) + + if generators := kwargs.pop("generators", None): + self.generators = parse_rules(generators) return kwargs @@ -442,6 +448,7 @@ def _update_response(self, **kwargs: str) -> dict[str, str]: - `response_content`: Response content type. - `response_body`: Response body. - `response_matching_rules`: Response matching rules. + - `response_generators`: Response generators. Returns: The remaining keyword arguments. @@ -476,7 +483,13 @@ def _update_response(self, **kwargs: str) -> dict[str, str]: kwargs.pop("response_matching_rules", None) or kwargs.pop("response matching rules", None) ): - self.response_matching_rules = parse_matching_rules(matching_rules) + self.response_matching_rules = parse_rules(matching_rules) + + if generators := ( + kwargs.pop("response_generators", None) + or kwargs.pop("response generators", None) + ): + self.response_generators = parse_rules(generators) return kwargs @@ -562,6 +575,10 @@ def add_to_pact( # noqa: C901, PLR0912, PLR0915 logger.info("with_matching_rules(%r)", self.matching_rules) interaction.with_matching_rules(self.matching_rules) + if self.generators: + logger.info("with_generators(%r)", self.generators) + interaction.with_generators(self.generators) + if self.response: assert isinstance(interaction, HttpInteraction), ( "Response requires an HTTP interaction" @@ -586,6 +603,10 @@ def add_to_pact( # noqa: C901, PLR0912, PLR0915 logger.info("with_matching_rules(%r)", self.response_matching_rules) interaction.with_matching_rules(self.response_matching_rules) + if self.response_generators: + logger.info("with_generators(%r)", self.response_generators) + interaction.with_generators(self.response_generators) + if self.metadata: interaction.with_metadata(self.metadata) From cbbb03ac2e49c49b6fcdf64ffaa6a6fe12ccabfa Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 1 Sep 2025 17:04:36 +1000 Subject: [PATCH 0988/1376] chore(tests): test v3 generators Signed-off-by: JP-Ellis --- .../compatibility_suite/test_v3_generators.py | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 tests/compatibility_suite/test_v3_generators.py diff --git a/tests/compatibility_suite/test_v3_generators.py b/tests/compatibility_suite/test_v3_generators.py new file mode 100644 index 000000000..8ebfae973 --- /dev/null +++ b/tests/compatibility_suite/test_v3_generators.py @@ -0,0 +1,191 @@ +"""Test of V3 generators.""" + +import logging +import re +from collections.abc import Callable + +import pytest +import requests +from pytest_bdd import ( + given, + parsers, + scenario, + then, + when, +) + +from pact import Pact +from tests.compatibility_suite.util import parse_horizontal_table +from tests.compatibility_suite.util.interaction_definition import InteractionDefinition + +logger = logging.getLogger(__name__) + + +@pytest.fixture +def pact() -> Pact: + return Pact( + "v3-generators-consumer", + "v3-generators-provider", + ).with_specification("V3") + + +################################################################################ +## Scenario +################################################################################ + + +@scenario("definition/features/V3/generators.feature", "Supports a UUID generator") +def test_supports_a_uuid_generator() -> None: + """Supports a UUID generator.""" + + +@scenario("definition/features/V3/generators.feature", "Supports a boolean generator") +def test_supports_a_boolean_generator() -> None: + """Supports a boolean generator.""" + + +@scenario("definition/features/V3/generators.feature", "Supports a date generator") +def test_supports_a_date_generator() -> None: + """Supports a date generator.""" + + +@scenario("definition/features/V3/generators.feature", "Supports a date-time generator") +def test_supports_a_datetime_generator() -> None: + """Supports a date-time generator.""" + + +@scenario( + "definition/features/V3/generators.feature", "Supports a random decimal generator" +) +def test_supports_a_random_decimal_generator() -> None: + """Supports a random decimal generator.""" + + +@scenario( + "definition/features/V3/generators.feature", + "Supports a random hexadecimal generator", +) +def test_supports_a_random_hexadecimal_generator() -> None: + """Supports a random hexadecimal generator.""" + + +@scenario( + "definition/features/V3/generators.feature", "Supports a random integer generator" +) +def test_supports_a_random_integer_generator() -> None: + """Supports a random integer generator.""" + + +@scenario( + "definition/features/V3/generators.feature", "Supports a random string generator" +) +def test_supports_a_random_string_generator() -> None: + """Supports a random string generator.""" + + +@scenario("definition/features/V3/generators.feature", "Supports a regex generator") +def test_supports_a_regex_generator() -> None: + """Supports a regex generator.""" + + +@scenario("definition/features/V3/generators.feature", "Supports a time generator") +def test_supports_a_time_generator() -> None: + """Supports a time generator.""" + + +################################################################################ +## Scenario +################################################################################ + + +@given( + "a request configured with the following generators:", + target_fixture="interaction", +) +def a_request_configured_with_the_following_generators( + pact: Pact, + datatable: list[list[str]], +) -> InteractionDefinition: + """A request configured with the following generators.""" + data = parse_horizontal_table(datatable) + assert len(data) == 1, "Expected a single row of data" + row = data[0] + + # These tests only define the response + row["response body"] = row.pop("body") + row["response generators"] = row.pop("generators") + + interaction = InteractionDefinition( + method="GET", + path="/", + response="200", + **row, # type: ignore[arg-type] + ) + interaction.add_to_pact(pact, "a generators request") + return interaction + + +################################################################################ +## When +################################################################################ + + +@when("the request is prepared for use", target_fixture="response") +def the_request_is_prepared_for_use(pact: Pact) -> requests.Response: + """The request is prepared for use.""" + with pact.serve() as srv: + response = requests.get( + str(srv.url), + timeout=2, + ) + response.raise_for_status() + + return response + + +################################################################################ +## Then +################################################################################ + +GENERATOR_PATTERN: dict[str, Callable[[object], bool]] = { + "UUID": lambda v: isinstance(v, str) + and re.match(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", v) + is not None, + "boolean": lambda v: isinstance(v, bool), + "date": lambda v: isinstance(v, str) + and re.match(r"^\d{4}-\d{2}-\d{2}$", v) is not None, + "time": lambda v: isinstance(v, str) + and re.match(r"^\d{2}:\d{2}:\d{2}(\.\d+)?$", v) is not None, + "date-time": lambda v: isinstance(v, str) + and re.match( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$", v + ) + is not None, + "decimal number": lambda v: isinstance(v, str) + and re.match(r"^-?\d+\.\d+$", v) is not None, + "hexadecimal number": lambda v: isinstance(v, str) + and re.match(r"^[0-9a-fA-F]+$", v) is not None, + "integer": lambda v: isinstance(v, int) or (isinstance(v, str) and v.isdigit()), + "random string": lambda v: isinstance(v, str) and len(v) > 0, + "string from the regex": lambda v: isinstance(v, str) + and re.match(r"^\d{1,8}$", v) is not None, +} + + +@then( + parsers.re( + r'the body value for "\$\.one" ' + r'will have been replaced with a "(?P[^"]+)"' + ) +) +def the_body_value_will_have_been_replaced( + response: requests.Response, + value: str, +) -> None: + """The body value for "$.one" will have been replaced with a value.""" + data = response.json() + logger.info("Response JSON: %r", data) + + assert "one" in data, 'Response body does not contain key "one"' + assert value in GENERATOR_PATTERN, f"Unknown generator type {value!r}" + assert GENERATOR_PATTERN[value](data["one"]) From b4b2d76b04fd562d864544a38d454e6eff0c0359 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 3 Sep 2025 16:55:52 +1000 Subject: [PATCH 0989/1376] chore(test): add v4 generators tests Signed-off-by: JP-Ellis --- tests/.ruff.toml | 1 + .../compatibility_suite/test_v4_generators.py | 311 ++++++++++++++++++ 2 files changed, 312 insertions(+) create mode 100644 tests/compatibility_suite/test_v4_generators.py diff --git a/tests/.ruff.toml b/tests/.ruff.toml index 4fcff4b2c..c5797dca2 100644 --- a/tests/.ruff.toml +++ b/tests/.ruff.toml @@ -7,6 +7,7 @@ ignore = [ "D104", # Require docstring in public package "INP001", # Forbid implicit namespaces "PLR2004", # Forbid Magic Numbers + "RUF018", # Forbid assignment in assertions "S101", # Forbid assert statements "TID252", # Require absolute imports ] diff --git a/tests/compatibility_suite/test_v4_generators.py b/tests/compatibility_suite/test_v4_generators.py new file mode 100644 index 000000000..760098646 --- /dev/null +++ b/tests/compatibility_suite/test_v4_generators.py @@ -0,0 +1,311 @@ +"""Test of V4 generators.""" + +from __future__ import annotations + +import json +import logging +import re +from contextvars import ContextVar +from typing import TYPE_CHECKING, Literal + +import pytest +import requests +from pytest_bdd import ( + given, + parsers, + scenario, + then, + when, +) + +from pact import Pact, Verifier +from tests.compatibility_suite.util import parse_horizontal_table +from tests.compatibility_suite.util.interaction_definition import ( + InteractionDefinition, + InteractionState, +) +from tests.compatibility_suite.util.provider import Provider + +if TYPE_CHECKING: + from collections.abc import Callable + from pathlib import Path + +logger = logging.getLogger(__name__) + +SERVER_URL: ContextVar[str | None] = ContextVar("SERVER_URL", default=None) + + +@pytest.fixture +def pact() -> Pact: + return Pact( + "v4-generators-consumer", + "v4-generators-provider", + ).with_specification("V4") + + +@pytest.fixture +def verifier() -> Verifier: + return Verifier("v4-generators-provider") + + +def test_provider_state_generator( + pact: Pact, tmp_path: Path, verifier: Verifier +) -> None: + """Test the provider state generator.""" + ( + pact.upon_receiving("a generators request") + .given("a provider state exists", {"id": 1000}) + .with_request("POST", "/") + .with_body({"one": "a", "two": "b"}) + .with_generators({ + "body": { + "$.one": { + "type": "ProviderState", + "expression": "${id}", + } + } + }) + .will_respond_with(200) + ) + + pacts_path = tmp_path / "pacts" + pacts_path.mkdir(exist_ok=True, parents=True) + pact.write_file(pacts_path) + verifier.add_source(pacts_path) + + provider = Provider() + provider.add_interaction( + InteractionDefinition( + method="POST", + path="/", + response="200", + ) + ) + verifier.add_transport(url=provider.url) + + with provider: + verifier.verify() + + assert provider.requests + assert len(provider.requests) == 1 + request = provider.requests[0] + assert request["body"] + assert json.loads(request["body"]) == {"one": 1000, "two": "b"} + + +################################################################################ +## Scenario +################################################################################ + + +@scenario( + "definition/features/V4/generators.feature", + "Supports a Mock server URL generator", +) +def test_supports_a_mock_server_url_generator() -> None: + """Supports a Mock server URL generator.""" + + +@pytest.mark.skip(reason="Manually implemented outside of pytest-bdd") +@scenario( + "definition/features/V4/generators.feature", + "Supports a Provider State generator", +) +def test_supports_a_provider_state_generator() -> None: + """Supports a Provider State generator.""" + + +@scenario( + "definition/features/V4/generators.feature", + "Supports a URN UUID generator", +) +def test_supports_a_urn_uuid_generator() -> None: + """Supports a URN UUID generator.""" + + +@scenario( + "definition/features/V4/generators.feature", + "Supports a lower-case-hyphenated UUID generator", +) +def test_supports_a_lowercasehyphenated_uuid_generator() -> None: + """Supports a lower-case-hyphenated UUID generator.""" + + +@scenario( + "definition/features/V4/generators.feature", + "Supports a simple UUID generator", +) +def test_supports_a_simple_uuid_generator() -> None: + """Supports a simple UUID generator.""" + + +@scenario( + "definition/features/V4/generators.feature", + "Supports a upper-case-hyphenated UUID generator", +) +def test_supports_a_uppercasehyphenated_uuid_generator() -> None: + """Supports a upper-case-hyphenated UUID generator.""" + + +################################################################################ +## Scenario +################################################################################ + + +@given( + "a request configured with the following generators:", + target_fixture="interaction", +) +def a_request_configured_with_the_following_generators( + pact: Pact, + datatable: list[list[str]], +) -> InteractionDefinition: + """A request configured with the following generators.""" + data = parse_horizontal_table(datatable) + assert len(data) == 1, "Expected a single row of data" + row = data[0] + + # These tests only define the response + row["response body"] = row.pop("body") + row["response generators"] = row.pop("generators") + + interaction = InteractionDefinition( + method="GET", + path="/", + response="200", + **row, # type: ignore[arg-type] + ) + interaction.states.append(InteractionState("a provider state exists", {"id": 1000})) + interaction.add_to_pact(pact, "a generators request") + return interaction + + +@given( + parsers.re(r'the generator test mode is set as "(?PConsumer|Provider)"'), + target_fixture="mode", +) +def the_generator_test_mode_is_set( + mode: Literal["Consumer", "Provider"], +) -> Literal["Consumer", "Provider"]: + """The generator test mode is set.""" + return mode + + +################################################################################ +## When +################################################################################ + + +@when("the request is prepared for use", target_fixture="response") +def the_request_is_prepared_for_use(pact: Pact) -> requests.Response: + """The request is prepared for use.""" + with pact.serve() as srv: + response = requests.get( + str(srv.url), + timeout=2, + ) + response.raise_for_status() + + return response + + +@when( + parsers.re( + r"the request is prepared for use " + r'with a "(?PmockServer)" context:' + ), + target_fixture="response", +) +def the_request_is_prepared_with_context( + pact: Pact, + context: Literal["mockServer"], + datatable: list[list[str]], +) -> requests.Response | Provider: + """The request is prepared for use with a context:.""" + if context == "mockServer": + data = json.loads(datatable[0][0]) + assert data + with pact.serve() as srv: + SERVER_URL.set(str(srv.url)) + response = requests.get( + str(srv.url), + timeout=2, + ) + response.raise_for_status() + + return response + + msg = f"Unknown context {context!r}" + raise ValueError(msg) + + +################################################################################ +## Then +################################################################################ + + +GENERATOR_PATTERN: dict[str, Callable[[object], bool]] = { + "upper-case-hyphenated UUID": lambda v: isinstance(v, str) + and re.match(r"^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", v) + is not None, + "lower-case-hyphenated UUID": lambda v: isinstance(v, str) + and re.match(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", v) + is not None, + "simple UUID": lambda v: isinstance(v, str) + and re.match(r"^[0-9a-fA-F]{32}$", v) is not None, + "URN UUID": lambda v: isinstance(v, str) + and re.match( + r"^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", v + ) + is not None, +} + + +@then( + parsers.re( + r'the body value for "\$\.one" ' + r'will have been replaced with a "(?P[^"]+)"' + ) +) +def the_body_value_will_have_been_replaced( + response: requests.Response, + value: str, +) -> None: + """The body value for "$.one" will have been replaced with a value.""" + data = response.json() + logger.info("Response JSON: %r", data) + + assert "one" in data, 'Response body does not contain key "one"' + assert value in GENERATOR_PATTERN, f"Unknown generator type {value!r}" + assert GENERATOR_PATTERN[value](data["one"]) + + +@then( + parsers.re( + r'the body value for "\$\.one" will have been replaced with "(?P[^"]+)"' + ) +) +def the_body_value_will_have_been_replaced_with_value( + response: requests.Response | Provider, +) -> None: + """The body value for "$.one" will have been replaced.""" + assert isinstance(response, requests.Response) + data = response.json() + logger.info("Response JSON: %r", data) + + assert "one" in data, 'Response body does not contain key "one"' + assert (url := SERVER_URL.get()) + logger.info("Server URL: %r", url) + # Note: IPv6 requires the square brackets, but there is currently a bug in + # the mock server that may result in the brackets being omitted. + url_pattern = re.escape(url).replace( + r"localhost", r"(127\.0\.0\.1|\[?::1\]?|localhost)" + ) + logger.info("URL Pattern: %r", url_pattern) + assert ( + re.match( + url_pattern, + data["one"], + ) + is not None + ) From 2a549261c6dae131eba411d49499a6096420f195 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 4 Sep 2025 09:49:04 +1000 Subject: [PATCH 0990/1376] chore: re-add pytest rerunfailrure There are some tests which are flakey, especially when it comes to allocate ports. Signed-off-by: JP-Ellis --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index a2fa9426e..5ae9467d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,6 +111,7 @@ test = [ "pytest-asyncio~=1.0", "pytest-bdd~=8.0", "pytest-cov~=6.0", + "pytest-rerunfailures~=16.0", "pytest~=8.0", "requests~=2.0", "testcontainers~=4.0", @@ -294,6 +295,7 @@ requires = ["hatch-vcs", "hatchling"] "ignore::DeprecationWarning:tests", ] pythonpath = "." + reruns = 3 log_date_format = "%H:%M:%S" log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" From 9f828f2bbc29fec4c89ac200e1ab0772cd35d5d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:34:10 +1000 Subject: [PATCH 0991/1376] chore(deps): update astral-sh/setup-uv action to v6.6.1 (#1218) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 04a56bd42..0f359390f 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index c4b9db3f3..7e1f95a1a 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f64836e78..fb42fb2a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 14d884b1a..0b0a829b5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 24a891e9b..f289cc341 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -83,7 +83,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 with: enable-cache: true @@ -151,7 +151,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 with: enable-cache: true @@ -194,7 +194,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 with: enable-cache: true @@ -226,7 +226,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 with: enable-cache: true @@ -259,7 +259,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 with: enable-cache: true @@ -298,7 +298,7 @@ jobs: key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Set up uv - uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 with: enable-cache: true cache-suffix: pre-commit From 98dce4aabcab55fefbe8ff64bf5cdf56ad04574e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 00:44:55 +0000 Subject: [PATCH 0992/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.36.1 (#1216) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ed7f6b621..dccb48805 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,7 +78,7 @@ repos: )$ - repo: https://github.com/crate-ci/typos - rev: v1.35.6 + rev: v1.36.1 hooks: - id: typos exclude: | From f06871e8be8062ab60be2054e68cd98565d2ca1a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 4 Sep 2025 15:21:24 +1000 Subject: [PATCH 0993/1376] style(tests): add sections Signed-off-by: JP-Ellis --- tests/compatibility_suite/test_v3_generators.py | 2 +- tests/compatibility_suite/test_v4_matching_rules.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/compatibility_suite/test_v3_generators.py b/tests/compatibility_suite/test_v3_generators.py index 8ebfae973..3e5ec82ef 100644 --- a/tests/compatibility_suite/test_v3_generators.py +++ b/tests/compatibility_suite/test_v3_generators.py @@ -94,7 +94,7 @@ def test_supports_a_time_generator() -> None: ################################################################################ -## Scenario +## Given ################################################################################ diff --git a/tests/compatibility_suite/test_v4_matching_rules.py b/tests/compatibility_suite/test_v4_matching_rules.py index 608869462..f7f4ea741 100644 --- a/tests/compatibility_suite/test_v4_matching_rules.py +++ b/tests/compatibility_suite/test_v4_matching_rules.py @@ -55,6 +55,11 @@ def verifier() -> Verifier: return Verifier("v4-matching-rules-provider") +################################################################################ +## Scenario +################################################################################ + + @scenario( "definition/features/V4/matching_rules.feature", "Supports a ArrayContains matcher (negative case)", From fdc72bbb925e4f4ccab8a11784ea41a8bdbc5433 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 4 Sep 2025 15:25:14 +1000 Subject: [PATCH 0994/1376] chore(tests): add v3 http generators Signed-off-by: JP-Ellis --- .../test_v3_http_generators.py | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 tests/compatibility_suite/test_v3_http_generators.py diff --git a/tests/compatibility_suite/test_v3_http_generators.py b/tests/compatibility_suite/test_v3_http_generators.py new file mode 100644 index 000000000..7a5c36526 --- /dev/null +++ b/tests/compatibility_suite/test_v3_http_generators.py @@ -0,0 +1,314 @@ +"""Test V3 HTTP generators.""" + +from __future__ import annotations + +import contextlib +import json +import re +from typing import TYPE_CHECKING, Literal +from urllib.parse import parse_qs + +import pytest +import requests +from pytest_bdd import ( + given, + parsers, + scenario, + then, + when, +) + +from pact import Pact, Verifier +from tests.compatibility_suite.util import parse_horizontal_table +from tests.compatibility_suite.util.interaction_definition import ( + InteractionDefinition, + InteractionState, +) +from tests.compatibility_suite.util.provider import Provider + +if TYPE_CHECKING: + from collections.abc import Callable + from pathlib import Path + + +@pytest.fixture +def pact() -> Pact: + return Pact( + "v3-http-generators-consumer", + "v3-http-generators-provider", + ).with_specification("V3") + + +@pytest.fixture +def verifier() -> Verifier: + return Verifier("v3-http-generators-provider") + + +@pytest.fixture +def response() -> requests.Response | None: + """ + Default response, which gets overridden when needed. + """ + return None + + +################################################################################ +## Scenario +################################################################################ + + +@scenario( + "definition/features/V3/http_generators.feature", + "Supports using a generator with the request body", +) +def test_supports_using_a_generator_with_the_request_body() -> None: + """Supports using a generator with the request body.""" + + +@scenario( + "definition/features/V3/http_generators.feature", + "Supports using a generator with the request headers", +) +def test_supports_using_a_generator_with_the_request_headers() -> None: + """Supports using a generator with the request headers.""" + + +@scenario( + "definition/features/V3/http_generators.feature", + "Supports using a generator with the request path", +) +def test_supports_using_a_generator_with_the_request_path() -> None: + """Supports using a generator with the request path.""" + + +@scenario( + "definition/features/V3/http_generators.feature", + "Supports using a generator with the request query parameters", +) +def test_supports_using_a_generator_with_the_request_query_parameters() -> None: + """Supports using a generator with the request query parameters.""" + + +@scenario( + "definition/features/V3/http_generators.feature", + "Supports using a generator with the response body", +) +def test_supports_using_a_generator_with_the_response_body() -> None: + """Supports using a generator with the response body.""" + + +@scenario( + "definition/features/V3/http_generators.feature", + "Supports using a generator with the response headers", +) +def test_supports_using_a_generator_with_the_response_headers() -> None: + """Supports using a generator with the response headers.""" + + +@scenario( + "definition/features/V3/http_generators.feature", + "Supports using a generator with the response status", +) +def test_supports_using_a_generator_with_the_response_status() -> None: + """Supports using a generator with the response status.""" + + +################################################################################ +## Given +################################################################################ + + +@given( + parsers.re( + r"a (?Prequest|response) configured with the following generators:" + ), + target_fixture="provider", +) +def a_request_configured_with_the_following_generators( + part: Literal["request", "response"], + tmp_path: Path, + pact: Pact, + verifier: Verifier, + datatable: list[list[str]], +) -> Provider: + """A request configured with the following generators.""" + data = parse_horizontal_table(datatable) + assert len(data) == 1, "Expected a single row of data" + row: dict[str, str | None] = data[0] # type: ignore[assignment] + + if part == "response": + row["response generators"] = row.pop("generators") + if body := row.pop("body", None): + row["response body"] = body + + interaction = InteractionDefinition( + method="POST", + path="/", + response="200", + **row, # type: ignore[arg-type] + ) + interaction.states.append(InteractionState("a provider state exists", {"id": 1000})) + interaction.add_to_pact(pact, "a generators request") + + provider = Provider() + provider.add_interaction(interaction) + + pacts_path = tmp_path / "pacts" + pacts_path.mkdir(exist_ok=True, parents=True) + pact.write_file(pacts_path) + verifier.add_source(pacts_path) + + # with provider: + # yield provider + return provider + + +@given('the generator test mode is set as "Provider"') +def the_generator_test_mode_is_set_as_provider() -> None: + """The generator test mode is set as "Provider".""" + + +################################################################################ +## When +################################################################################ + + +@when(parsers.re("the request is prepared for use.*")) +def the_request_is_prepared_for_use( + verifier: Verifier, + provider: Provider, +) -> None: + """The request is prepared for use.""" + verifier.add_transport(url=provider.url) + + with provider, contextlib.suppress(RuntimeError): + verifier.verify() + + +@when("the response is prepared for use", target_fixture="response") +def the_response_is_prepared_for_use( + pact: Pact, +) -> requests.Response: + """The response is prepared for use.""" + with pact.serve() as srv: + return requests.post( + str(srv.url), + json={"one": "1"}, + headers={"Content-Type": "application/json"}, + timeout=2, + ) + + +################################################################################ +## Then +################################################################################ + + +GENERATOR_PATTERN: dict[str, Callable[[object], bool]] = { + "integer": lambda v: isinstance(v, int) or (isinstance(v, str) and v.isdigit()), +} + + +@then( + parsers.re( + r'the body value for "\$\.one" ' + r'will have been replaced with an? "(?P[^"]+)"' + ) +) +def the_body_value_will_have_been_replaced( + value: str, + provider: Provider, + response: requests.Response | None, +) -> None: + """The body value for "$.one" will have been replaced with a value.""" + assert provider.requests or response + + if provider.requests: + request = provider.requests[0] + + assert (body := request["body"]) + assert (data := json.loads(body)) + assert (one := data.get("one")) + assert value in GENERATOR_PATTERN + assert GENERATOR_PATTERN[value](one) + + if response: + data = response.json() + assert "one" in data + assert value in GENERATOR_PATTERN + assert GENERATOR_PATTERN[value](data["one"]) + + +@then( + parsers.re( + r'the request "(?PqueryParameter|header)\[(?P[^\]]+)\]" ' + r'will match "(?P[^"]+)"' + ) +) +def the_request_header_will_match( + part: Literal["queryParameter", "header"], + name: str, + pattern: str, + provider: Provider, +) -> None: + """The request header will match.""" + assert provider.requests + request = provider.requests[0] + + if part == "queryParameter": + assert (query := request["query"]) + query_dict = parse_qs(query) + assert (value := query_dict.get(name)) + assert re.match(pattern, value[0]) + return + + if part == "header": + assert (headers := request["headers"]) + assert (value := headers.get(name)) # type: ignore[assignment] + assert value is not None + assert re.match(pattern, value) + + +@then( + parsers.re( + r'the response "header\[(?P[^\]]+)\]" will match "(?P[^"]+)"' + ) +) +def the_response_header_will_match( + name: str, + pattern: str, + response: requests.Response | None, +) -> None: + """The response header will match the given pattern.""" + assert response is not None + value = response.headers.get(name) + assert value is not None + assert re.match(pattern, value) + + +@then(parsers.re(r'the request "path" will be set as "(?P[^"]+)"')) +def the_request_path_will_be_set_as(path: str, provider: Provider) -> None: + """The request "path" will be set as "/path/1000".""" + assert provider.requests + request = provider.requests[0] + + assert request.get("path") == path + + +@then(parsers.re(r'the response "status" will match "(?P[^"]+)"')) +def the_response_status_will_match( + pattern: str, response: requests.Response | None +) -> None: + """The response "status" will match a given pattern.""" + assert response is not None + status_code = str(response.status_code) + assert re.match(pattern, status_code) + + +@then(parsers.re(r'the response "status" will not be "(?P\d+)"')) +def the_response_status_will_not_be_200( + status: str, response: requests.Response | None +) -> None: + """The response "status" will not be the given status.""" + assert response is not None + assert str(response.status_code) != status From 4e293b2da3581eacffbab14e1e31c8e6e67836ff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 20:44:26 +0000 Subject: [PATCH 0995/1376] chore(deps): update codecov/codecov-action action to v5.5.1 (#1223) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f289cc341..fe232d336 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -171,7 +171,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0 + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} flags: examples From f33d2c2174f9a62ae147694651e5f23210cd1f74 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:16:18 +1000 Subject: [PATCH 0996/1376] chore(deps): update ruff to v0.12.12 (#1224) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dccb48805..c0acb6d1f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.11 + rev: v0.12.12 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index fb4c9bdd4..1413b14e5 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.9" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.12.11", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.12.12", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=6.0", "pytest~=8.0"] types = ["mypy==1.17.1"] From 6eea0ee7308d9f8603454dc61844006c7c447a62 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:16:24 +1000 Subject: [PATCH 0997/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.36.2 (#1225) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c0acb6d1f..0d77e1062 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,7 +78,7 @@ repos: )$ - repo: https://github.com/crate-ci/typos - rev: v1.36.1 + rev: v1.36.2 hooks: - id: typos exclude: | From 1cdfd1005199bb520c157a2926266736de5e978d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:16:59 +1000 Subject: [PATCH 0998/1376] chore(deps): update pypa/gh-action-pypi-publish action to v1.13.0 (#1220) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 0f359390f..d049e6fd4 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -180,7 +180,7 @@ jobs: generate_release_notes: true - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: skip-existing: true packages-dir: wheelhouse diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 7e1f95a1a..69ab8076b 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -181,7 +181,7 @@ jobs: generate_release_notes: true - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: skip-existing: true packages-dir: wheelhouse diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fb42fb2a6..3476195e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -147,7 +147,7 @@ jobs: generate_release_notes: true - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: skip-existing: true packages-dir: wheelhouse From 574053297cf1b9131348925e233167e0be58f345 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Sep 2025 09:40:51 +0000 Subject: [PATCH 0999/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.2.3 (#1226) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d77e1062..474cfe7b5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.2.2 + rev: v2.2.3 hooks: - id: biome-check From d9020dfe472bb7dd4c8b5e9005185edea7a9c2ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 09:59:58 +0000 Subject: [PATCH 1000/1376] chore(deps): update softprops/action-gh-release action to v2.3.3 (#1227) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index d049e6fd4..4515b3c04 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -171,7 +171,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 + uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 69ab8076b..21740aa58 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -172,7 +172,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 + uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3476195e8..d24a6f3a9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,7 +138,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 + uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md From 5b8bfcebe6c44e066cb2bd55a835a3956096b0ef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 00:48:34 +0000 Subject: [PATCH 1001/1376] chore(deps): update taiki-e/install-action action to v2.59.1 (#1228) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 4515b3c04..3fdb012e9 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21 + uses: taiki-e/install-action@57511bcdf8cdb0eab6448cb7fa632952d9f25742 # v2.59.1 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 21740aa58..4050f9fb7 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21 + uses: taiki-e/install-action@57511bcdf8cdb0eab6448cb7fa632952d9f25742 # v2.59.1 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d24a6f3a9..4a29a0ebb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21 + uses: taiki-e/install-action@57511bcdf8cdb0eab6448cb7fa632952d9f25742 # v2.59.1 with: tool: git-cliff,typos From 08005fda3889e802ca9f49825c7c13bdbd1553d2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 05:24:49 +0000 Subject: [PATCH 1002/1376] chore(deps): update dependency cffi to v2 (#1229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 017d4c8f2..d0ebc2e06 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ requires-python = ">=3.9" -dependencies = ["cffi~=1.0"] +dependencies = ["cffi~=2.0"] [project.urls] "Bug Tracker" = "https://github.com/pact-foundation/pact-python/issues" diff --git a/pyproject.toml b/pyproject.toml index 5ae9467d4..934b74e03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ # Pact dependencies "pact-python-ffi~=0.4.0", # External dependencies - "cffi~=1.0", + "cffi~=2.0", "yarl~=1.0", "typing-extensions~=4.0 ; python_version < '3.10'", ] From 6e7084b44d81d2e96ea11c9cc5d31ebf15cdcf8f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 18:07:59 +0000 Subject: [PATCH 1003/1376] chore(deps): update dependency pytest-cov to v7 (#1230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pact-python-cli/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 1413b14e5..c3d117ac5 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -52,7 +52,7 @@ requires-python = ">=3.9" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = ["ruff==0.12.12", { include-group = "test" }, { include-group = "types" }] -test = ["pytest-cov~=6.0", "pytest~=8.0"] +test = ["pytest-cov~=7.0", "pytest~=8.0"] types = ["mypy==1.17.1"] ################################################################################ From c9adee7f59a539b545aa7adc58681025a38510e7 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 11 Sep 2025 10:38:33 +1000 Subject: [PATCH 1004/1376] chore: prefer prek over pre-commit Prek is an alternative to `pre-commit` which is implemented in Rust and has a large number of improvements (most of all, the main developer behind it is actually open to suggestions). Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 32 ++++++++++++++------------------ .pre-commit-config.yaml | 1 - CONTRIBUTING.md | 4 ++-- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe232d336..0fa3709e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: - format - lint - typecheck - - pre-commit + - prek steps: - name: Failed @@ -276,36 +276,32 @@ jobs: working-directory: pact-python-cli run: hatch run typecheck - pre-commit: - name: Pre-commit + prek: + name: Prek (pre-commit) runs-on: ubuntu-latest - env: - PRE_COMMIT_HOME: ${{ github.workspace }}/.pre-commit - steps: - - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Cache pre-commit + - name: Cache prek uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: | - ${{ env.PRE_COMMIT_HOME }} - key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + $HOME/.cache/prek + key: ${{ runner.os }}-prek-${{ hashFiles('.pre-commit-config.yaml') }} - name: Set up uv uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 with: enable-cache: true - cache-suffix: pre-commit + cache-suffix: prek cache-dependency-glob: '' - - name: Install pre-commit - run: uv tool install pre-commit + - name: Install prek + run: uv tool install prek + - name: Install hatch + run: uv tool install hatch - - name: Run pre-commit - run: pre-commit run --show-diff-on-failure --color=always --all-files + - name: Run prek + run: prek run --show-diff-on-failure --color=always --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 474cfe7b5..acdd20cc7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,5 @@ --- default_install_hook_types: - - commit-msg - pre-commit - pre-push diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 84b4b5098..8fdddca94 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -113,10 +113,10 @@ You can also try using the new [github.dev](https://github.dev/pact-foundation/p Don't worry too much about styles in general—the maintainers will help you fix them as they review your code. -To help catch a lot of simple formatting or linting issues, you can run `hatch run lint` to run the linter and `hatch run format` to format your code. This process can also be automated by installing [`pre-commit`](https://pre-commit.com/) hooks: +To help catch a lot of simple formatting or linting issues, you can run `hatch run lint` to run the linter and `hatch run format` to format your code. This process can also be automated by installing [`prek`](https://prek.j178.dev/) to manage pre-commit hooks: ```sh -pre-commit install +prek install ``` ## Pull Requests From 23b31b683ebd88b475c969f0b6a9a130fc4c9134 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 11 Sep 2025 10:42:01 +1000 Subject: [PATCH 1005/1376] chore: disable reruns in vscode Signed-off-by: JP-Ellis --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index bb00d41ff..1688b7440 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "python.testing.pytestArgs": [ + "--reruns=0", "--no-cov", "--ignore=examples/v2", "--ignore=tests/v2", From b4af43090c958e3386019ba0d53a9166bb809c25 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 11:02:56 +1000 Subject: [PATCH 1006/1376] chore(deps): update ruff to v0.13.0 (#1231) Co-authored-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- src/pact/interaction/_base.py | 4 ++-- tests/test_pact.py | 5 ++++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index acdd20cc7..0a5af7eda 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.12 + rev: v0.13.0 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index c3d117ac5..65269c332 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.9" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.12.12", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.13.0", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=8.0"] types = ["mypy==1.17.1"] diff --git a/src/pact/interaction/_base.py b/src/pact/interaction/_base.py index 7ae068111..aaeea5be1 100644 --- a/src/pact/interaction/_base.py +++ b/src/pact/interaction/_base.py @@ -13,7 +13,7 @@ import abc import json -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Literal, Optional import pact_ffi from pact.match.matcher import IntegrationJSONEncoder @@ -103,7 +103,7 @@ def _interaction_part(self) -> pact_ffi.InteractionPart: def _parse_interaction_part( self, - part: Literal["Request", "Response", None], + part: Optional[Literal["Request", "Response"]], ) -> pact_ffi.InteractionPart: """ Convert the input into an InteractionPart. diff --git a/tests/test_pact.py b/tests/test_pact.py index 5166fd892..5fdb4e6e1 100644 --- a/tests/test_pact.py +++ b/tests/test_pact.py @@ -65,7 +65,10 @@ def test_metadata(pact: Pact) -> None: def test_invalid_interaction(pact: Pact) -> None: - with pytest.raises(ValueError, match="Invalid interaction type: .*"): + with pytest.raises( + ValueError, + match=r"Invalid interaction type: .*", + ): pact.upon_receiving("a basic request", "Invalid") # type: ignore[call-overload] From cd1c1f75c25ba22dcf1a218ab2a1a208cdfc9347 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 08:36:23 +0000 Subject: [PATCH 1007/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.2.4 (#1234) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a5af7eda..328db7f7c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.2.3 + rev: v2.2.4 hooks: - id: biome-check From da8f9c58cfdcddaaee2fbfd82e49f0757ce3baac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Sep 2025 05:27:59 +0000 Subject: [PATCH 1008/1376] chore(deps): update dependency mypy to v1.18.1 (#1235) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pact-python-cli/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 65269c332..a8b05bdae 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -53,7 +53,7 @@ requires-python = ">=3.9" # developper consistency. All other dependencies are as above. dev = ["ruff==0.13.0", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=8.0"] -types = ["mypy==1.17.1"] +types = ["mypy==1.18.1"] ################################################################################ ## Build System From c2decf55af6603a8d0dcddbb2d0dc240711a8914 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 14 Sep 2025 20:48:29 +0000 Subject: [PATCH 1009/1376] chore(deps): update astral-sh/setup-uv action to v6.7.0 (#1236) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 3fdb012e9..f97647aa9 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 + uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 4050f9fb7..4ef518343 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 + uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a29a0ebb..69f126e29 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 + uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0b0a829b5..8654c605c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 + uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0fa3709e1..2b25c53cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -83,7 +83,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 + uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0 with: enable-cache: true @@ -151,7 +151,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 + uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0 with: enable-cache: true @@ -194,7 +194,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 + uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0 with: enable-cache: true @@ -226,7 +226,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 + uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0 with: enable-cache: true @@ -259,7 +259,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 + uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0 with: enable-cache: true @@ -292,7 +292,7 @@ jobs: key: ${{ runner.os }}-prek-${{ hashFiles('.pre-commit-config.yaml') }} - name: Set up uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 + uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0 with: enable-cache: true cache-suffix: prek From ae6ac957f4fe4708e78053719b549271b2cb46e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:17:42 +1000 Subject: [PATCH 1010/1376] chore(deps): update taiki-e/install-action action to v2.61.3 (#1237) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index f97647aa9..38b9b0c2d 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@57511bcdf8cdb0eab6448cb7fa632952d9f25742 # v2.59.1 + uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 4ef518343..45c38e569 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@57511bcdf8cdb0eab6448cb7fa632952d9f25742 # v2.59.1 + uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 69f126e29..2737a77d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@57511bcdf8cdb0eab6448cb7fa632952d9f25742 # v2.59.1 + uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3 with: tool: git-cliff,typos From e9105bb67a289c7e4c6655633ee0a2412d0a986f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 24 Sep 2025 15:54:50 +1000 Subject: [PATCH 1011/1376] docs: generate llms.txt Signed-off-by: JP-Ellis --- mkdocs.yml | 9 ++++++++- pyproject.toml | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 49c757a8b..5cc941a99 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,12 +16,19 @@ plugins: nav_file: SUMMARY.md - section-index - gh-admonitions - # Library documentation - gen-files: scripts: - docs/scripts/markdown.py - docs/scripts/python.py # - docs/scripts/other.py + - llmstxt: + full_output: llms-full.txt + sections: + Usage documentation: + - api/*.md + - api/**/*.md + Examples: + - examples/*.md - mkdocstrings: default_handler: python enable_inventory: true diff --git a/pyproject.toml b/pyproject.toml index 934b74e03..6683f8bd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,7 @@ dev = [ docs = [ "mkdocs-github-admonitions-plugin~=0.0", "mkdocs-literate-nav~=0.6", + "mkdocs-llmstxt~=0.3", "mkdocs-material[recommended,git,imaging]~=9.0", "mkdocs-section-index~=0.3", "mkdocs_gen_files~=0.5", From 8c7919e19ac1248e7bc63aab93439bb83e9c06a6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 24 Sep 2025 15:55:25 +1000 Subject: [PATCH 1012/1376] docs: update mkdocs material features Signed-off-by: JP-Ellis --- mkdocs.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 5cc941a99..b5a825d3a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -129,18 +129,23 @@ theme: repo: fontawesome/brands/github features: + - content.action.edit + - content.action.view - content.code.annotate - content.code.copy - content.tooltips - navigation.indexes - navigation.instant + - navigation.instant.progress - navigation.sections - - navigation.tracking - navigation.tabs - navigation.top + - navigation.tracking + - navigation.footer - search.highlight - search.share - search.suggest + - toc.follow palette: - media: (prefers-color-scheme) From 6ee669ecb747e05a131cf65b0777ee5eb22e6dd6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 22 Sep 2025 12:52:11 +1000 Subject: [PATCH 1013/1376] chore(cli): use new standalone repo path Signed-off-by: JP-Ellis --- pact-python-cli/hatch_build.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pact-python-cli/hatch_build.py b/pact-python-cli/hatch_build.py index 30591e74a..90fc23814 100644 --- a/pact-python-cli/hatch_build.py +++ b/pact-python-cli/hatch_build.py @@ -15,6 +15,7 @@ import urllib.request import zipfile from pathlib import Path +from typing import Any from hatchling.builders.config import BuilderConfig from hatchling.builders.hooks.plugin.interface import BuildHookInterface @@ -23,7 +24,7 @@ logger = logging.getLogger(__name__) PKG_DIR = Path(__file__).parent.resolve() / "src" / "pact_cli" -PACT_CLI_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" +PACT_CLI_URL = "https://github.com/pact-foundation/pact-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" class UnsupportedPlatformError(RuntimeError): @@ -57,7 +58,7 @@ class PactCliBuildHook(BuildHookInterface[BuilderConfig]): PLUGIN_NAME = "pact-cli" - def __init__(self, *args: object, **kwargs: object) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 """ Initialize the build hook. From 68cd657abaea07c92d5a9ca1be85bb6ce03952d4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 07:31:25 +0000 Subject: [PATCH 1014/1376] chore(deps): update dependency mypy to v1.18.2 (#1239) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pact-python-cli/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index a8b05bdae..c92e76e09 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -53,7 +53,7 @@ requires-python = ">=3.9" # developper consistency. All other dependencies are as above. dev = ["ruff==0.13.0", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=8.0"] -types = ["mypy==1.18.1"] +types = ["mypy==1.18.2"] ################################################################################ ## Build System From 5cdd8da6c5e8a8c64cfa5f148f707bd63245fab2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 07:32:17 +0000 Subject: [PATCH 1015/1376] chore(deps): update taiki-e/install-action action to v2.62.5 (#1240) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 38b9b0c2d..4f592b7e9 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3 + uses: taiki-e/install-action@6f69ec9970ed0c500b1b76d648e05c4c7e0e5671 # v2.62.5 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 45c38e569..9cbe099ec 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3 + uses: taiki-e/install-action@6f69ec9970ed0c500b1b76d648e05c4c7e0e5671 # v2.62.5 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2737a77d9..b9afd893f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3 + uses: taiki-e/install-action@6f69ec9970ed0c500b1b76d648e05c4c7e0e5671 # v2.62.5 with: tool: git-cliff,typos From 10c56e690e56a56c7c5a536a77b6689df0f09f98 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 08:38:09 +0000 Subject: [PATCH 1016/1376] chore(deps): update ruff to v0.13.1 (#1238) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 328db7f7c..c366c539d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.0 + rev: v0.13.1 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index c92e76e09..4c5acfd91 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.9" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.13.0", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.13.1", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=8.0"] types = ["mypy==1.18.2"] From f11facda47d2c292c426ddf0a902e2b634588647 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 08:40:28 +0000 Subject: [PATCH 1017/1376] chore(deps): update pypa/cibuildwheel action to v3.2.0 (#1242) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 4f592b7e9..f9ea6366e 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 + uses: pypa/cibuildwheel@7c619efba910c04005a835b110b057fc28fd6e93 # v3.2.0 with: package-dir: pact-python-cli env: diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 9cbe099ec..b6c99d9e8 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 + uses: pypa/cibuildwheel@7c619efba910c04005a835b110b057fc28fd6e93 # v3.2.0 with: package-dir: pact-python-ffi env: From 339d5c845a5e96627f74496f91a352ac886d79a9 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 25 Sep 2025 12:04:53 +1000 Subject: [PATCH 1018/1376] chore(ci): fix prek caching The previous caching did not work as expected. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b25c53cc..fa868fdfe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -287,16 +287,25 @@ jobs: - name: Cache prek uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: - path: | - $HOME/.cache/prek - key: ${{ runner.os }}-prek-${{ hashFiles('.pre-commit-config.yaml') }} + path: |- + ~/.cache/prek + key: >- + ${{ runner.os }}-prek-${{ + hashFiles( + '**/.pre-commit-config.yaml', + '**/.pre-commit-config.yml' + ) }} + restore-keys: |- + ${{ runner.os }}-prek- - name: Set up uv uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0 with: enable-cache: true cache-suffix: prek - cache-dependency-glob: '' + cache-dependency-glob: |- + **/.pre-commit-config.yaml + **/.pre-commit-config.yml - name: Install prek run: uv tool install prek From d79d1a0e6304b03f465783a8c16a2b762ea9a63c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 25 Sep 2025 12:18:28 +1000 Subject: [PATCH 1019/1376] chore(ci): generate junit xml files Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fa868fdfe..7815a85d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,18 +103,31 @@ jobs: run: uv tool install hatch - name: Run tests - run: hatch run test.py${{ matrix.python-version }}:test + run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml - name: Run tests (v2) - run: hatch run v2-test.py${{ matrix.python-version }}:test + run: hatch run v2-test.py${{ matrix.python-version }}:test --junit-xml=v2-junit.xml - name: Run tests (CLI) working-directory: pact-python-cli - run: hatch run test.py${{ matrix.python-version }}:test + run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml - name: Run tests (FFI) working-directory: pact-python-ffi - run: hatch run test.py${{ matrix.python-version }}:test + run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml + + - name: Upload coverage + if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: tests + + - name: Upload test results + if: ${{ !cancelled() }} + uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} example: name: >- @@ -166,7 +179,7 @@ jobs: while IFS= read -r -d $'\0' file <&3; do cd "$(dirname "$file")" - uv run --python ${{ matrix.python-version }} --group test pytest + uv run --python ${{ matrix.python-version }} --group test pytest --junit-xml=junit.xml done 3< <(find "$(pwd)/examples" -name pyproject.toml -print0) - name: Upload coverage From 68fb7ebe9aca8d702370889cfc3122dedfaf3993 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 05:00:44 +0000 Subject: [PATCH 1020/1376] chore(deps): update actions/cache action to v4.3.0 (#1245) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7815a85d1..b80c8cbc4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -298,7 +298,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Cache prek - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: |- ~/.cache/prek From 5009ab6cd298c83f62e42827165a05295a868b79 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:23:44 +1000 Subject: [PATCH 1021/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.36.3 (#1248) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c366c539d..481ca7234 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -77,7 +77,7 @@ repos: )$ - repo: https://github.com/crate-ci/typos - rev: v1.36.2 + rev: v1.36.3 hooks: - id: typos exclude: | From 65cf3bc1d7613ef16f91f47f715b4cd1c42e99b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:22:31 +1000 Subject: [PATCH 1022/1376] chore(deps): update ruff to v0.13.2 (#1249) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 481ca7234..ec3da5673 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.1 + rev: v0.13.2 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 4c5acfd91..25921029e 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.9" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.13.1", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.13.2", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=8.0"] types = ["mypy==1.18.2"] From 9ad21b0c717436099993ba9d0672af79099a5b65 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 26 Sep 2025 16:05:47 +1000 Subject: [PATCH 1023/1376] chore: move mascot file out of root Signed-off-by: JP-Ellis --- README.md | 2 +- mascot.svg => docs/img/mascot.svg | 0 pyproject.toml | 3 +++ 3 files changed, 4 insertions(+), 1 deletion(-) rename mascot.svg => docs/img/mascot.svg (100%) diff --git a/README.md b/README.md index 1284d8e66..8739eb03b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
- Pact Python Mascot diff --git a/mascot.svg b/docs/img/mascot.svg similarity index 100% rename from mascot.svg rename to docs/img/mascot.svg diff --git a/pyproject.toml b/pyproject.toml index 6683f8bd2..cba29fc57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -389,3 +389,6 @@ exclude = """(?x)^( [tool.typos.default] extend-ignore-re = ['(?Rm)^.*(#|//| + +=== "Local Files" + + ```python title="v2" + success, logs = verifier.verify_pacts( + './pacts/consumer1-provider.json', + './pacts/consumer2-provider.json' + ) + ``` + + ```python title="v3" + verifier = ( + Verifier('my-provider') + # It can discover all Pact files in a directory + .add_source('./pacts/') + # Or read individual files + .add_source('./pacts/specific-consumer.json') + ) + ``` + +=== "Pact Broker" + + ```python title="v2" + success, logs = verifier.verify_with_broker( + broker_url='https://pact-broker.example.com', + broker_username='username', + broker_password='password' + ) + ``` + + ```python title="v3" + verifier = ( + Verifier('my-provider') + .broker_source( + 'https://pact-broker.example.com', + username='username', + password='password' + ) + ) + + # Or with selectors for more control + broker_builder = ( + verifier + .broker_source( + 'https://pact-broker.example.com', + selector=True + ) + .include_pending() + .provider_branch('main') + .consumer_tags('main', 'develop') + .build() + ) + ``` + + The `selector=True` argument returns a [`BrokerSelectorBuilder`][pact.verifier.BrokerSelectorBuilder] instance, which provides methods to configure which pacts to fetch. The `build()` call finalizes the configuration and returns the `Verifier` instance which can then be further configured. + + + +#### Provider State Handling + +The old v2 API required the provider to expose an HTTP endpoint dedicated to handling provider states. This is still supported in v3, but there are now more flexible options, allowing Python functions (or mappings of state names to functions) to be used instead. + + + +=== "URL-based State Handling" + + ```python title="v2" + success, logs = verifier.verify_pacts( + './pacts/consumer-provider.json', + provider_states_setup_url='http://localhost:8080/_pact/provider_states' + ) + ``` + + ```python title="v3" + # Option 1: URL-based (similar to v2) + verifier = ( + Verifier('my-provider') + .add_transport(url='http://localhost:8080') + .state_handler( + 'http://localhost:8080/_pact/provider_states', + body=True # (1) + ) + .add_source('./pacts/') + ) + ``` + + 1. The `body` argument specifies whether to use a `POST` request and pass information in the body, or to use a `GET` request and pass information through HTTP headers. For more details, see the [`state_handler` API documentation][pact.verifier.Verifier.state_handler]. + +=== "Functional State Handling" + + ```python title="v2" + # Not supported + ``` + + ```python title="v3 - Function" + def handler(name, params=None): + if name == 'user exists': + # Set up user in database/mock + create_user(params.get('id', 123)) + elif name == 'no users exist': + # Clear users + clear_users() + + verifier = ( + Verifier('my-provider') + .add_transport(url='http://localhost:8080') + .state_handler(handler) + .add_source('./pacts/') + ) + ``` + + ```python title="v3 - Mapping" + state_handlers = { + 'user exists': lambda name, params: create_user(params.get('id', 123)), + 'no users exist': lambda name, params: clear_users(), + } + + verifier = ( + Verifier('my-provider') + .add_transport(url='http://localhost:8080') + .state_handler(state_handlers) + .add_source('./pacts/') + ) + ``` + + More information on the state handler function signature can be found in the [`state_handler` API documentation][pact.verifier.Verifier.state_handler]. By default, the handlers only _set up_ the provider state. If you need to also _tear down_ the state after verification, you can use the `teardown=True` argument to enable this behaviour. + + !!! warning + + These functions run in the test process, so any side effects must be properly shared with the provider. If using mocking libraries, ensure the provider is started in a separate thread of the same process (using `threading.Thread` or similar), rather than a separate process (e.g., using `multiprocessing.Process` or `subprocess.Popen`). + + + +#### Message Verification + +Message verification is now much more straightforward in v3, with a a similar interface to HTTP verification and fixes a number of issues and deficiencies present in the v2 implementation (including the swapped behaviour of `expects_to_receive` and `given`, and the lack of support for matchers and generators). + +```python title="v3 - Functional Handler" +def message_handler(description, metadata): + if description == 'user created event': + return { + 'id': 123, + 'name': 'Alice', + 'event': 'created' + } + elif description == 'user deleted event': + return {'id': 123, 'event': 'deleted'} + +verifier = ( + Verifier('my-provider') + .message_handler(message_handler) + .add_source('./pacts/') +) +``` + +```python title="v3 - Dictionary Mapping" +messages = { + 'user created event': {'id': 123, 'name': 'Alice', 'event': 'created'}, + 'user deleted event': lambda desc, meta: {'id': 123, 'event': 'deleted'} +} + +verifier = ( + Verifier('my-provider') + .message_handler(messages) + .add_source('./pacts/') +) +``` + +#### Running Verification + +Verification has been simplified and no longer requires checking return codes. Instead, the `verify()` method raises an exception on failure, or returns normally on success. + +```python title="v2" +success, logs = verifier.verify_pacts('./pacts/consumer-provider.json') +if not success: + print(logs) + raise AssertionError("Verification failed!") +``` + +```python title="v3" +verifier.verify() +``` diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index f86c2df59..99021f73d 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -4,6 +4,7 @@ - [Consumer](consumer.md) - [Provider](provider.md) - [Releases](releases.md) + - [Migration Guide](MIGRATION.md) - [Changelog](CHANGELOG.md) - [Contributing](CONTRIBUTING.md) - [Examples](examples/) diff --git a/docs/consumer.md b/docs/consumer.md index b145ded0f..c42967119 100644 --- a/docs/consumer.md +++ b/docs/consumer.md @@ -1,356 +1,358 @@ # Consumer Testing -Pact is a consumer-driven contract testing tool. This means that the consumer specifies the expected interactions with the provider, and these interactions are used to create a contract. This contract is then used to verify that the provider meets the consumer's expectations. +Pact is a consumer-driven contract testing tool. The consumer specifies the expected interactions with the provider, which are used to create a contract. This contract is then used to verify that the provider meets the consumer's expectations. + + +
+ +```mermaid +sequenceDiagram + box Consumer Side + participant Consumer + participant P1 as Pact + end + box Provider Side + participant P2 as Pact + participant Provider + end + Consumer->>P1: GET /users/123 + P1->>Consumer: 200 OK + Consumer->>P1: GET /users/999 + P1->>Consumer: 404 Not Found + + P1--)P2: Pact Broker + + P2->>Provider: GET /users/123 + Provider->>P2: 200 OK + P2->>Provider: GET /users/999 + Provider->>P2: 404 Not Found +``` + +
+ + +The consumer is the client that makes requests, and the provider is the server that responds. In most cases, the consumer is a front-end application and the provider is a back-end service; however, a back-end service may also require information from another service, making it a consumer of that service. -The consumer is the client that makes requests, and the provider is the server that responds to those requests. In most straightforward cases, the consumer is a front-end application and the provider is a back-end service; however, a back-end service may also require information from another service, making it a consumer of that service. +The core logic is implemented in Rust and exposed to Python through the [core Pact FFI](https://github.com/pact-foundation/pact-reference). This will help ensure feature parity between different language implementations and improve performance and reliability. This also brings compatibility with the latest Pact Specification (v4). + +> [!NOTE] +> +> For asynchronous interactions (e.g., message queues), the consumer refers to the service that processes the messages. This is not covered here, but further information is available in the [Message Pact](https://docs.pact.io/getting_started/how_pact_works#non-http-testing-message-pact) section of the Pact documentation. ## Writing the Test -For an illustrative example, consider a simple API client that fetches user data from a service. The client might look like this: +> [!NOTE] +> +> The code below is an abridged version of [this example](./examples/http/requests_and_fastapi/README.md). + +### Consumer Client + +For example, consider a simple API client that interacts with a user provider service. The client has methods to get, create, and delete users. The user data model is defined using a dataclass. ```python +from dataclasses import dataclass +from datetime import datetime +from typing import Any import requests +@dataclass() +class User: # (1) + id: int + name: str + created_on: datetime class UserClient: - def __init__( - self, - base_url: str = "https://example.com/api/v1", - ): - self.base_url = base_url - - def get_user(self, user_id) -> dict[str, str | int | list[str]]: - """ - Fetch a user's data. - - Args: - user_id: The user's ID. - - Returns: - The user's data as a dictionary. It should have the following keys: - - - id: The user's ID. - - username: The user's username. - - groups: A list of groups the user belongs to. - """ - return requests.get("/".join([self.base_url, "user", user_id])).json() + """Simple HTTP client for interacting with a user provider service.""" + + def __init__(self, hostname: str) -> None: # (2) + self._hostname = hostname + + def get_user(self, user_id: int) -> User: + """Get a user by ID from the provider.""" + response = requests.get(f"{self._hostname}/users/{user_id}") + response.raise_for_status() + data: dict[str, Any] = response.json() + return User( # (3) + id=data["id"], + name=data["name"], + created_on=datetime.fromisoformat(data["created_on"]), + ) ``` -The Pact test for this client would look like this: +1. The `User` dataclass represents the user data model _as used by the client_. Importantly, this is not necessarily the same as the data model used by the provider. The Pact contract should reflect what the consumer needs, not what the provider actually implements. -```python -import atexit -import unittest - -from user_client import UserClient -from pact import Consumer, Provider - -pact = Consumer("UserConsumer").has_pact_with(Provider("UserProvider")) -pact.start_service() -atexit.register(pact.stop_service) - - -class GetUserData(unittest.TestCase): - def test_get_user(self) -> None: - expected = { - "username": "UserA", - "id": 123, - "groups": ["Editors"], - } - - ( - pact.given("User 123 exists") - .upon_receiving("a request for user 123") - .with_request("get", "/user/123") - .will_respond_with(200, body=expected) - ) +2. The initialiser for the `UserClient` class takes a `hostname` parameter, which is the base URL of the user provider service. This ensures that the client can be easily pointed to the mock service during testing. - client = UserClient(pact.uri) - - with pact: - result = client.get_user(123) - self.assertEqual(result, expected) -``` +3. Only the fields required by the consumer are included in the `User` dataclass. The provider might return additional fields (e.g., `email`, `last_login`, etc.), but this consumer does not need to know about them and therefore they are ignored in the client implementation. -This test does the following: +### Consumer Test -- defines the Consumer and Provider objects that describe the product and the service under test, -- uses `given` to define the setup criteria for the Provider, and -- defines the expected request and response for the interaction. +The following is a Pact test for the `UserClient` class defined above. It sets up a mock provider, defines the expected interactions, and verifies that the client behaves as expected. -The mock service is started when the `pact` object is used as a context manager. The `UserClient` object is created with the URI of the mock service, and the `get_user` method is called. The mock service responds with the expected data, and the test asserts that the response matches the expected data. +```python +from pathlib import Path + +import pytest +from pact import Pact, match + +@pytest.fixture +def pact() -> Generator[Pact, None, None]: # (1) + """Set up a Pact mock provider for consumer tests.""" + pact = Pact("user-consumer", "user-provider").with_specification("V4") # (2) + yield pact + pact.write_file(Path(__file__).parent / "pacts") + +def test_get_user(pact: Pact) -> None: + """Test the GET request for a user.""" + response: dict[str, object] = { # (3) + "id": match.int(123), + "name": match.str("Alice"), + "created_on": match.datetime(), + } + ( + pact.upon_receiving("A user request") # (4) + .given("the user exists", id=123, name="Alice") # (5) + .with_request("GET", "/users/123") # (6) + .will_respond_with(200) # (7) + .with_body(response, content_type="application/json") # (8) + ) - + with pact.serve() as srv: # (9) + client = UserClient(str(srv.url)) # (10) + user = client.get_user(123) + assert user.name == "Alice" +``` -!!! info +1. A [Pytest fixture](https://docs.pytest.org/en/stable/explanation/fixtures.html) provides a reusable `pact` object for multiple tests. In this case, the fixture creates a [`Pact`][pact.Pact] instance representing the contract between the consumer and provider. The fixture yields the `pact` object to the test function, and after the test completes, writes the generated pact file to the specified directory. - A common mistake is to use a generic HTTP client to make requests to the mock service. This defeats the purpose of the test as it does not verify that the client is making the correct requests and handling the responses correctly. +2. The Pact specification version is set to `"V4"` to ensure compatibility with the latest features and improvements in the Pact ecosystem. Note that this is the default version, so this line is optional unless you want to specify a different version. - +3. The expected response is defined using the `match` module for flexible matching of the response data. Here, the `id` field is expected to be an integer, the `name` field a string, and the `created_on` field a datetime string. The specific values are not important, as long as they match the expected types. -An alternative to using the `pact` object as a context manager is to manually call the `setup` and `verify` methods: +4. The `upon_receiving` method defines the description of the interaction. This description also uniquely identifies the interaction within the Pact file. -```python -with pact: - result = client.get_user(123) - self.assertEqual(result, expected) +5. The `given` method sets up the provider state, indicating that the user with ID 123 exists. Pact allows parameters to be passed to the provider state, which can be used to set up the provider in a specific way. Here, the parameters `id=123` and `name="Alice"` are provided, which the provider can use to create the user if necessary. -# Is equivalent to +6. The `with_request` method defines the expected request that the consumer will make. Here, it specifies that a `GET` request will be made to the `/users/123` endpoint. -pact.setup() -result = client.get_user(123) -self.assertEqual(result, expected) -pact.verify() -``` +7. The `will_respond_with` method specifies the expected HTTP status code of the response. Here, a `200 OK` status is expected. The `will_respond_with` method also helps separate the request definition from the response definition, improving readability. -## Mock Service +8. The `with_body` method defines the expected body of the response, using the `response` dictionary defined earlier. The `content_type` parameter specifies that the response will be in JSON format. -Pact provides a mock service that simulates the provider service based on the defined interactions. The mock service is started when the `pact` object is used as a context manager, or when the `setup` method is called. +9. The `pact.serve()` method starts the mock service, and the `srv` object provides the URL of the mock service. Within this context, any requests made to the mock service will be handled according to the interactions defined on the `pact` object. Once the context is exited, the mock service is stopped, and the interactions are verified to ensure all expected requests were made. -The mock service is started by default on `localhost:1234`, but you can adjust this during Pact creation. This is particularly useful if the consumer interactions with multiple services. +10. The `UserClient` is instantiated with this URL, and the `get_user` method is called to retrieve the user data. The test asserts that the returned user's name is "Alice". -```python -pact = Consumer('Consumer').has_pact_with( - Provider('Provider'), - host_name='mockservice', - port=8080, -) -``` +The test begins with a Pytest fixture that creates a reusable Pact instance representing the contract between `"user-consumer"` and `"user-provider"`. The expected response is defined using flexible matchers (`match.int()`, `match.str()`, `match.datetime()`) to validate data types rather than exact values, making the test more robust against varying response data. -The mock service offers you several important features when building your contracts: +The interaction definition includes a description, provider state parameters, request details, and expected response format. Only the required parts of the interaction are specified, rather than an exhaustive specification. For example, the client will typically add additional headers (e.g., `User-Agent`, `Accept`, etc.) to the request, but these are not necessary for the contract and are therefore omitted. Similarly, the provider's response may include additional fields or headers that the consumer will ignore, so these are also not included in the contract. -- It provides a real HTTP server that your code can contact during the test and provides the responses you defined. -- You provide it with the expectations for the request your code will make and it will assert the contents of the actual requests made based on your expectations. -- If a request is made that does not match one you defined or if a request from your code is missing it will return an error with details. -- Finally, it will record your contracts as a JSON file that you can store in your repository or publish to a Pact broker. + The `pact.serve()` context manager starts a mock provider service that handles requests according to the defined interactions, creating a controlled testing environment. The actual client code is then executed against this mock service, ensuring it makes correct requests and handles responses properly. Once the context is exited, the Pact file is automatically written to the specified directory for later provider verification, completing the consumer-driven contract testing cycle. -## Requests +> [!WARNING] +> +> A common mistake is to use a generic HTTP client (e.g., `requests`, `httpx`, etc.) to make requests to the mock service within the test. This defeats the purpose of the test, as it does not verify that the client is making the correct requests and handling the responses correctly. -The expected request in the example above is defined with the `with_request` method. It is possible to customize the request further by specifying the method, path, body, headers, and query with the `method`, `path`, `body`, `headers` and `query` keyword arguments. +### Multi-Interaction Testing -- Adding query parameters: +The mock service can handle multiple interactions within a single test. This is useful when you want to test a sequence of requests and responses. For example, a first request might create a background task, a second request might check the status of that task, and a final request retrieves the result. This flow can be tested in a single test function by defining multiple interactions on the `pact` object: - ```python - pact.with_request( - path="/user/search", - query={"group": "editor"}, - ) - ``` +```python +( + pact.upon_receiving("A request to create a task") + .with_request("POST", "/tasks", body={"type": "long_running"}) + .will_respond_with(202) + .with_header("Location", "/tasks/1/status") +) -- Using different HTTP methods: +( + pact.upon_receiving("A request to check task status") + .with_request("GET", "/tasks/1/status") + .will_respond_with(200) + .with_body({"status": "completed"}) + .with_headers({ + "Task-ID": "1", + "Location": "/tasks/1/result", + }) +) - ```python - pact.with_request( - method="DELETE", - path="/user/123", - ) - ``` +( + pact.upon_receiving("A request to get task result") + .with_request("GET", "/tasks/1/result") + .will_respond_with(200) + .with_body({"result": "Task completed successfully"}) +) +``` -- Adding a request body and headers: +When the mock service is started with `pact.serve()`, it will handle requests for all defined interactions, ensuring the client code can be tested against a realistic sequence of operations. Furthermore, for the test to pass, all defined interactions must be exercised by the client code. If any interaction is not used, the test will fail. - ```python - pact.with_request( - method="POST", - path="/user/123", - body={"username": "UserA"}, - headers={"Content-Type": "application/json"}, - ) - ``` +## Mock Service -You can define exact values for your expected request like the examples above, or you can use the matchers defined later to assist in handling values that are variable. +Pact provides a mock service that simulates the provider service based on the defined interactions. The mock service is started when the `pact` object is used as a context manager with `pact.serve()`, as shown in the [consumer test](#consumer-test) example above. -It is important to note that the code you are testing _must_ complete all requests defined. Similarly, if a client makes a request that is not defined in the contract, the test will also fail. +The mock service automatically selects a random free port by default, helping to avoid port conflicts when running multiple tests. You can optionally specify a custom host and port during Pact creation if needed for your testing environment. -## Pattern Matching +```python +with pact.serve(host="localhost", port=1234) as srv: + client = UserClient(str(srv.url)) + user = client.get_user(123) +``` -Simple equality checks might be sufficient for simple requests, but more realistic tests will require more flexible matching. For example, the above scenario works great if the user information is always static, but will fail if the user has a datetime field that is regularly updated. +The mock service offers several important features when building your contracts: -In order to handle variable data and make tests more robust, there are a number of matchers available as described below. +- It provides a real HTTP server that your code can contact during the test and returns the responses you defined. +- You provide the expectations for the requests your code will make, and it asserts the contents of the actual requests made against your expectations. +- If a request is made that does not match one you defined, or if a request from your code is missing, it returns an error with details. -### Terms +## Broker -The `Term` matcher allows you to define a regular expression that the value should match, along with an example value. The pattern is used by Pact for determining the validity of the response, while the example value is returned by Pact in cases where a response needs to be generated. +The above example showed how to test a single consumer; however, without also testing the provider, the test is incomplete. The Pact Broker is a service that allows you to share and manage your contracts between your consumer and provider tests. It acts as a central repository for your contracts, allowing you to publish contracts from your consumer tests and retrieve them in your provider tests. -This is useful when you need to assert that a value has a particular format, but you are unconcerned about the exact value. +Once the tests are complete (and successful), the contracts can be uploaded to the Pact Broker. The provider can then download the contracts and run its own tests to ensure it meets the consumer's expectations. -```python -body = { - "id": 123, - "reference": Term(r"[A-Z]\d{3,6}-[0-9a-f]{6}", "X1234-456def"), - "last_modified": Term( - r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", - "2024-07-20T13:27:03Z", - ), -} +The Broker CLI is a command-line tool that can be installed through the `pact-python-cli` package, or directly from the [Pact Standalone](https://github.com/pact-foundation/pact-standalone) releases page. It bundles several standalone CLI tools, including the `pact-broker` CLI client. -( - pact.given("User 123 exists") - .upon_receiving("a request for user 123") - .with_request("get", "/user/123") - .will_respond_with(200, body=body, headers={ - "X-Request-ID": Term( - r"[a-z]{4}[0-9]{8}-[A-Z]{3}", - "abcd1234-EFG", - ), - }) -) - -client = UserClient(pact.uri) +The general syntax for the CLI is: -with pact: - result = client.get_user(123) - assert result["id"] == 123 - assert result["reference"] == "X1234-456def" - assert result["last_modified"] == "2024-07-20T13:27:03Z" +```console +pact-broker publish \ + /path/to/pacts/consumer-provider.json \ + --consumer-app-version 1.0.0 \ + --auto-detect-version-properties ``` -In this example, the `UserClient` must include a `X-Request-ID` header matching the pattern (irrespective of the actual value), and the mock service will respond with the example values. +It expects the following environment variables to be set: -### Like +`PACT_BROKER_BASE_URL` -The `Like` matcher asserts that the element's type matches the matcher. If the mock service needs to produce an answer, the example value provided will be returned. Some examples of the `Like` matcher are: +: The base URL of the Pact Broker (e.g., `https://test.pactflow.io` if using [PactFlow](https://pactflow.io), or the URL to your self-hosted Pact Broker instance). -```python -from pact import Like +`PACT_BROKER_USERNAME` / `PACT_BROKER_PASSWORD` -Like(123) # Requires any integer -Like("hello world") # Requires any string -Like(3.14) # Requires any float -Like(True) # Requires any boolean -``` +: The username and password for authenticating with the Pact Broker. -More complex object can be defined, in which case the `Like` matcher will be applied recursively: +`PACT_BROKER_TOKEN` -```python -from pact import Like, Term - -Like({ - 'id': 123, # Requires any integer - "reference": Term(r"[A-Z]\d{3,6}-[0-9a-f]{6}", "X1234-456def"), - 'confirmed': False, # Requires any boolean - 'address': { # Requires a dictionary - 'street': '200 Bourke St' # Requires any string - } -}) -``` +: An alternative to using username and password, this is a token that can be used for authentication (e.g., used with [PactFlow](https://pactflow.io)). -### EachLike +## Pattern Matching -The `EachLike` matcher asserts the value is an array type that consists of elements like the one passed in. It can be used to assert simple arrays, +Simple equality checks work for basic scenarios, but realistic tests need flexible matching to handle variable data such as timestamps, IDs, and dynamic content. The `match` module provides matchers that validate data structure and types rather than exact values. ```python -from pact import EachLike - -EachLike(1) # All items are integers -EachLike('hello') # All items are strings -``` +from pact import match -or other matchers can be nested inside to assert more complex objects +# Instead of exact matches that break easily: +response = { + "id": 12345, # Brittle - specific value + "email": "user@example.com", # Fails if email changes + "created_at": "2024-01-15T10:30:00Z" # Breaks on different timestamps +} -```python -from pact import EachLike, Term -EachLike({ - 'username': Term('[a-zA-Z]+', 'username'), - 'id': 123, - 'groups': EachLike('administrators') -}) +# Use flexible matchers: +response = { + "id": match.int(12345), # Any integer + "email": match.regex("user@example.com", regex=r".+@.+\..+"), + "created_at": match.datetime("2024-01-15T10:30:00Z") +} ``` -> Note, you do not need to specify everything that will be returned from the Provider in a JSON response, any extra data that is received will be ignored and the tests will still pass. +Common matcher types include: - +- **Type matchers**: `match.int()`, `match.str()`, `match.bool()` - validate data types +- **Pattern matchers**: `match.regex()`, `match.uuid()` - validate specific formats +- **Collection matchers**: `match.each_like()`, `match.array_containing()` - handle arrays and objects +- **Date/time matchers**: `match.date()`, `match.time()`, `match.datetime()` - flexible timestamp handling -> Note, to get the generated values from an object that can contain matchers like Term, Like, EachLike, etc. for assertion in self.assertEqual(result, expected) you may need to use get_generated_values() helper function: +Matchers ensure your contracts focus on data structure and semantics rather than brittle exact values, making tests more robust and maintainable. -```python -from pact.matchers import get_generated_values -self.assertEqual(result, get_generated_values(expected)) -``` +For comprehensive documentation and examples, see the [API Reference](api/match/README.md) and the [`match` module documentation][pact.match]. For more about Pact's matching specification, see [Matching](https://docs.pact.io/getting_started/matching). -### Common Formats +## Dynamic Data Generation -As you have seen above, regular expressions are a powerful tool for matching complex patterns; however, they can be cumbersome to write and maintain. A number of common formats have been predefined for ease of use: +While matchers validate that received data conforms to expected patterns, generators produce realistic test data for responses. The `generate` module provides functions to create dynamic values that change on each test run, making your Pact contracts more realistic and robust. -| matcher | description | -| ----------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `identifier` | Match an ID (e.g. 42) | -| `integer` | Match all numbers that are integers (both ints and longs) | -| `decimal` | Match all real numbers (floating point and decimal) | -| `hexadecimal` | Match all hexadecimal encoded strings | -| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) | -| `timestamp` | Match a string containing an RFC3339 formatted timestamp (e.g. Mon, 31 Oct 2016 15:21:41 -0400) | -| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) | -| `iso_datetime` | Match string containing ISO 8601 formatted dates (e.g. 2015-08-06T16:53:10+01:00) | -| `iso_datetime_ms` | Match string containing ISO 8601 formatted dates, enforcing millisecond precision (e.g. 2015-08-06T16:53:10.123+01:00) | -| `ip_address` | Match string containing IP4 formatted address | -| `ipv6_address` | Match string containing IP6 formatted address | -| `uuid` | Match strings containing UUIDs | +```python +from pact import generate -These can be used to replace other matchers +# Instead of static values in your mock responses +response = { + "user_id": 123, # Always the same + "session_token": "abc-def-123", # Predictable + "created_at": "2024-07-20T14:30:00+00:00" # Never changes +} -```python -from pact import Like, Format - -Like({ - 'id': Format().integer, - 'lastUpdated': Format().timestamp, - 'location': { - 'host': Format().ip_address - }, -}) +# Use generators for dynamic, realistic data +response = { + "user_id": generate.int(min=1, max=999999), + "session_token": generate.uuid(), + "created_at": generate.datetime("%Y-%m-%dT%H:%M:%S%z") +} ``` -For more information see [Matching](https://docs.pact.io/getting_started/matching) +Generators are particularly useful when: -## Broker +- **Testing with fresh data**: Each test run uses different values, helping catch issues with data handling +- **Avoiding test pollution**: Dynamic IDs and tokens prevent tests from accidentally depending on specific values +- **Simulating real conditions**: Generated timestamps, UUIDs, and random numbers better represent actual API behavior +- **Provider state integration**: Using `generate.provider_state()` to inject values from the provider's test setup -The above example showed how to test a single consumer; however, without also testing the provider, the test is incomplete. The Pact Broker is a service that allows you to share your contracts between your consumer and provider tests. +### Common Generators -The Pact Broker acts as a central repository for all your contracts and verification results, and provides a number of features to help you get the most out of your Pact workflow. +```python +from pact import generate -Once the tests are complete, the contracts can be uploaded to the Pact Broker. The provider can then download the contracts and run its own tests to ensure it meets the consumer's expectations. There are two ways to upload contracts as shown below. +response = { + # Numeric values with constraints + "user_id": generate.int(min=1, max=999999), + "price": generate.float(precision=2), # 2 total digits + "hex_color": generate.hex(digits=6), # 6-digit hex code -### Broker CLI (_recommended_) + # String and text data + "username": generate.str(size=8), # 8-character string + "confirmation": generate.regex(r"[A-Z]{3}-\d{4}"), # Pattern-based -The Broker CLI is a command-line tool that is bundled with the Pact Python package. It can be used to publish contracts to the Pact Broker. See [Publishing and retrieving pacts](https://docs.pact.io/pact_broker/publishing_and_retrieving_pacts) + # Identifiers + "session_id": generate.uuid(), # Standard UUID format + "simple_id": generate.uuid(format="simple"), # No hyphens -The general syntax for the CLI is: + # Dates and times + "created_at": generate.datetime("%Y-%m-%dT%H:%M:%S%z"), + "birth_date": generate.date("%Y-%m-%d"), + "start_time": generate.time("%H:%M:%S"), -```console -pact-broker publish \ - /path/to/pacts/consumer-provider.json \ - --consumer-app-version 1.0.0 \ - --branch main \ - --broker-base-url https://test.pactflow.io \ - --broker-username someUsername \ - --broker-password somePassword -``` + # Boolean values + "is_active": generate.bool(), -If the broker requires a token, you can use the `--broker-token` flag instead of `--broker-username` and `--broker-password`. + # Provider-specific values + "server_url": generate.mock_server_url(), + "dynamic_value": generate.provider_state("${expression}") +} +``` -### Python API +### Combining Matchers and Generators -If you wish to use a more programmatic approach within Python, it is possible to use the `Broker` class to publish contracts to the Pact Broker. Note that it is ultimately a wrapper around the CLI, and as a result, the CLI is recommended for most use cases. +Matchers and generators work together to create flexible, realistic contracts. Use matchers to validate incoming data and generators to produce dynamic response data: ```python -broker = Broker(broker_base_url="http://localhost") -broker.publish( - "TestConsumer", - "2.0.1", - branch="consumer-branch", - pact_dir=".", -) +# Request validation with matchers +request_body = { + "email": match.regex("user@example.com", regex=r".+@.+\..+"), + "age": match.int(25, min=18, max=100), + "preferences": match.array_containing([match.str("notifications")]) +} + +# Response generation with dynamic data +response_body = { + "id": generate.int(min=100000, max=999999), + "email": match.str("user@example.com"), # Echo back the input + "verification_token": generate.uuid(), # Fresh token each time + "created_at": generate.datetime("%Y-%m-%dT%H:%M:%S%z"), + "profile_url": generate.mock_server_url( + example="/profiles/12345", + regex=r"/profiles/\d+" + ) +} ``` -The parameters for this differ slightly in naming from their CLI equivalents: - -| CLI | native Python | -| ---------------------------------- | -------------------------------- | -| `--branch` | `branch` | -| `--build-url` | `build_url` | -| `--auto-detect-version-properties` | `auto_detect_version_properties` | -| `--tag=TAG` | `consumer_tags` | -| `--tag-with-git-branch` | `tag_with_git_branch` | -| `PACT_DIRS_OR_FILES` | `pact_dir` | -| `--consumer-app-version` | `version` | -| `n/a` | `consumer_name` | +This approach ensures your tests validate the correct data structures while generating realistic, varied response data that better simulates real-world API behaviour. diff --git a/docs/provider.md b/docs/provider.md index 6145f9a47..6344331dc 100644 --- a/docs/provider.md +++ b/docs/provider.md @@ -1,158 +1,298 @@ # Provider Testing -Pact is a consumer-driven contract testing tool. This means that the consumer specifies the expected interactions with the provider, and these interactions are used to create a contract. This contract is then used to verify that the provider behaves as expected. +Pact is a consumer-driven contract testing tool. The consumer specifies the expected interactions with the provider, which are used to create a contract. This contract is then used to verify that the provider behaves as expected. + + +
+ +```mermaid +sequenceDiagram + box Consumer Side + participant Consumer + participant P1 as Pact + end + box Provider Side + participant P2 as Pact + participant Provider + end + Consumer->>P1: GET /users/123 + P1->>Consumer: 200 OK + Consumer->>P1: GET /users/999 + P1->>Consumer: 404 Not Found + + P1--)P2: Pact Broker + + P2->>Provider: GET /users/123 + Provider->>P2: 200 OK + P2->>Provider: GET /users/999 + Provider->>P2: 404 Not Found +``` -The provider verification process works by replaying the interactions from the consumer against the provider and checking that the responses match what was expected. This is done by using the Pact files created by the consumer tests, either by reading them from a local filesystem, or by fetching them from a Pact Broker. +
+ -## Verifying Pacts +The provider verification process works by replaying the interactions from the consumer against the provider and checking that the responses match what was expected. This is done using the Pact files created by the consumer tests, either by reading them from the local file system or by fetching them from a Pact Broker. -### Command Line Interface +The core verification logic is implemented in Rust and exposed to Python through the [core Pact FFI](https://github.com/pact-foundation/pact-reference). This will help ensure feature parity between different language implementations and improve performance and reliability. This also brings compatibility with the latest Pact Specification (v4). -Pact Python comes bundled[^1] with the `pact-verifier` CLI tool to verify your provider. It is located at within the `{site-packages}/pact/bin` directory, and the following command will add it to your path: +## Verifying Pacts -[^1]: The CLI is available for most architecture, but if you are on a platform where the CLI is not bundled, you can install the [Pact Ruby Standalone](https://github.com/pact-foundation/pact-ruby-standalone) release. +Pact Python's [`Verifier`][pact.verifier.Verifier] class provides the mechanism to fetch and verify Pacts against your provider application, while also facilitating provider state management and result publishing. - +### Basic Usage -=== "Linux / macOS (`sh`)" +You can verify Pacts from a local directory as follows: - ```bash - site_packages=$(python -c 'import sysconfig; print(sysconfig.get_path("purelib"))') - if [ -d "$sit_p_packages/pact/bi ]; then]; then - export PATH_p$site_packages/pact/bin:$P - else - echo "Pact CLI not found." - fi - ``` +```python +from pact import Verifier -=== "Windows (`pwsh`)" +def test_provider(): + """Test the provider against the consumer contract.""" + verifier = ( + Verifier("my-provider") # Provider name + .add_source("./pacts/") # Directory containing Pact files + .add_transport(url="http://localhost:8080") # Provider URL + ) - ```pwsh - $sitePackages = (python -c 'import sysconfig; print(sysconfig.get_path("purelib"))') - if (Test-Path "$sitePackages/pact/bin") { - $env:PATH += ";$sitePackages/pact/bin" - } else { - Write-Host "Pact CLI not found." - } - ``` + verifier.verify() +``` - +The `Verifier` inspects the specified directory for Pact files matching the provider name, and verifies each interaction against the running provider at the given URL. -You can verify that the CLI is available by running: +### Verifying from a Pact Broker -```console -pact-verifier --help +Although local Pact files are useful for quick tests, in most cases you will want to verify Pacts from a Pact Broker. In this case, specify the broker URL and any necessary authentication: + +```python +from pact import Verifier + +def test_provider_from_broker(): + """Test the provider against contracts from a Pact Broker.""" + verifier = ( + Verifier("my-provider") + .add_transport(url="http://localhost:8080") + .broker_source( + "https://my-broker.example.com", + username="broker-username", # or use token="bearer-token" + password="broker-password", + ) + ) + + verifier.verify() ``` -A minimal invocation of the Pact verifier looks like this: +For advanced broker configurations, use the selector builder pattern to filter which Pacts to verify: -```console -pact-verifier ./pacts/ \ - --provider-base-url=http://localhost:8080 +```python +from pact import Verifier + +def test_provider_with_selectors(): + """Test with advanced broker selectors.""" + verifier = ( + Verifier("my-provider") + .add_transport(url="http://localhost:8080") + .broker_source( + "https://my-broker.example.com", + token="bearer-token", + selector=True, # Enable selector builder + ) + .include_pending() # Include pending pacts + .include_wip_since("2023-01-01") # Include WIP pacts since date + .provider_tags("main", "develop") + .consumer_tags("production", "main") + .build() # Build the selector + ) + + verifier.verify() ``` -This will verify all the Pacts in the `./pacts/` directory against the provider located at `http://localhost:8080`. +More information on the selector options is available in the [API reference][pact.verifier.BrokerSelectorBuilder]. -#### Options +### Publishing Results -| Option | Description | -| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--provider-base-url TEXT` | Base URL of the provider to verify against. [required] | -| `--provider-states-setup-url TEXT` | URL to send POST requests to setup a given provider state. | -| `--pact-broker-username TEXT` | Username for Pact Broker basic authentication. Can also be specified via the environment variable PACT_BROKER_USERNAME. | -| `--pact-broker-url TEXT` | Base URl for the Pact Broker instance to publish pacts to. Can also be specified via the environment variable PACT_BROKER_BASE_URL. | -| `--consumer-version-tag TEXT` | Retrieve the latest pacts with this consumer version tag. Used in conjunction with --provider. May be specified multiple times. | -| `--consumer-version-selector TEXT` | Retrieve the latest pacts with this consumer version selector. Used in conjunction with --provider. May be specified multiple times. | -| `--provider-version-tag TEXT` | Tag to apply to the provider application version. May be specified multiple times. | -| `--provider-version-branch TEXT` | The name of the branch the provider version belongs to. | -| `--pact-broker-password TEXT` | Password for Pact Broker basic authentication. Can also be specified via the environment variable PACT_BROKER_PASSWORD. | -| `--pact-broker-token TEXT` | Bearer token for Pact Broker authentication. Can also be specified via the environment variable PACT_BROKER_TOKEN. | -| `--provider TEXT` | Retrieve the latest pacts for this provider. | -| `--custom-provider-header TEXT` | Header to add to provider state set up and pact verification requests. eg 'Authorization: Basic cGFjdDpwYWN0'. May be specified multiple times. | -| `-t`, `--timeout INTEGER` | The duration in seconds we should wait to confirm that the verification process was successful. Defaults to 30. | -| `-a`, `--provider-app-version TEXT` | The provider application version. Required for publishing verification results. | -| `-r`, -`-publish-verification-results` | Publish verification results to the broker. | -| `--verbose` / `--no-verbose` | Toggle verbose logging, defaults to False. | -| `--log-dir TEXT` | The directory for the pact.log file. | -| `--log-level TEXT` | The logging level. | -| `--enable-pending` / `--no-enable-pending` | Allow pacts which are in pending state to be verified without causing the overall task to fail. For more information, see [`pact.io/pending`](https://pact.io/pending) | -| `--include-wip-pacts-since TEXT` | Automatically include the pending pacts in the verification step. For more information, see [WIP pacts](https://docs.pact.io/pact_broker/advanced_topics/wip_pacts/) | -| `--help` | Show this message and exit. | +To publish verification results to the Broker: - +```python +verifier = ( + Verifier("my-provider") + .add_transport(url="http://localhost:8080") + .broker_source("https://my-broker.example.com", token="bearer-token") +) -??? note "Deprecated Options" +if "CI" in os.environ: + verifier.set_publish_options( # (1) + version="1.2.3", + branch="main", + tags=["production"], + ) - | Option | Description | - | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | `--pact-url TEXT` | specify pacts as arguments instead. The URI of the pact to verify. Can be an HTTP URI, a local file or directory path. It can be specified multiple times to verify several pacts. | - | `--pact-urls TEXT` | specify pacts as arguments instead. The URI(s) of the pact to verify. Can be an HTTP URI(s) or local file path(s). Provide multiple URI separated by a comma. | - | `--provider-states-url TEXT` | URL to fetch the provider states for the given provider API. | +verifier.verify() +``` - +1. While we use static values here, in practice, you would use dynamic values taken from your CI/CD environment, or helper function to extract version information from your source control system. -### Python API +## Configuration Options -Pact Python also provides a pythonic wrapper around the command line interface, allowing you to use the Pact verifier directly from your Python code. This can be useful if you want to integrate the verifier into your test suite or CI/CD pipeline. +### Filtering -To use the Python API, you need to import the `Verifier` class from the `pact` module: +Filter interactions to verify: ```python -verifier = Verifier( - provider='UserService', - provider_base_url="http://localhost:8080", +verifier = ( + Verifier("my-provider") + .filter(description="user.*", state="user exists") # Regex filters + .filter_consumers("mobile-app", "web-app") # Specific consumers only ) ``` -If you are verifying Pacts from the local filesystem, you can use the `verify_pacts` method: - -```python -success, logs = verifier.verify_pacts('./userserviceclient-userservice.json') -assert success == 0 -``` +### Custom Headers -On the other hand, if you are using a Pact Broker, you can use the `verify_with_broker` method: +While the Pact contract should define all necessary request and response details, there are cases where you may need to add custom headers to every request made to the provider during verification (e.g., for authentication). ```python -success, logs = verifier.verify_with_broker( - broker_url=PACT_BROKER_URL, - # Auth options +verifier = ( + Verifier("my-provider") + .add_custom_header("Authorization", "Bearer token123") + .add_custom_headers({ + "X-Debug-Mode": "true", + "X-Debug-Secret": "123-abc", + }) ) -assert success == 0 ``` -Where the auth options can either be `broker_username` and `broker_password` for OSS Pact Broker, or `broker_token` for PactFlow. +## Provider States + +Provider states are a crucial concept in Pact testing. When a consumer creates a Pact, it specifies not just what request to make, but also what state the provider should be in when that request is made. This is expressed using the `.given(...)` method in consumer tests. -The CLI options are available as keyword arguments to the various methods of the `Verifier` class: +For example, if a consumer test includes `given("user 123 exists")`, it means the provider must have a user with ID 123 in its system when the interaction is verified. A better approach is to parameterise the provider state instead of hard-coding values within the state name, such as `given("user exists", id=123, name="Alice")`. -| CLI | native Python | -| -------------------------------- | ------------------------------ | -| `--log-dir` | `log_dir` | -| `--log-level` | `log_level` | -| `--provider-app-version` | `provider_app_version` | -| `--headers` | `custom_provider_headers` | -| `--consumer-version-tag` | `consumer_tags` | -| `--provider-version-tag` | `provider_tags` | -| `--provider-states-setup-url` | `provider_states_setup_url` | -| `--verbose` | `verbose` | -| `--consumer-version-selector` | `consumer_selectors` | -| `--publish-verification-results` | `publish_verification_results` | -| `--provider-version-branch` | `provider_version_branch` | +For these provider states to be meaningful, the provider tests need to set up the appropriate state before each interaction is verified. This is done using state handler methods. Optionally, these handlers can also perform teardown actions after the interaction is verified, which is useful for cleaning up test data. -You can see more details in the examples +### State Handler Methods -- [Message Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/main/examples/tests/test_03_message_provider.py) -- [Flask Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/main/examples/tests/test_01_provider_flask.py) -- [FastAPI Provider Verifier Test](https://github.com/pact-foundation/pact-python/blob/main/examples/tests/test_01_provider_fastapi.py) +The `Verifier` class provides three ways to handle provider states: -## Provider States +1. **Function-based handlers** - A single function handles all states +2. **Dictionary-based handlers** - Map state names to specific functions +3. **URL-based handlers** - External HTTP endpoint manages states -In general, the consumer will make a request to the provider under the assumption that the provider has certain data, or is in a certain state. This is expressed in the consumer side through the `.given(...)` method. For example, `given("user 123 exists")` assumes that the provider knows about a user with the ID 123. +> [!WARNING] +> +> If using mocking libraries, the function- and dictionary-based handlers must run in the same process as the provider application. For example, using `threading.Thread` to run the provider in a separate thread of the same process is acceptable, but using `multiprocessing.Process` to run the provider in a separate process will not work. -To support this, the provider needs to be able to set up the state of the provider to match the expected state of the consumer. This is done through the `--provider-states-setup-url` option, which is a URL that the verifier will call to set up the provider state. +### Function-Based State Handler -Managing the provider state is an important part of the provider testing process, and the best way to manage it will depend on your application. A couple of options include: +A single function can handle all provider states: -1. Having an endpoint is part of the provider application, but not active in production. A call to this endpoint will set up the provider state, typically by [mocking][unittest.mock] the data store or external services. This method is used in the examples above. +```python +from pact import Verifier +from typing import Literal, Any + +def handle_provider_state( + state: str, + action: Literal["setup", "teardown"], + parameters: dict[str, Any] | None, +) -> None: + """Handle all provider state changes.""" + parameters = parameters or {} + if state == "user exists": + if action == "setup": + return create_user( + parameters.get("id", 123), + name=parameters.get("name", "Alice"), + ) + if action == "teardown": + return delete_user(parameters.get("id", 123)) + + if state == "no users exist": + if action == "setup": + return clear_all_users() + + msg = f"Unknown state/action: {state}/{action}" + raise ValueError(msg) + +verifier = ( + Verifier("my-provider") + .add_transport(url="http://localhost:8080") + .add_source("./pacts/") + .state_handler(handle_provider_state, teardown=True) +) + +verifier.verify() +``` + +### Dictionary-Based State Handler (Recommended) + +Map specific state names to dedicated handler functions: + +```python +from pact import Verifier +from typing import Literal, Any + +def mock_user_exists( + action: Literal["setup", "teardown"], + parameters: dict[str, Any] | None, +) -> None: + """Mock the provider state where a user exists.""" + parameters = parameters or {} + user_id = parameters.get("id", 123) + + if action == "setup": + # Set up the user in your test database/mock + return UserDb.create(User( + id=user_id, + name=parameters.get("name", "Test User"), + email=parameters.get("email", "test@example.com"), + )) + if action == "teardown": + # Clean up after the test + return UserDb.delete(user_id) + +def mock_user_does_not_exist( + action: Literal["setup", "teardown"], + parameters: dict[str, Any] | None, +) -> None: + """Mock the provider state where a user does not exist.""" + parameters = parameters or {} + user_id = parameters.get("id", 123) + + if action == "setup" and user_id: + # Ensure the user doesn't exist + if UserDb.get(user_id): + UserDb.delete(user_id) + +# Map state names to handler functions +state_handlers = { + "user exists": mock_user_exists, + "user 123 exists": mock_user_exists, + "user does not exist": mock_user_does_not_exist, +} + +verifier = ( + Verifier("my-provider") + .add_transport(url="http://localhost:8080") + .add_source("./pacts/") + .state_handler(state_handlers, teardown=True) +) + +verifier.verify() +``` + +### URL-Based State Handler + +This approach relies on the provider exposing an HTTP endpoint to manage provider states. This can be necessary if the handler logic cannot be implemented in Python (for example, if the provider is written in a different language). + +```python +verifier = ( + Verifier("my-provider") + .add_transport(url="http://localhost:8080") + .add_source("./pacts/") + .state_handler( + "http://localhost:8080/_pact/setup", # Your state setup endpoint + teardown=True, + body=True, # Send state info in request body + ) +) +``` -2. A separate application that has access to the same data store as the provider. This application can be started and stopped with different data store states. +The state setup endpoint should handle POST requests with the state information if `body=True` is set; otherwise, the state information will be passed through query parameters and headers. diff --git a/docs/releases.md b/docs/releases.md index aac85519d..ba8d34996 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -13,6 +13,7 @@ There are a couple of exceptions to the [semantic versioning](https://semver.org - Dropping support for a Python version is not considered a breaking change and is not necessarily accompanied by a major version bump. - Private APIs are not considered part of the public API and are not subject to the same rules as the public API. They can be changed at any time without a major version bump. Private APIs are denoted by a leading underscore in the name. Please be aware that the distinction between public and private APIs will be made concrete from version 3 onwards, and best judgement is used in the meantime to determine what is public and what is private. - Deprecations are not considered breaking changes and are not necessarily accompanied by a major version bump. Their removal is considered a breaking change and is accompanied by a major version bump. +- Changes to the type annotations will not be considered breaking changes, unless they are accompanied by a change to the runtime behaviour. Any deviation from the the standard semantic versioning rules will be clearly documented in the release notes. @@ -34,7 +35,7 @@ In order to reduce the build time, the pipeline builds different sets of wheels | Trigger | Platforms | Wheels | | ------------ | ----------------- | --------- | | Tag | `x86_64`, `arm64` | all | -| `main` | `x86_64` | all | +| `main` | `x86_64` | all | | Pull Request | `x86_64` | `cp312-*` | ### Publish Step diff --git a/mkdocs.yml b/mkdocs.yml index b5a825d3a..724b8f617 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,24 +11,16 @@ hooks: - docs/scripts/rewrite-docs-links.py plugins: - - search - - literate-nav: - nav_file: SUMMARY.md - - section-index - - gh-admonitions - gen-files: scripts: - docs/scripts/markdown.py - docs/scripts/python.py # - docs/scripts/other.py - - llmstxt: - full_output: llms-full.txt - sections: - Usage documentation: - - api/*.md - - api/**/*.md - Examples: - - examples/*.md + - search + - literate-nav: + nav_file: SUMMARY.md + - section-index + - gh-admonitions - mkdocstrings: default_handler: python enable_inventory: true @@ -72,6 +64,14 @@ plugins: annotations_path: brief show_signature: true show_signature_annotations: true + - llmstxt: + full_output: llms-full.txt + sections: + Usage documentation: + - api/*.md + - api/**/*.md + Examples: + - examples/*.md - social - blog: blog_toc: true diff --git a/src/pact/verifier.py b/src/pact/verifier.py index 7fa57b705..aab82ac1e 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -1161,6 +1161,15 @@ def broker_source( """ Adds a broker source to the verifier. + By default, or if `selector=False`, this function returns the verifier + instance to allow for method chaining. If `selector=True` is given, this + function returns a + [`BrokerSelectorBuilder`][pact.verifier.BrokerSelectorBuilder] instance + which allows for further configuration of the broker source in a fluent + interface. The [`build()`][pact.verifier.BrokerSelectorBuilder.build] + call is then used to finalise the broker source and return the verifier + instance for further configuration. + Args: url: The broker URL. The URL may contain a username and password for @@ -1180,7 +1189,11 @@ def broker_source( be specified through arguments, or embedded in the URL). selector: - Whether to return a BrokerSelectorBuilder instance. + Whether to return a + [BrokerSelectorBuilder][pact.verifier.BrokerSelectorBuilder] + instance. The builder instance allows for further configuration + of the broker source and must be finalised with a call to + [`build()`][pact.verifier.BrokerSelectorBuilder.build]. Raises: ValueError: From da945f07dc80dc19ded008d6b2b28b12abd0566a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 3 Oct 2025 12:06:55 +1000 Subject: [PATCH 1043/1376] feat: populate broker source from env Allows the broker source information to be populated from environment variables. This still requires the `broker_source` method to be called: ```python ( Verifier("my-provider") # ... .broker_source() ) ``` This can be disabled by setting the new `use_env` argument to `False` (defaults to True), or by setting the relevant argument to `None` if you want to disable an input. Signed-off-by: JP-Ellis --- src/pact/verifier.py | 69 ++++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/src/pact/verifier.py b/src/pact/verifier.py index aab82ac1e..4be20dcb7 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -75,6 +75,7 @@ import json import logging +import os from collections.abc import Mapping from contextlib import nullcontext from datetime import date @@ -87,7 +88,7 @@ import pact_ffi from pact._server import MessageProducer, StateCallback from pact._util import apply_args -from pact.types import Message, MessageProducerArgs, StateHandlerArgs +from pact.types import UNSET, Message, MessageProducerArgs, StateHandlerArgs, Unset if TYPE_CHECKING: from collections.abc import Iterable @@ -1114,53 +1115,67 @@ def _add_source_remote( @overload def broker_source( self, - url: str | URL, + url: str | URL | Unset = UNSET, *, - username: str | None = None, - password: str | None = None, + username: str | Unset = UNSET, + password: str | Unset = UNSET, selector: Literal[False] = False, + use_env: bool = True, ) -> Self: ... @overload def broker_source( self, - url: str | URL, + url: str | URL | None | Unset = UNSET, *, - token: str | None = None, + token: str | None | Unset = UNSET, selector: Literal[False] = False, + use_env: bool = True, ) -> Self: ... @overload def broker_source( self, - url: str | URL, + url: str | URL | None | Unset = UNSET, *, - username: str | None = None, - password: str | None = None, + username: str | None | Unset = UNSET, + password: str | None | Unset = UNSET, selector: Literal[True], + use_env: bool = True, ) -> BrokerSelectorBuilder: ... @overload def broker_source( self, - url: str | URL, + url: str | URL | None | Unset = UNSET, *, - token: str | None = None, + token: str | None | Unset = UNSET, selector: Literal[True], + use_env: bool = True, ) -> BrokerSelectorBuilder: ... - def broker_source( + def broker_source( # noqa: PLR0913 self, - url: str | URL, + url: str | URL | None | Unset = UNSET, *, - username: str | None = None, - password: str | None = None, - token: str | None = None, + username: str | None | Unset = UNSET, + password: str | None | Unset = UNSET, + token: str | None | Unset = UNSET, selector: bool = False, + use_env: bool = True, ) -> BrokerSelectorBuilder | Self: """ Adds a broker source to the verifier. + If any of the values are `None`, the value will be read from the + environment variables unless the `use_env` parameter is set to `False`. + The known variables are: + + - `PACT_BROKER_BASE_URL` for the `url` parameter. + - `PACT_BROKER_USERNAME` for the `username` parameter. + - `PACT_BROKER_PASSWORD` for the `password` parameter. + - `PACT_BROKER_TOKEN` for the `token` parameter. + By default, or if `selector=False`, this function returns the verifier instance to allow for method chaining. If `selector=True` is given, this function returns a @@ -1195,22 +1210,40 @@ def broker_source( of the broker source and must be finalised with a call to [`build()`][pact.verifier.BrokerSelectorBuilder.build]. + use_env: + Whether to read missing values from the environment variables. + This is `True` by default which allows for easy configuration + from the standard Pact environment variables. In all cases, the + explicitly provided values take precedence over the environment + variables. + Raises: ValueError: If mutually exclusive authentication parameters are provided. """ + + def maybe_var(v: Any | Unset, env: str) -> str | None: # noqa: ANN401 + if isinstance(v, Unset): + return os.getenv(env) if use_env else None + return v + + url = maybe_var(url, "PACT_BROKER_BASE_URL") + if not url: + msg = "A broker URL must be provided" + raise ValueError(msg) url = URL(url) + username = maybe_var(username, "PACT_BROKER_USERNAME") if url.user and username: msg = "Cannot specify both `username` and a username in the URL" raise ValueError(msg) - username = url.user or username + password = maybe_var(password, "PACT_BROKER_PASSWORD") if url.password and password: msg = "Cannot specify both `password` and a password in the URL" raise ValueError(msg) - password = url.password or password + token = maybe_var(token, "PACT_BROKER_TOKEN") if token and (username or password): msg = "Cannot specify both `token` and `username`/`password`" raise ValueError(msg) From 1812e079a9fa2239236c94b91c4f15937fa0dad6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 3 Oct 2025 12:26:44 +1000 Subject: [PATCH 1044/1376] chore: clarify explanation of given Signed-off-by: JP-Ellis --- MIGRATION.md | 4 +++- src/pact/interaction/_base.py | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 08c39cecd..a79cfb628 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -103,7 +103,9 @@ The v3 interface favours method chaining and provides more granular control over .with_body({'id': 123, 'name': 'Alice'}, content_type='application/json')) ``` -1. The new provider states can be streamlined by parameterizing them directly in the `given()` method. So instead of defining multiple variations of a `"user exists"` state, you can define it once and pass different parameters as needed. These can be passed as keyword arguments to `given()`, or as a dictionary in the second positional argument. +1. In v2, there was limited support for parameterizing provider states, and each state variation often required a separate definition. For example, `given("user Alice exists with id 123")` and `given("user Bob exists with id 456")` would be two distinct states, which would then need to be handled separately in the provider state setup. + + The new interface can now define a common descriptor that can be reused with different parameters: `.given("user exists", id=123, name='Alice')` and `.given("user exists", id=456, name='Bob')`. This approach reduces redundancy and makes it easier to manage provider states. Some methods are shared across request and response definitions, such as `with_header()` and `with_body()`. Pact Python automatically applies them to the correct part of the interaction based on whether they are called before or after `will_respond_with()`. Alternatively, these methods accept an optional `part` argument to explicitly specify whether they apply to the request or response. diff --git a/src/pact/interaction/_base.py b/src/pact/interaction/_base.py index dfaa7a483..410fdfd35 100644 --- a/src/pact/interaction/_base.py +++ b/src/pact/interaction/_base.py @@ -151,8 +151,16 @@ def given( ``` This function can be called repeatedly to specify multiple provider - states for the same Interaction. If the same state is specified with - different parameters, then the parameters are merged together. + states for the same Interaction. This allows for the same provider state + to be reused with different parameters: + + ```python + ( + pact.upon_receiving("a request") + .given("a user exists", id=123, name="Alice") + .given("a user exists", id=456, name="Bob") + ) + ``` Args: state: @@ -170,11 +178,17 @@ def given( ) ``` + These parameters are merged with any additional keyword + arguments passed to the function. + kwargs: The additional parameters for the provider state, specified as additional arguments to the function. The values must be serializable using Python's [`json.dumps`][json.dumps] function. + + These parameters are merged with any parameters passed in the + `parameters` positional argument. """ if not parameters and not kwargs: pact_ffi.given(self._handle, state) From 0e3942fba87c2fed396839b1aee29f89e4cbfaa8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 00:54:52 +0000 Subject: [PATCH 1045/1376] chore(deps): update taiki-e/install-action action to v2.62.20 (#1268) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index dcd7fbbf9..8be36ea14 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@d0f4f69b07c0804d1003ca9a5a5f853423872ed9 # v2.62.13 + uses: taiki-e/install-action@f355b1dcaf1a1c56ccead97cc540a259faf4bd5a # v2.62.20 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 0eb328546..9da331d75 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@d0f4f69b07c0804d1003ca9a5a5f853423872ed9 # v2.62.13 + uses: taiki-e/install-action@f355b1dcaf1a1c56ccead97cc540a259faf4bd5a # v2.62.20 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bcebd3ed9..f74b665e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@d0f4f69b07c0804d1003ca9a5a5f853423872ed9 # v2.62.13 + uses: taiki-e/install-action@f355b1dcaf1a1c56ccead97cc540a259faf4bd5a # v2.62.20 with: tool: git-cliff,typos From c1318954de6901a86ea095b6598ace0ae26d5bef Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 3 Oct 2025 13:46:30 +1000 Subject: [PATCH 1046/1376] chore!: drop python 3.9 add 3.14 Add support for Python 3.14, and drop support for Python 3.9 as it has reached its end-of-life. BREAKING CHANGE: Python 3.9 is no longer supported. Signed-off-by: JP-Ellis --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/test.yml | 18 ++--------------- conftest.py | 2 ++ docs/scripts/other.py | 4 +++- examples/http/aiohttp_and_flask/README.md | 2 +- .../http/aiohttp_and_flask/pyproject.toml | 2 +- .../http/aiohttp_and_flask/test_provider.py | 3 +-- examples/http/requests_and_fastapi/README.md | 2 +- .../http/requests_and_fastapi/provider.py | 6 +++--- .../http/requests_and_fastapi/pyproject.toml | 2 +- .../requests_and_fastapi/test_provider.py | 3 +-- examples/plugins/proto/person_pb2.py | 2 ++ examples/plugins/proto/person_pb2_grpc.py | 2 ++ examples/plugins/protobuf/__init__.py | 2 ++ pact-python-cli/pyproject.toml | 6 +++--- pact-python-ffi/pyproject.toml | 6 +++--- pact-python-ffi/tests/test_init.py | 2 ++ pyproject.toml | 15 +++++++------- src/pact/__init__.py | 2 ++ src/pact/__version__.pyi | 2 +- src/pact/_server.py | 4 ++-- src/pact/_util.py | 8 ++++++-- src/pact/interaction/__init__.py | 2 ++ src/pact/interaction/_base.py | 11 ++++------ src/pact/interaction/_http_interaction.py | 5 +---- .../interaction/_sync_message_interaction.py | 5 ++++- src/pact/pact.py | 10 +++------- src/pact/types.py | 5 ++--- src/pact/types.pyi | 3 +-- src/pact/verifier.py | 20 +++++++++++-------- tests/compatibility_suite/conftest.py | 8 ++++++-- .../compatibility_suite/test_v3_generators.py | 7 ++++++- .../test_v3_http_matching.py | 12 ++++++++--- tests/compatibility_suite/util/__init__.py | 2 +- tests/compatibility_suite/util/consumer.py | 3 +-- .../util/interaction_definition.py | 3 ++- tests/compatibility_suite/util/provider.py | 3 ++- tests/conftest.py | 2 ++ tests/test_error.py | 2 ++ tests/test_match.py | 8 ++++++-- tests/test_server.py | 2 ++ tests/test_verifier.py | 2 ++ 43 files changed, 121 insertions(+), 93 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 8be36ea14..475d979c0 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -19,7 +19,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: - STABLE_PYTHON_VERSION: '313' + STABLE_PYTHON_VERSION: '310' HATCH_VERBOSE: '1' FORCE_COLOR: '1' CIBW_BUILD_FRONTEND: build diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 9da331d75..ac81f07fd 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -19,7 +19,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: - STABLE_PYTHON_VERSION: '39' + STABLE_PYTHON_VERSION: '310' HATCH_VERBOSE: '1' FORCE_COLOR: '1' CIBW_BUILD_FRONTEND: build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a7b80b3db..ddcb1fdd7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,18 +62,11 @@ jobs: - ubuntu-latest - windows-latest python-version: - - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' - # Python 3.9 aren't supported on macos-latest (ARM) - exclude: - - os: macos-latest - python-version: '3.9' - include: - - os: macos-13 - python-version: '3.9' + - '3.14' steps: - name: Checkout code @@ -144,18 +137,11 @@ jobs: - ubuntu-latest - windows-latest python-version: - - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' - # Python 3.9 aren't supported on macos-latest (ARM) - exclude: - - os: macos-latest - python-version: '3.9' - include: - - os: macos-13 - python-version: '3.9' + - '3.14' steps: - name: Checkout code diff --git a/conftest.py b/conftest.py index 359b5e916..371bd108d 100644 --- a/conftest.py +++ b/conftest.py @@ -6,6 +6,8 @@ only be defined in this file. """ +from __future__ import annotations + import pytest diff --git a/docs/scripts/other.py b/docs/scripts/other.py index 7a309b231..eece99d5d 100644 --- a/docs/scripts/other.py +++ b/docs/scripts/other.py @@ -14,6 +14,8 @@ continue silently. """ +from __future__ import annotations + import subprocess from pathlib import Path from typing import TYPE_CHECKING @@ -77,7 +79,7 @@ def is_binary(buffer: bytes) -> bool: if str(dest_path) in EDITOR.files: continue - fi: "io.IOBase" + fi: io.IOBase with Path(source_path).open("rb") as fi: buf = fi.read(2048) diff --git a/examples/http/aiohttp_and_flask/README.md b/examples/http/aiohttp_and_flask/README.md index 0fb792f79..1a6671a16 100644 --- a/examples/http/aiohttp_and_flask/README.md +++ b/examples/http/aiohttp_and_flask/README.md @@ -36,7 +36,7 @@ Use the above links to view additional documentation within. ## Prerequisites -- Python 3.9 or higher +- Python 3.10 or higher - A dependency manager ([uv](https://docs.astral.sh/uv/) recommended, [pip](https://pip.pypa.io/en/stable/) also works) ## Running the Example diff --git a/examples/http/aiohttp_and_flask/pyproject.toml b/examples/http/aiohttp_and_flask/pyproject.toml index 6205fe030..b68d34d7c 100644 --- a/examples/http/aiohttp_and_flask/pyproject.toml +++ b/examples/http/aiohttp_and_flask/pyproject.toml @@ -5,7 +5,7 @@ name = "example-aiohttp-and-flask" description = "Example of using an aiohttp client and Flask server with Pact Python" dependencies = ["aiohttp~=3.0", "flask~=3.0", "typing-extensions~=4.0"] -requires-python = ">=3.9" +requires-python = ">=3.10" version = "1.0.0" [dependency-groups] diff --git a/examples/http/aiohttp_and_flask/test_provider.py b/examples/http/aiohttp_and_flask/test_provider.py index 22ded9c63..8c99e6f39 100644 --- a/examples/http/aiohttp_and_flask/test_provider.py +++ b/examples/http/aiohttp_and_flask/test_provider.py @@ -31,8 +31,7 @@ if TYPE_CHECKING: from pathlib import Path - - from typing_extensions import TypeAlias + from typing import TypeAlias ACTION_TYPE: TypeAlias = Literal["setup", "teardown"] diff --git a/examples/http/requests_and_fastapi/README.md b/examples/http/requests_and_fastapi/README.md index 547ae14f3..1b8a4e08a 100644 --- a/examples/http/requests_and_fastapi/README.md +++ b/examples/http/requests_and_fastapi/README.md @@ -45,7 +45,7 @@ This example is intended for software engineers and engineering managers who wan ## Prerequisites -- Python 3.9 or higher +- Python 3.10 or higher - A dependency manager ([uv](https://docs.astral.sh/uv/) recommended, [pip](https://pip.pypa.io/en/stable/) also works) ## Running the Example diff --git a/examples/http/requests_and_fastapi/provider.py b/examples/http/requests_and_fastapi/provider.py index 3e925afbf..1275a3806 100644 --- a/examples/http/requests_and_fastapi/provider.py +++ b/examples/http/requests_and_fastapi/provider.py @@ -30,7 +30,7 @@ import logging from datetime import datetime, timezone -from typing import Any, ClassVar, Optional +from typing import Any, ClassVar from fastapi import FastAPI, HTTPException, status from pydantic import BaseModel, Field, field_validator @@ -53,8 +53,8 @@ class User(BaseModel): id: int name: str created_on: datetime = Field(default_factory=lambda: datetime.now(tz=timezone.utc)) - email: Optional[str] = None - ip_address: Optional[str] = None + email: str | None = None + ip_address: str | None = None hobbies: list[str] = Field(default_factory=list) admin: bool = False diff --git a/examples/http/requests_and_fastapi/pyproject.toml b/examples/http/requests_and_fastapi/pyproject.toml index c03a74f95..9b03fc883 100644 --- a/examples/http/requests_and_fastapi/pyproject.toml +++ b/examples/http/requests_and_fastapi/pyproject.toml @@ -5,7 +5,7 @@ name = "example-requests-and-fastapi" description = "Example of using a requests client and FastAPI server with Pact Python" dependencies = ["requests~=2.0", "fastapi~=0.0", "typing-extensions~=4.0"] -requires-python = ">=3.9" +requires-python = ">=3.10" version = "1.0.0" [dependency-groups] diff --git a/examples/http/requests_and_fastapi/test_provider.py b/examples/http/requests_and_fastapi/test_provider.py index a5e1cee9a..6c6fe9e6d 100644 --- a/examples/http/requests_and_fastapi/test_provider.py +++ b/examples/http/requests_and_fastapi/test_provider.py @@ -32,8 +32,7 @@ if TYPE_CHECKING: from pathlib import Path - - from typing_extensions import TypeAlias + from typing import TypeAlias ACTION_TYPE: TypeAlias = Literal["setup", "teardown"] diff --git a/examples/plugins/proto/person_pb2.py b/examples/plugins/proto/person_pb2.py index 3bf786c1f..fd2f14a03 100644 --- a/examples/plugins/proto/person_pb2.py +++ b/examples/plugins/proto/person_pb2.py @@ -12,6 +12,8 @@ This file is generated code. Manual changes (except for documentation improvements) will be overwritten if the file is regenerated. """ +from __future__ import annotations + from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import runtime_version as _runtime_version diff --git a/examples/plugins/proto/person_pb2_grpc.py b/examples/plugins/proto/person_pb2_grpc.py index d613482c0..1e0ebc4e1 100644 --- a/examples/plugins/proto/person_pb2_grpc.py +++ b/examples/plugins/proto/person_pb2_grpc.py @@ -11,6 +11,8 @@ This file is generated and should not be modified manually, except for documentation improvements. """ +from __future__ import annotations + from typing import Any import grpc diff --git a/examples/plugins/protobuf/__init__.py b/examples/plugins/protobuf/__init__.py index f256825a4..c230692cb 100644 --- a/examples/plugins/protobuf/__init__.py +++ b/examples/plugins/protobuf/__init__.py @@ -34,6 +34,8 @@ have a basic understanding of Pact and Protocol Buffers. """ +from __future__ import annotations + from examples.plugins.proto.person_pb2 import AddressBook, Person diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 86932c6ee..4489c0653 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -24,12 +24,12 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.14", "Programming Language :: Python", "Topic :: Software Development :: Testing", ] -requires-python = ">=3.9" +requires-python = ">=3.10" [project.urls] "Bug Tracker" = "https://github.com/pact-foundation/pact-python/issues" @@ -128,7 +128,7 @@ requires = ["hatch-vcs", "hatchling", "packaging"] pre-install-commands = ["uv pip install --group test -e ."] [[tool.hatch.envs.test.matrix]] - python = ["3.10", "3.11", "3.12", "3.13", "3.9"] + python = ["3.10", "3.11", "3.12", "3.13", "3.14"] ################################################################################ ## PyTest Configuration diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index a802f8294..bc0374cd8 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -24,12 +24,12 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.14", "Programming Language :: Python", "Topic :: Software Development :: Testing", ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = ["cffi~=2.0"] @@ -125,7 +125,7 @@ requires = ["hatch-vcs", "hatchling", "packaging", "cffi"] # platform.windows.env-vars = { PATH = "{root}/src/pact_ffi;{env:PATH}" } [[tool.hatch.envs.test.matrix]] - python = ["3.10", "3.11", "3.12", "3.13", "3.9"] + python = ["3.10", "3.11", "3.12", "3.13", "3.14"] ################################################################################ ## PyTest Configuration diff --git a/pact-python-ffi/tests/test_init.py b/pact-python-ffi/tests/test_init.py index 941658bb4..3faf3ff9f 100644 --- a/pact-python-ffi/tests/test_init.py +++ b/pact-python-ffi/tests/test_init.py @@ -11,6 +11,8 @@ are functioning as expected. """ +from __future__ import annotations + import re import pytest diff --git a/pyproject.toml b/pyproject.toml index 456936867..00d3dac85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,12 +27,12 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.14", "Programming Language :: Python", "Topic :: Software Development :: Testing", ] -requires-python = ">=3.9" +requires-python = ">=3.10" # Dependencies of Pact Python should be specified using the broadest range # compatible version unless: @@ -45,7 +45,6 @@ dependencies = [ # External dependencies "cffi~=2.0", "yarl~=1.0", - "typing-extensions~=4.0 ; python_version < '3.10'", ] [project.urls] @@ -123,6 +122,8 @@ types = [ "types-grpcio~=1.0", "types-protobuf~=6.0", "types-requests~=2.0", + # This is required for Python 3.10 support + "typing-extensions~=4.0", ] # Dependencies for v2 example and test environments @@ -224,7 +225,7 @@ requires = ["hatch-vcs", "hatchling"] pre-install-commands = ["uv pip install --group test -e ."] [[tool.hatch.envs.test.matrix]] - python = ["3.10", "3.11", "3.12", "3.13", "3.9"] + python = ["3.10", "3.11", "3.12", "3.13", "3.14"] # Test environment for running unit tests. This automatically tests against all # supported Python versions. @@ -237,7 +238,7 @@ requires = ["hatch-vcs", "hatchling"] all = ["example"] [[tool.hatch.envs.example.matrix]] - python = ["3.10", "3.11", "3.12", "3.13", "3.9"] + python = ["3.10", "3.11", "3.12", "3.13", "3.14"] [tool.hatch.envs.v2-test] features = ["v2"] @@ -250,7 +251,7 @@ requires = ["hatch-vcs", "hatchling"] test = "pytest tests/v2 {args}" [[tool.hatch.envs.v2-test.matrix]] - python = ["3.10", "3.11", "3.12", "3.13", "3.9"] + python = ["3.10", "3.11", "3.12", "3.13", "3.14"] [tool.hatch.envs.v2-example] features = ["v2"] @@ -263,7 +264,7 @@ requires = ["hatch-vcs", "hatchling"] example = "pytest examples/v2 {args}" [[tool.hatch.envs.v2-example.matrix]] - python = ["3.10", "3.11", "3.12", "3.13", "3.9"] + python = ["3.10", "3.11", "3.12", "3.13", "3.14"] ################################################################################ ## UV Workspace diff --git a/src/pact/__init__.py b/src/pact/__init__.py index 1293d1561..e67eaea59 100644 --- a/src/pact/__init__.py +++ b/src/pact/__init__.py @@ -99,6 +99,8 @@ [examples](https://pact-foundation.github.io/pact-python/examples). """ +from __future__ import annotations + from pact.__version__ import __version__, __version_tuple__ from pact.pact import Pact from pact.verifier import Verifier diff --git a/src/pact/__version__.pyi b/src/pact/__version__.pyi index a8b247d06..a019c2c56 100644 --- a/src/pact/__version__.pyi +++ b/src/pact/__version__.pyi @@ -1,4 +1,4 @@ -from typing_extensions import TypeAlias +from typing import TypeAlias __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] diff --git a/src/pact/_server.py b/src/pact/_server.py index 61f9d1a9b..e05c9dfe4 100644 --- a/src/pact/_server.py +++ b/src/pact/_server.py @@ -31,8 +31,6 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar from urllib.parse import urlparse -from typing_extensions import Self - from pact import __version__ from pact._util import find_free_port from pact.types import Message @@ -40,6 +38,8 @@ if TYPE_CHECKING: from types import TracebackType + from typing_extensions import Self + logger = logging.getLogger(__name__) diff --git a/src/pact/_util.py b/src/pact/_util.py index e34020f61..6fe8cc2e4 100644 --- a/src/pact/_util.py +++ b/src/pact/_util.py @@ -7,15 +7,19 @@ notice. """ +from __future__ import annotations + import inspect import logging import socket import warnings -from collections.abc import Callable, Mapping from contextlib import closing from functools import partial from inspect import Parameter, _ParameterKind -from typing import TypeVar +from typing import TYPE_CHECKING, TypeVar + +if TYPE_CHECKING: + from collections.abc import Callable, Mapping logger = logging.getLogger(__name__) diff --git a/src/pact/interaction/__init__.py b/src/pact/interaction/__init__.py index 5d93fa664..e5de3445d 100644 --- a/src/pact/interaction/__init__.py +++ b/src/pact/interaction/__init__.py @@ -70,6 +70,8 @@ in the interaction. """ +from __future__ import annotations + from pact.interaction._async_message_interaction import AsyncMessageInteraction from pact.interaction._base import Interaction from pact.interaction._http_interaction import HttpInteraction diff --git a/src/pact/interaction/_base.py b/src/pact/interaction/_base.py index 410fdfd35..7b0363571 100644 --- a/src/pact/interaction/_base.py +++ b/src/pact/interaction/_base.py @@ -13,7 +13,7 @@ import abc import json -from typing import TYPE_CHECKING, Any, Literal, Optional +from typing import TYPE_CHECKING, Any, Literal import pact_ffi from pact.match.matcher import IntegrationJSONEncoder @@ -21,12 +21,9 @@ if TYPE_CHECKING: from pathlib import Path - from pact.match import AbstractMatcher + from typing_extensions import Self - try: - from typing import Self - except ImportError: - from typing_extensions import Self + from pact.match import AbstractMatcher class Interaction(abc.ABC): @@ -103,7 +100,7 @@ def _interaction_part(self) -> pact_ffi.InteractionPart: def _parse_interaction_part( self, - part: Optional[Literal["Request", "Response"]], + part: Literal["Request", "Response"] | None, ) -> pact_ffi.InteractionPart: """ Convert the input into an InteractionPart. diff --git a/src/pact/interaction/_http_interaction.py b/src/pact/interaction/_http_interaction.py index 5c9496c4f..15b8a8e93 100644 --- a/src/pact/interaction/_http_interaction.py +++ b/src/pact/interaction/_http_interaction.py @@ -17,10 +17,7 @@ if TYPE_CHECKING: from collections.abc import Iterable - try: - from typing import Self - except ImportError: - from typing_extensions import Self + from typing_extensions import Self class HttpInteraction(Interaction): diff --git a/src/pact/interaction/_sync_message_interaction.py b/src/pact/interaction/_sync_message_interaction.py index 42a8453e4..92258d7b1 100644 --- a/src/pact/interaction/_sync_message_interaction.py +++ b/src/pact/interaction/_sync_message_interaction.py @@ -4,11 +4,14 @@ from __future__ import annotations -from typing_extensions import Self +from typing import TYPE_CHECKING import pact_ffi from pact.interaction._base import Interaction +if TYPE_CHECKING: + from typing_extensions import Self + class SyncMessageInteraction(Interaction): """ diff --git a/src/pact/pact.py b/src/pact/pact.py index 5efb3608b..f8aff745c 100644 --- a/src/pact/pact.py +++ b/src/pact/pact.py @@ -68,7 +68,6 @@ from pathlib import Path from typing import ( TYPE_CHECKING, - Callable, Literal, overload, ) @@ -88,15 +87,12 @@ from pact.interaction._sync_message_interaction import SyncMessageInteraction if TYPE_CHECKING: - from collections.abc import Generator + from collections.abc import Callable, Generator from types import TracebackType - from pact.interaction import Interaction + from typing_extensions import Self - try: - from typing import Self - except ImportError: - from typing_extensions import Self + from pact.interaction import Interaction logger = logging.getLogger(__name__) diff --git a/src/pact/types.py b/src/pact/types.py index 6fb083ab2..bc2f021f4 100644 --- a/src/pact/types.py +++ b/src/pact/types.py @@ -8,9 +8,8 @@ from __future__ import annotations -from typing import Any, Literal, TypedDict, Union +from typing import Any, Literal, TypeAlias, TypedDict -from typing_extensions import TypeAlias from yarl import URL Matchable: TypeAlias = Any @@ -133,7 +132,7 @@ class StateHandlerArgs(TypedDict, total=False): """ -StateHandlerUrl: TypeAlias = Union[str, URL] +StateHandlerUrl: TypeAlias = str | URL """ State handler URL signature. diff --git a/src/pact/types.pyi b/src/pact/types.pyi index f5e1ae880..7f4fc61aa 100644 --- a/src/pact/types.pyi +++ b/src/pact/types.pyi @@ -9,10 +9,9 @@ from collections.abc import Set as AbstractSet from datetime import date, datetime, time from decimal import Decimal from fractions import Fraction -from typing import Any, Literal, TypedDict +from typing import Any, Literal, TypeAlias, TypedDict from pydantic import BaseModel -from typing_extensions import TypeAlias from yarl import URL _BaseMatchable: TypeAlias = ( diff --git a/src/pact/verifier.py b/src/pact/verifier.py index 4be20dcb7..50487ca69 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -76,24 +76,30 @@ import json import logging import os -from collections.abc import Mapping +from collections.abc import Callable, Mapping from contextlib import nullcontext from datetime import date from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, overload +from typing import TYPE_CHECKING, Any, Literal, TypedDict, overload -from typing_extensions import Self from yarl import URL import pact_ffi from pact._server import MessageProducer, StateCallback from pact._util import apply_args -from pact.types import UNSET, Message, MessageProducerArgs, StateHandlerArgs, Unset +from pact.types import ( + UNSET, + Message, + MessageProducerArgs, + StateHandlerArgs, + StateHandlerUrl, + Unset, +) if TYPE_CHECKING: from collections.abc import Iterable - from pact.types import StateHandlerUrl + from typing_extensions import Self logger = logging.getLogger(__name__) @@ -582,9 +588,7 @@ def state_handler( TypeError: If the handler type is invalid. """ - # A tuple is required instead of `StateHandlerUrl` for support for - # Python 3.9. This should be changed to `StateHandlerUrl` in the future. - if isinstance(handler, (str, URL)): + if isinstance(handler, StateHandlerUrl): if body is None: msg = "The `body` parameter must be a boolean when providing a URL" raise ValueError(msg) diff --git a/tests/compatibility_suite/conftest.py b/tests/compatibility_suite/conftest.py index fd85d878b..26b910c3b 100644 --- a/tests/compatibility_suite/conftest.py +++ b/tests/compatibility_suite/conftest.py @@ -5,11 +5,12 @@ submodule has been initialized before running the tests. """ +from __future__ import annotations + import shutil import subprocess -from collections.abc import Generator from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import pytest from testcontainers.compose import DockerCompose # type: ignore[import-untyped] @@ -17,6 +18,9 @@ from pact.verifier import Verifier +if TYPE_CHECKING: + from collections.abc import Generator + @pytest.fixture(scope="session", autouse=True) def _submodule_init() -> None: diff --git a/tests/compatibility_suite/test_v3_generators.py b/tests/compatibility_suite/test_v3_generators.py index 3e5ec82ef..d02aee036 100644 --- a/tests/compatibility_suite/test_v3_generators.py +++ b/tests/compatibility_suite/test_v3_generators.py @@ -1,8 +1,10 @@ """Test of V3 generators.""" +from __future__ import annotations + import logging import re -from collections.abc import Callable +from typing import TYPE_CHECKING import pytest import requests @@ -18,6 +20,9 @@ from tests.compatibility_suite.util import parse_horizontal_table from tests.compatibility_suite.util.interaction_definition import InteractionDefinition +if TYPE_CHECKING: + from collections.abc import Callable + logger = logging.getLogger(__name__) diff --git a/tests/compatibility_suite/test_v3_http_matching.py b/tests/compatibility_suite/test_v3_http_matching.py index 1d664b598..6de96aa2b 100644 --- a/tests/compatibility_suite/test_v3_http_matching.py +++ b/tests/compatibility_suite/test_v3_http_matching.py @@ -1,9 +1,10 @@ """Matching HTTP parts (request or response) feature tests.""" +from __future__ import annotations + import re import sys -from collections.abc import Generator -from pathlib import Path +from typing import TYPE_CHECKING import pytest from pytest_bdd import ( @@ -15,12 +16,17 @@ ) from pact import Pact -from pact.verifier import Verifier from tests.compatibility_suite.util.interaction_definition import ( InteractionDefinition, ) from tests.compatibility_suite.util.provider import Provider +if TYPE_CHECKING: + from collections.abc import Generator + from pathlib import Path + + from pact.verifier import Verifier + ################################################################################ ## Scenarios ################################################################################ diff --git a/tests/compatibility_suite/util/__init__.py b/tests/compatibility_suite/util/__init__.py index 440cfc262..e227e9186 100644 --- a/tests/compatibility_suite/util/__init__.py +++ b/tests/compatibility_suite/util/__init__.py @@ -177,7 +177,7 @@ def parse_horizontal_table(content: list[list[str]]) -> list[dict[str, str]]: msg = f"Expected at least two rows in the table, got {len(content)}" raise ValueError(msg) - return [dict(zip(content[0], row)) for row in content[1:]] + return [dict(zip(content[0], row, strict=True)) for row in content[1:]] def parse_vertical_table(content: list[list[str]]) -> dict[str, str]: diff --git a/tests/compatibility_suite/util/consumer.py b/tests/compatibility_suite/util/consumer.py index a5fa26b80..237a9b804 100644 --- a/tests/compatibility_suite/util/consumer.py +++ b/tests/compatibility_suite/util/consumer.py @@ -7,12 +7,11 @@ import json import logging import re -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeGuard import pytest import requests from pytest_bdd import given, parsers, then, when -from typing_extensions import TypeGuard from yarl import URL from pact import Pact diff --git a/tests/compatibility_suite/util/interaction_definition.py b/tests/compatibility_suite/util/interaction_definition.py index 1b91e89a0..5b4e69b2c 100644 --- a/tests/compatibility_suite/util/interaction_definition.py +++ b/tests/compatibility_suite/util/interaction_definition.py @@ -17,7 +17,6 @@ from xml.etree import ElementTree as ET from multidict import MultiDict -from typing_extensions import Self from yarl import URL from pact.interaction import HttpInteraction, Interaction @@ -32,6 +31,8 @@ from http.server import SimpleHTTPRequestHandler from pathlib import Path + from typing_extensions import Self + from pact.interaction import Interaction from pact.pact import Pact from pact.types import Message diff --git a/tests/compatibility_suite/util/provider.py b/tests/compatibility_suite/util/provider.py index bc78fb4b4..9c42c4940 100644 --- a/tests/compatibility_suite/util/provider.py +++ b/tests/compatibility_suite/util/provider.py @@ -31,7 +31,6 @@ import requests from multidict import CIMultiDict from pytest_bdd import given, parsers, then, when -from typing_extensions import Self from yarl import URL import pact_cli @@ -53,6 +52,8 @@ from pathlib import Path from types import TracebackType + from typing_extensions import Self + from pact.types import Message from pact.verifier import Verifier diff --git a/tests/conftest.py b/tests/conftest.py index 9e99b3a75..f41ae4845 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,8 @@ directory. """ +from __future__ import annotations + import json import tempfile from pathlib import Path diff --git a/tests/test_error.py b/tests/test_error.py index a49ea5cb1..b945a7b26 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -2,6 +2,8 @@ Error handling and mismatch tests. """ +from __future__ import annotations + import re import aiohttp diff --git a/tests/test_match.py b/tests/test_match.py index a2bfb03cc..f533fd6ab 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -2,18 +2,19 @@ Example test to show usage of matchers (and generators by extension). """ +from __future__ import annotations + import logging import re import subprocess import sys import time -from collections.abc import Generator from contextlib import contextmanager from datetime import datetime from pathlib import Path from random import randint, uniform from threading import Thread -from typing import NoReturn +from typing import TYPE_CHECKING, NoReturn import requests from flask import Flask, Response, make_response @@ -21,6 +22,9 @@ from pact import Pact, Verifier, generate, match +if TYPE_CHECKING: + from collections.abc import Generator + logger = logging.getLogger(__name__) diff --git a/tests/test_server.py b/tests/test_server.py index dab2fb5ae..3526ac1d5 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -2,6 +2,8 @@ Tests for `pact._server` module. """ +from __future__ import annotations + import json from unittest.mock import MagicMock diff --git a/tests/test_verifier.py b/tests/test_verifier.py index 18580eb42..0de481b0a 100644 --- a/tests/test_verifier.py +++ b/tests/test_verifier.py @@ -6,6 +6,8 @@ that is handled by the compatibility suite. """ +from __future__ import annotations + import re from pathlib import Path From f1816d36f51c279b4f5e2356d5a639c3b9c3b994 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 6 Oct 2025 12:07:36 +1100 Subject: [PATCH 1047/1376] chore(ci): disable 3.14 tests using pydantic Pydantic 3.12 is going to be released shortly, but until then, any test that depends on Pydantic will not pass CI. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ddcb1fdd7..5ff75fff9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -99,6 +99,8 @@ jobs: run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml - name: Run tests (v2) + # Temporary workaround until Pydantic 3.12 is released with Python 3.14 support + if: matrix.python-version != '3.14' run: hatch run v2-test.py${{ matrix.python-version }}:test --junit-xml=v2-junit.xml - name: Run tests (CLI) @@ -141,7 +143,9 @@ jobs: - '3.11' - '3.12' - '3.13' - - '3.14' + # Temporarily excluded until Pydantic 3.12 is released with Python + # 3.14 support + # - '3.14' steps: - name: Checkout code From 88cb8960dcfa4480fa0a8762b09662c607df1eb5 Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Mon, 6 Oct 2025 01:54:42 +0000 Subject: [PATCH 1048/1376] docs: update changelog for pact-python/3.0.0 --- CHANGELOG.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c9d07e55..81d3db623 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,77 @@ All notable changes to this project will be documented in this file. +## [pact-python/3.0.0] _2025-10-06_ + +### 🚀 Features + +- [**breaking**] Default to v4 specification + > Pact instances default to version 4 of the Pact specification (previously used version 3). This should be mostly backwards compatible, but can be reverted by using `with_specification("V3")`. +- Populate broker source from env + +### 🚜 Refactor + +- _(ci)_ If statement + +### 🎨 Styling + +- _(tests)_ Add sections + +### 📚 Documentation + +- Update changelog for pact-python/3.0.0a1 +- Add mascot +- Give mascot outline +- Set mascot width and height +- _(examples)_ Add requests and fastapi +- Generate llms.txt +- Update mkdocs material features +- Fix CI badge links +- Update matcher docs +- Improve matchers +- Improve generators +- Update for v3 and add migration guide + +### ⚙️ Miscellaneous Tasks + +- _(ci)_ Remove spelling check +- _(examples)_ Minor improvements +- Store hatch venv in .venv +- Update mismatch repr +- Save mismatches before exiting the server +- _(examples)_ Remove old http example +- Fix sub-project git cliff config +- Hide import from traceback +- Fix flask integer coercion +- Add v3 matching rules test +- Add v4 matching rules tests +- _(ci)_ Add publish as completion dependency +- _(tests)_ Add generators to interaction defn +- _(tests)_ Test v3 generators +- _(test)_ Add v4 generators tests +- Re-add pytest rerunfailrure +- _(tests)_ Add v3 http generators +- Prefer prek over pre-commit +- Disable reruns in vscode +- _(ci)_ Fix prek caching +- _(ci)_ Generate junit xml files +- Move mascot file out of root +- Update uuid format names +- Fix import warning +- Make Unset falsey +- [**breaking**] Rename abstract matcher class + > The abstract `pact.match.Matcher` class has been renamed to `pact.match.AbstractMatcher`. +- [**breaking**] Rename abstract generator + > The abstract `pact.generate.Generator` class has been renamed to `pact.generate.AbstractGenerator`. +- Clarify explanation of given +- [**breaking**] Drop python 3.9 add 3.14 + > Python 3.9 is no longer supported. +- _(ci)_ Disable 3.14 tests using pydantic + +### Contributors + +- @JP-Ellis + ## [pact-python/3.0.0a1] _2025-08-12_ ### 🚀 Features @@ -562,7 +633,6 @@ All notable changes to this project will be documented in this file. - _(ci)_ Speed up wheels building on prs - _(ci)_ Add caching - Migrate from flat to src layout -- _(docs)_ Update changelog - _(ci)_ Automate release process - _(v3)_ Add warning on pact.v3 import - _(ci)_ Remove check of wheels @@ -1517,4 +1587,4 @@ All notable changes to this project will be documented in this file. - @matthewbalvanz-wf - @mefellows - + From a76857dac990b6b357f45ef878f90eaf5ae0e785 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 6 Oct 2025 15:51:15 +1100 Subject: [PATCH 1049/1376] chore: drop cffi dependency Since the FFI has been moved into its own standalone `pact-python-ffi` package, the core Pact Python library has no need for `cffi`. Signed-off-by: JP-Ellis --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 00d3dac85..8f4477375 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,6 @@ dependencies = [ # Pact dependencies "pact-python-ffi~=0.4.0", # External dependencies - "cffi~=2.0", "yarl~=1.0", ] @@ -118,7 +117,6 @@ test = [ ] types = [ "mypy==1.18.2", - "types-cffi~=1.0", "types-grpcio~=1.0", "types-protobuf~=6.0", "types-requests~=2.0", From 80e4f215591c8249dd9784e9756e3b13d2186767 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 6 Oct 2025 16:59:43 +1100 Subject: [PATCH 1050/1376] chore(ci): fix publish step There was an issue with releasing Pact Python 3.0.1 and Pact Python FFI 0.4.28.2 whereby the release changelog could not be generated (despite the rest of the changelog being generated fine). This inserts a placeholder to be filled in, as opposed to failing the release entirely. Ref: https://github.com/pact-foundation/pact-python/actions/runs/18270656326/job/52012475672 Signed-off-by: JP-Ellis --- .github/workflows/build-cli.yml | 23 +++++++++++++++++++---- .github/workflows/build-ffi.yml | 23 +++++++++++++++++++---- .github/workflows/build.yml | 23 +++++++++++++++++++---- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 475d979c0..60c4e6675 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -154,12 +154,27 @@ jobs: id: release-changelog working-directory: pact-python-cli run: | - git cliff \ + if ! git cliff \ --current \ --strip header \ - --output ${{ runner.temp }}/release-changelog.md - - echo -e "\n\n## Pull Requests\n\n" >> ${{ runner.temp }}/release-changelog.md + --output ${{ runner.temp }}/release-changelog.md; then + { + echo "> [!WARNING]" + echo ">" + echo "> No changelog generated. To be filled in." + } > ${{ runner.temp }}/release-changelog.md + fi + + { + echo "" + echo "
" + echo "" + echo "" + echo "## Pull Requests" + echo "" + echo "" + echo "" + } >> ${{ runner.temp }}/release-changelog.md env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index ac81f07fd..4c9b61556 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -155,12 +155,27 @@ jobs: id: release-changelog working-directory: pact-python-ffi run: | - git cliff \ + if ! git cliff \ --current \ --strip header \ - --output ${{ runner.temp }}/release-changelog.md - - echo -e "\n\n## Pull Requests\n\n" >> ${{ runner.temp }}/release-changelog.md + --output ${{ runner.temp }}/release-changelog.md ; then + { + echo "> [!WARNING]" + echo ">" + echo "> No changelog generated. To be filled in." + } > ${{ runner.temp }}/release-changelog.md + fi + + { + echo "" + echo "
" + echo "" + echo "" + echo "## Pull Requests" + echo "" + echo "" + echo "" + } >> ${{ runner.temp }}/release-changelog.md env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f74b665e2..765dbc3b9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -121,12 +121,27 @@ jobs: - name: Generate release changelog id: release-changelog run: | - git cliff \ + if ! git cliff \ --current \ --strip header \ - --output ${{ runner.temp }}/release-changelog.md - - echo -e "\n\n## Pull Requests\n\n" >> ${{ runner.temp }}/release-changelog.md + --output ${{ runner.temp }}/release-changelog.md; then + { + echo "> [!WARNING]" + echo ">" + echo "> No changelog generated. To be filled in." + } > ${{ runner.temp }}/release-changelog.md + fi + + { + echo "" + echo "
" + echo "" + echo "" + echo "## Pull Requests" + echo "" + echo "" + echo "" + } >> ${{ runner.temp }}/release-changelog.md env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} From 66dc084662facee26431b62915cb79a2a4b13bc1 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 6 Oct 2025 06:27:56 +0000 Subject: [PATCH 1051/1376] docs: update changelogs Signed-off-by: JP-Ellis --- CHANGELOG.md | 15 +++++++++++++++ pact-python-ffi/CHANGELOG.md | 18 +++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81d3db623..84f82c50e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ All notable changes to this project will be documented in this file. +## [pact-python/3.0.1] _2025-10-06_ + +### 📚 Documentation + +- Update changelog for pact-python/3.0.0 + +### ⚙️ Miscellaneous Tasks + +- Drop cffi dependency +- _(ci)_ Fix publish step + +### Contributors + +- @JP-Ellis + ## [pact-python/3.0.0] _2025-10-06_ ### 🚀 Features diff --git a/pact-python-ffi/CHANGELOG.md b/pact-python-ffi/CHANGELOG.md index 59d698a79..af61c39e8 100644 --- a/pact-python-ffi/CHANGELOG.md +++ b/pact-python-ffi/CHANGELOG.md @@ -8,6 +8,22 @@ Note that this _only_ includes changes to the Python FFI interface. For changes +## [pact-python-ffi/0.4.28.2] _2025-10-06_ + +### 📚 Documentation + +- Update changelog for pact-python-ffi/0.4.28.1 +- Fix CI badge links + +### ⚙️ Miscellaneous Tasks + +- [**breaking**] Drop python 3.9 add 3.14 + > Python 3.9 is no longer supported. + +### Contributors + +- @JP-Ellis + ## [pact-python-ffi/0.4.28.1] _2025-08-28_ ### 🐛 Bug Fixes @@ -73,4 +89,4 @@ Note that this _only_ includes changes to the Python FFI interface. For changes - @JP-Ellis - + From 54c20021671444bc21de8aefef80de71edccb3ad Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 6 Oct 2025 22:03:32 +1100 Subject: [PATCH 1052/1376] chore(ci): add area-core label Signed-off-by: JP-Ellis --- .github/labels.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/labels.yml b/.github/labels.yml index 41e6e777a..d1bd66533 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -3,6 +3,10 @@ description: Relating to the CLI color: C2E0C6 +- name: area:core + description: Relating to the core Pact Python library + color: C2E0C6 + - name: area:examples description: Relating to the examples color: C2E0C6 From 8bb0c84e535e9d77ed32fa5399f3b97f816b8b8c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 6 Oct 2025 22:09:27 +1100 Subject: [PATCH 1053/1376] chore(ci): fix labels workflow permissions Signed-off-by: JP-Ellis --- .github/workflows/labels.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index bfa0ec8af..d238dda01 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -13,6 +13,9 @@ on: paths: - .github/labels.yml +permissions: + issues: write + jobs: sync-labels: name: Synchronise labels From 8223d8b3213a74863003fa0163d9760f3926cdad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 08:17:33 +1100 Subject: [PATCH 1054/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.38.0 (#1278) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7042f17e6..f6acdcf32 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -77,7 +77,7 @@ repos: )$ - repo: https://github.com/crate-ci/typos - rev: v1.37.2 + rev: v1.38.0 hooks: - id: typos exclude: | From 2e1b16bab5bb264e8cc8cf2799334da2ae78886f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 7 Oct 2025 15:45:08 +1100 Subject: [PATCH 1055/1376] chore: remove no longer relevant todo Signed-off-by: JP-Ellis --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8f4477375..910eede92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -332,9 +332,6 @@ requires = ["hatch-vcs", "hatchling"] ## Ruff Configuration ################################################################################ [tool.ruff] - -# TODO: Don't check v2 files -# https://github.com/pact-foundation/pact-python/issues/458 extend-exclude = ["src/pact/v2/*", "tests/v2/*", "examples/v2/*"] [tool.ruff.lint] From 8b503a84de679307504e7fab339b578fe319371f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 7 Oct 2025 15:42:47 +1100 Subject: [PATCH 1056/1376] chore(docs): use normalized project url keys While they are, for the most part, equivalent, I prefer to avoid using the quotes and use the normalised keys as documented in . Signed-off-by: JP-Ellis --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 910eede92..c06dbf2bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,11 +47,11 @@ dependencies = [ ] [project.urls] - "Bug Tracker" = "https://github.com/pact-foundation/pact-python/issues" - "Changelog" = "https://github.com/pact-foundation/pact-python/blob/main/CHANGELOG.md" - "Documentation" = "https://docs.pact.io" - "Homepage" = "https://pact.io" - "Repository" = "https://github.com/pact-foundation/pact-python" + changelog = "https://github.com/pact-foundation/pact-python/blob/main/CHANGELOG.md" + documentation = "https://pact-foundation.github.io/pact-python/" + homepage = "https://pact.io" + issues = "https://github.com/pact-foundation/pact-python/issues" + source = "https://github.com/pact-foundation/pact-python" [project.scripts] pact-verifier = "pact.v2.cli.verify:main" From db041b688d0bf70a6d4ad0cf66c64fb662642466 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 7 Oct 2025 15:45:32 +1100 Subject: [PATCH 1057/1376] fix!: replace v2 extra with compat-v2 The `v2` group of optional dependencies unfortunately results in an issue when Pact Python is installed using `pip`, as `pip` tries to parse the group name as if it were a version. To avoid this, the group has been renamed to `compat-v2` which should bypass the version parsing. While this is a bug in `pip`, pip comes pre-bundled in many situations and is rarely at the latest version; so it is impractical to wait for the upstream issue to be resolved. BREAKING CHANGE: Installing Pact Python with v2 compatibility requires `pip install 'pact-python[compat-v2]'`, and the old `pip install 'pact-python[v2]'` is no longer supported. Fixes: https://github.com/pact-foundation/pact-python/issues/1275 Ref: https://github.com/pypa/packaging/issues/938 Signed-off-by: JP-Ellis --- MIGRATION.md | 4 ++-- pyproject.toml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index a79cfb628..c5186d8c3 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -20,10 +20,10 @@ This document outlines the key changes and migration steps for users transitioni For teams with larger codebases that need time to fully migrate to the new v3 API, a backwards compatibility module is provided at `pact.v2`. This module contains the same API as Pact Python v2.x and serves as an interim measure to assist gradual migration. -To use the v2 compatibility module, you must install pact-python with the `v2` feature enabled: +To use the v2 compatibility module, you must install pact-python with the `compat-v2` feature enabled: ```bash -pip install pact-python[v2] +pip install pact-python[compat-v2] ``` All existing `pact.*` imports need to be updated to use `pact.v2.*` instead. Here are some common examples: diff --git a/pyproject.toml b/pyproject.toml index c06dbf2bd..ffc7b934f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ dependencies = [ [project.optional-dependencies] # Dependencies required for v2 only - v2 = [ + compat-v2 = [ # Pact dependencies "pact-python-cli~=2.0", # External dependencies @@ -239,7 +239,7 @@ requires = ["hatch-vcs", "hatchling"] python = ["3.10", "3.11", "3.12", "3.13", "3.14"] [tool.hatch.envs.v2-test] - features = ["v2"] + features = ["compat-v2"] installer = "uv" path = ".venv/v2-test" pre-install-commands = ["uv pip install --group test-v2 -e ."] @@ -252,7 +252,7 @@ requires = ["hatch-vcs", "hatchling"] python = ["3.10", "3.11", "3.12", "3.13", "3.14"] [tool.hatch.envs.v2-example] - features = ["v2"] + features = ["compat-v2"] installer = "uv" path = ".venv/v2-example" pre-install-commands = ["uv pip install --group example-v2 -e ."] From 2fd2cd019e9d54d1b602a69e04f057256cc60c7f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:18:26 +1100 Subject: [PATCH 1058/1376] chore(deps): update softprops/action-gh-release action to v2.4.0 (#1279) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 60c4e6675..bc27c73c4 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -186,7 +186,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 + uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 4c9b61556..1804a853f 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -187,7 +187,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 + uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 765dbc3b9..8bd7a6e42 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,7 +153,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 + uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md From 1bf6a8b2216011ea3cc43d139a24c9a82dbe689b Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Tue, 7 Oct 2025 05:32:42 +0000 Subject: [PATCH 1059/1376] docs: update changelog for pact-python/3.1.0 --- CHANGELOG.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f82c50e..dc633af41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,28 @@ All notable changes to this project will be documented in this file. +## [pact-python/3.1.0] _2025-10-07_ + +### 🐛 Bug Fixes + +- [**breaking**] Replace v2 extra with compat-v2 + > Installing Pact Python with v2 compatibility requires `pip install 'pact-python[compat-v2]'`, and the old `pip install 'pact-python[v2]'` is no longer supported. + +### 📚 Documentation + +- Update changelogs + +### ⚙️ Miscellaneous Tasks + +- _(ci)_ Add area-core label +- _(ci)_ Fix labels workflow permissions +- Remove no longer relevant todo +- _(docs)_ Use normalized project url keys + +### Contributors + +- @JP-Ellis + ## [pact-python/3.0.1] _2025-10-06_ ### 📚 Documentation @@ -1602,4 +1624,4 @@ All notable changes to this project will be documented in this file. - @matthewbalvanz-wf - @mefellows - + From 1d14601df05648e041947a0928733dbbf47c0ee3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 07:53:10 +1100 Subject: [PATCH 1060/1376] chore(deps): update astral-sh/setup-uv action to v7 (#1286) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index bc27c73c4..885f2f4c2 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 1804a853f..4fc9f720c 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8bd7a6e42..36188c274 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c0fcaabd4..e931b27bc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ff75fff9..1c334f31d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 with: enable-cache: true @@ -154,7 +154,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 with: enable-cache: true @@ -197,7 +197,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 with: enable-cache: true @@ -229,7 +229,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 with: enable-cache: true @@ -262,7 +262,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 with: enable-cache: true @@ -302,7 +302,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 with: enable-cache: true cache-suffix: prek From 004dffb4299c84bea4d9d9b3a83ded1cd73be6e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 07:53:22 +1100 Subject: [PATCH 1061/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.38.1 (#1284) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f6acdcf32..a250fbff8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -77,7 +77,7 @@ repos: )$ - repo: https://github.com/crate-ci/typos - rev: v1.38.0 + rev: v1.38.1 hooks: - id: typos exclude: | From acd125d1930725053e41a63c88a68847bf1d4ce6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 07:53:44 +1100 Subject: [PATCH 1062/1376] chore(deps): update ruff to v0.14.0 (#1285) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a250fbff8..d34b0d622 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.14.0 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 4489c0653..f5be3295c 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.13.3", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.0", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=8.0"] types = ["mypy==1.18.2"] From 69db1639baa64dae1363cebfa5d95f730b1ef926 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 10 Oct 2025 11:01:38 +1100 Subject: [PATCH 1063/1376] chore: add llm instructions Signed-off-by: JP-Ellis --- .github/instructions/pact-cli.instructions.md | 11 ++++ .github/instructions/pact-ffi.instructions.md | 15 ++++++ .github/instructions/pact-v2.instructions.md | 14 +++++ .github/instructions/pact.instructions.md | 21 ++++++++ .../instructions/python-tests.instructions.md | 31 +++++++++++ .github/instructions/python.instructions.md | 52 +++++++++++++++++++ 6 files changed, 144 insertions(+) create mode 100644 .github/instructions/pact-cli.instructions.md create mode 100644 .github/instructions/pact-ffi.instructions.md create mode 100644 .github/instructions/pact-v2.instructions.md create mode 100644 .github/instructions/pact.instructions.md create mode 100644 .github/instructions/python-tests.instructions.md create mode 100644 .github/instructions/python.instructions.md diff --git a/.github/instructions/pact-cli.instructions.md b/.github/instructions/pact-cli.instructions.md new file mode 100644 index 000000000..e1b08ca03 --- /dev/null +++ b/.github/instructions/pact-cli.instructions.md @@ -0,0 +1,11 @@ +--- +description: "Pact CLI" +applyTo: "/pact-python-cli/**" +--- + +# Pact CLI + +- This directory contains the source for the `pact-python-cli` package, which provides a thin wrapper around the Pact CLI binaries. +- It allows users to install the Pact CLI tools via PyPI and use them in Python projects without requiring separate installation steps. +- The package includes executable wrappers for all major Pact CLI tools (`pact`, `pact-broker`, `pact-message`, `pact-mock-service`, `pact-provider-verifier`, etc.). +- By default, it uses bundled binaries, but can fall back to system-installed Pact CLI tools when `PACT_USE_SYSTEM_BINS` environment variable is set to `TRUE` or `YES`. diff --git a/.github/instructions/pact-ffi.instructions.md b/.github/instructions/pact-ffi.instructions.md new file mode 100644 index 000000000..da62203b6 --- /dev/null +++ b/.github/instructions/pact-ffi.instructions.md @@ -0,0 +1,15 @@ +--- +description: "Pact FFI" +applyTo: "/pact-python-ffi/**" +--- + +# Pact FFI + +- This directory contains the source for the `pact-python-ffi` package, which provides Python bindings to the Pact FFI library. +- This library only exposes low-level FFI bindings and is not intended for direct use by end users. All user-facing functionality should be provided through the higher-level `pact` package. +- Code in this package should focus exclusively on: + - Providing automatic memory management for FFI objects (implementing `__del__` methods to drop/free objects as needed) + - Converting between Python types and FFI types (input parameter casting and return value conversion) + - Handling errors returned from the FFI and converting them into appropriate Python exceptions + - Wrapping low-level C structs and handles in Python classes with proper lifecycle management +- Avoid implementing high-level business logic or convenience methods - these belong in the main `pact` package. diff --git a/.github/instructions/pact-v2.instructions.md b/.github/instructions/pact-v2.instructions.md new file mode 100644 index 000000000..cac47aea6 --- /dev/null +++ b/.github/instructions/pact-v2.instructions.md @@ -0,0 +1,14 @@ +--- +description: "Pact V2" +applyTo: "/src/pact/v2/**" +--- + +# Pact V2 Legacy Code + +- These files provide backwards compatibility with version 2 of Pact Python. +- They are in maintenance mode with only critical bug fixes being applied - no new features should be added. +- When making changes: + - Preserve existing APIs and behavior to maintain backwards compatibility + - Prioritize minimal, targeted fixes over architectural improvements + - Ensure changes do not break existing user code +- New development should focus on the main V3+ codebase in `/src/pact/` instead. diff --git a/.github/instructions/pact.instructions.md b/.github/instructions/pact.instructions.md new file mode 100644 index 000000000..9a6a35e23 --- /dev/null +++ b/.github/instructions/pact.instructions.md @@ -0,0 +1,21 @@ +--- +description: "Pact Core Library" +applyTo: "/src/pact/**" +--- + +# Pact Core Library + +- The code in `src/pact/` forms the core Pact library that provides the main user-facing APIs. +- This is the primary codebase for Pact Python functionality. +- Key modules include: + - `pact.py` - Main Pact class for consumer testing + - `verifier.py` - Provider verification functionality + - `match/` - Matching rules and matchers + - `generate/` - Value generators + - `interaction/` - Interaction building blocks + +## V2 Legacy Code + +- Files in `v2/` subdirectories implement the legacy version of Pact Python +- This version is in maintenance mode with only critical bug fixes being applied +- New features and active development should focus on the main codebase diff --git a/.github/instructions/python-tests.instructions.md b/.github/instructions/python-tests.instructions.md new file mode 100644 index 000000000..e5461847e --- /dev/null +++ b/.github/instructions/python-tests.instructions.md @@ -0,0 +1,31 @@ +--- +description: "Python testing conventions and guidelines" +applyTo: "**/tests/**/*.py" +--- + +# Python Testing Conventions + +- Use `pytest` as the testing framework, with all tests located in `tests/` directories and files prefixed with `test_`. +- Prefer descriptive function names that clearly indicate the test's purpose. Include docstrings only when additional context is needed beyond the function name. +- Use `@pytest.mark.parametrize` to cover multiple scenarios without code duplication: + + ```python + @pytest.mark.parametrize( + ("param1", "param2", "expected"), + [ + pytest.param(v1, x1, r1, id="description1"), + pytest.param(v2, x2, r2, id="description2"), + ... + ] + ) + def test_function(param1: Type1, param2: Type2, expected: ReturnType) -> None: + ... + ``` + +- Ensure test coverage for: + - Critical application paths and core functionality + - Common edge cases (empty inputs, invalid data types, boundary conditions) + - Error conditions and exception handling +- Include comments explaining complex test logic or edge case rationale. +- Minimize mocking and prefer testing with real data and dependencies when practical. Mock only external services or components that are unreliable or expensive to test against. +- Use pytest fixtures for common test setup and shared data. diff --git a/.github/instructions/python.instructions.md b/.github/instructions/python.instructions.md new file mode 100644 index 000000000..394240630 --- /dev/null +++ b/.github/instructions/python.instructions.md @@ -0,0 +1,52 @@ +--- +description: "Python coding conventions and guidelines" +applyTo: "**/*.py" +--- + +# Python Coding Conventions + +## Documentation + +- MkDocs-Material is used for documentation generation, allowing for Markdown formatting in docstrings. +- All functions must have Google-style, Markdown-compatible docstrings with proper formatting (note whitespace and indentation as shown): + + ```python + def function_name(param1: Type1, param2: Type2) -> ReturnType: + """ + Brief description of the function. + + Optional detailed description of the function. + + Args: + param1: + Description of param1. + + param2: + Description of param2. + + Returns: + Description of the return value. + """ + ``` + +- References to other functions, classes, or modules must be linked, using the fully qualified Python path: + + ```markdown + A link to a [`ClassName.method`][pact.module.ClassName.method] or a + [`function_name`][pact.module.function_name]. + ``` + +## General Instructions + +- Always prioritize readability and clarity. +- All functions must use type annotations for parameters and return types. Prefer generic types (e.g., `Iterable[str]`, `Mapping[str, int]`) over concrete types (`list[str]`, `dict[str, int]`) for better flexibility and reusability. +- Write code with good maintainability practices, including comments on why certain design decisions were made. +- Handle edge cases and write clear exception handling. +- Write concise, efficient, and idiomatic code that is also easily understandable. +- When performing validations, use early returns to reduce nesting and improve readability. + - Prefer built-in exceptions (such as `ValueError` for invalid values, `TypeError` for incorrect types, etc.) for standard Python errors. + - For Pact-specific issues, define and use custom exceptions to provide clear and meaningful error handling. These must all inherit from the `PactError` base class, and may inherit from other exceptions as appropriate. + +## Code Style and Linting + +- Use `ruff` for linting and formatting, preferring automatic fixes where possible. From 73a435bbd093aefc28c547d9d85ad6cc8983f79a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Oct 2025 11:31:31 +1100 Subject: [PATCH 1064/1376] chore(deps): update softprops/action-gh-release action to v2.4.1 (#1288) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 885f2f4c2..7c288113e 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -186,7 +186,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 4fc9f720c..b9e3aeb93 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -187,7 +187,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 36188c274..9e6f6fe1d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,7 +153,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md From 93f8fe6c9b9569c3bd324bde060a3fd15edf793e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 08:18:45 +1100 Subject: [PATCH 1065/1376] chore(deps): update pypa/cibuildwheel action to v3.2.1 (#1289) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 7c288113e..3dbfee1ab 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@7c619efba910c04005a835b110b057fc28fd6e93 # v3.2.0 + uses: pypa/cibuildwheel@9c00cb4f6b517705a3794b22395aedc36257242c # v3.2.1 with: package-dir: pact-python-cli env: diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index b9e3aeb93..0511e1b50 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@7c619efba910c04005a835b110b057fc28fd6e93 # v3.2.0 + uses: pypa/cibuildwheel@9c00cb4f6b517705a3794b22395aedc36257242c # v3.2.1 with: package-dir: pact-python-ffi env: From d474a6e132f3314c0692026dbd9eb34e888a2a0f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Oct 2025 21:20:15 +0000 Subject: [PATCH 1066/1376] chore(deps): update astral-sh/setup-uv action to v7.1.0 (#1290) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 3dbfee1ab..5e9e763be 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 + uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 0511e1b50..3cbf5582a 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 + uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e6f6fe1d..e6cde1a4e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 + uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e931b27bc..ef4feb759 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 + uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c334f31d..a193c8a08 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 + uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 with: enable-cache: true @@ -154,7 +154,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 + uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 with: enable-cache: true @@ -197,7 +197,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 + uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 with: enable-cache: true @@ -229,7 +229,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 + uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 with: enable-cache: true @@ -262,7 +262,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 + uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 with: enable-cache: true @@ -302,7 +302,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # v7.0.0 + uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 with: enable-cache: true cache-suffix: prek From f0b43e9435eb0647d6bb6494cd59029c49f40196 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 14:08:12 +1100 Subject: [PATCH 1067/1376] chore(deps): update taiki-e/install-action action to v2.62.28 (#1291) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 5e9e763be..c8af60dd5 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@f355b1dcaf1a1c56ccead97cc540a259faf4bd5a # v2.62.20 + uses: taiki-e/install-action@e7ef886cf8f69c25ecef6bbc2858a42e273496ec # v2.62.28 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 3cbf5582a..d67aa2f97 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@f355b1dcaf1a1c56ccead97cc540a259faf4bd5a # v2.62.20 + uses: taiki-e/install-action@e7ef886cf8f69c25ecef6bbc2858a42e273496ec # v2.62.28 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6cde1a4e..d1b9ecdd9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@f355b1dcaf1a1c56ccead97cc540a259faf4bd5a # v2.62.20 + uses: taiki-e/install-action@e7ef886cf8f69c25ecef6bbc2858a42e273496ec # v2.62.28 with: tool: git-cliff,typos From 776829b603b75cdc5e1a7920e745074eac499622 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 13 Oct 2025 15:36:19 +1100 Subject: [PATCH 1068/1376] docs: add agents.md Signed-off-by: JP-Ellis --- AGENTS.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..02a996e16 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,34 @@ +# Pact Python - Quick Reference for AI Agents + +## Commands + +- **Test**: `hatch run test` (all tests) or call `pytest` directly. +- **Lint**: `hatch run lint --fix` (check+fix) or `hatch run format` (auto-fix) +- **Typecheck**: `hatch run typecheck` (all) +- **Examples**: `hatch run example` (run example tests) +- **All checks**: `hatch run all` (format, lint, test, typecheck) +- **V2 tests**: `hatch run v2-test:test` (legacy v2 compatibility tests) + +## Code Style + +- **Imports**: Use generic types (`Iterable`, `Sequence`, `Mapping`) over concrete (`list`, `dict`). Absolute imports only (no relative). +- **Types**: All functions require type annotations. Use `typing` module for generics. +- **Docstrings**: Google-style with Markdown formatting. Link references: `[ClassName.method][pact.module.ClassName.method]` +- **Formatting**: Use `ruff` for linting/formatting. +- **Naming**: Follow PEP 8. Use descriptive names that indicate purpose. +- **Error handling**: Use built-in exceptions (`ValueError`, `TypeError`) for standard errors. Custom exceptions inherit from `PactError`. +- **Validation**: Use early returns to reduce nesting. +- **Comments**: Explain design decisions, not what code does. No comments unless necessary. + +## Testing + +- **Framework**: `pytest` with files prefixed `test_`. Use `@pytest.mark.parametrize` for multiple scenarios. +- **Coverage**: Focus on critical paths, edge cases, error conditions. +- **Fixtures**: Use pytest fixtures for shared setup. Minimize mocking. + +## Project Structure + +- `src/pact/`: Main V3+ codebase (active development) +- `src/pact/v2/`: Legacy V2 code (maintenance only - no new features) +- `pact-python-ffi/`: Low-level FFI bindings (memory management, type conversion only) +- `pact-python-cli/`: CLI wrapper (bundled binaries with system fallback) From e9337bc5792307fdd447cd200e0769b4bb9d30e0 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 13 Oct 2025 16:06:23 +1100 Subject: [PATCH 1069/1376] chore: update non-compliant docstrings and types With the LLM instructions in place, this is an automated check (and manually reviewed) of the codebase. Signed-off-by: JP-Ellis --- conftest.py | 14 +++++++-- pact-python-cli/hatch_build.py | 17 ++++++----- pact-python-cli/src/pact_cli/__init__.py | 23 +++++++++++---- pact-python-cli/tests/test_init.py | 36 ++++++++++++++++-------- pact-python-ffi/hatch_build.py | 24 +++++++++------- pact-python-ffi/src/pact_ffi/__init__.py | 10 +++++-- tests/compatibility_suite/conftest.py | 27 ++++++++++++------ 7 files changed, 104 insertions(+), 47 deletions(-) diff --git a/conftest.py b/conftest.py index 371bd108d..4affbbdf3 100644 --- a/conftest.py +++ b/conftest.py @@ -12,7 +12,13 @@ def pytest_addoption(parser: pytest.Parser) -> None: - """Define additional command lines to customise the examples.""" + """ + Define additional command line options for the Pact examples. + + Args: + parser: + Parser used to register CLI options for the tests. + """ parser.addoption( "--broker-url", help=( @@ -30,7 +36,11 @@ def pytest_addoption(parser: pytest.Parser) -> None: def pytest_runtest_setup(item: pytest.Item) -> None: """ - Hook into the setup phase of tests. + Hook into the test setup phase to apply container markers. + + Args: + item: + Pytest item under execution, used to inspect markers and options. """ if "container" in item.keywords and not item.config.getoption("--container"): pytest.skip("need --container to run this test") diff --git a/pact-python-cli/hatch_build.py b/pact-python-cli/hatch_build.py index 3a7893589..ffdb35931 100644 --- a/pact-python-cli/hatch_build.py +++ b/pact-python-cli/hatch_build.py @@ -14,7 +14,10 @@ import urllib.request import zipfile from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Mapping, MutableMapping, Sequence from hatchling.builders.config import BuilderConfig from hatchling.builders.hooks.plugin.interface import BuildHookInterface @@ -72,7 +75,7 @@ def __del__(self) -> None: """ shutil.rmtree(self.tmpdir, ignore_errors=True) - def clean(self, versions: list[str]) -> None: # noqa: ARG002 + def clean(self, versions: Sequence[str]) -> None: # noqa: ARG002 """ Code called to clean. @@ -85,7 +88,7 @@ def clean(self, versions: list[str]) -> None: # noqa: ARG002 def initialize( self, version: str, # noqa: ARG002 - build_data: dict[str, object], + build_data: MutableMapping[str, object], ) -> None: """ Code called immediately before each build. @@ -98,8 +101,8 @@ def initialize( Not used (but required by the parent class). build_data: - A dictionary to modify in-place used by Hatch when creating the - final wheel. + A mutable mapping to modify in-place used by Hatch when creating + the final wheel. Raises: UnsupportedPlatformError: @@ -129,7 +132,7 @@ def _sys_tag_platform(self) -> str: """ return next(t.platform for t in sys_tags()) - def _install(self, version: str) -> dict[str, str]: + def _install(self, version: str) -> Mapping[str, str]: """ Install the Pact standalone binaries. @@ -234,7 +237,7 @@ def _download(self, url: str) -> Path: url: The URL to download - Return: + Returns: The path to the downloaded artefact. """ filename = url.split("/")[-1] diff --git a/pact-python-cli/src/pact_cli/__init__.py b/pact-python-cli/src/pact_cli/__init__.py index 9b38d9213..0e1184700 100644 --- a/pact-python-cli/src/pact_cli/__init__.py +++ b/pact-python-cli/src/pact_cli/__init__.py @@ -46,12 +46,15 @@ def _exec() -> None: """ - A minimal wrapper to execute the Pact CLI tools. + Execute Pact CLI tools routed through the generated entry points. - This is designed to be used as an entry point the scripts defined in the - `pyproject.toml` file. It simply passes the command line arguments to the - appropriate Pact CLI tool based on the executable name. + This function is exposed via `pyproject.toml` console scripts and forwards + the provided command-line arguments to the matching Pact CLI binary. + Raises: + SystemExit: + If the requested command is unknown or an executable cannot be + located. """ import sys # noqa: PLC0415 @@ -117,7 +120,17 @@ def _find_executable(executable: str) -> str | None: if _USE_SYSTEM_BINS: bin_path = shutil.which(executable) else: - bin_path = shutil.which(executable, path=_BIN_DIR) + bin_path = shutil.which(executable, path=str(_BIN_DIR)) + if bin_path is None: + system_path = shutil.which(executable) + if system_path is not None: + warnings.warn( + f"Bundled {executable} binary not found; " + "using system installation instead.", + RuntimeWarning, + stacklevel=2, + ) + bin_path = system_path if bin_path is None: msg = f"Unable to find {executable} binary executable." warnings.warn(msg, RuntimeWarning, stacklevel=2) diff --git a/pact-python-cli/tests/test_init.py b/pact-python-cli/tests/test_init.py index 2ac7da782..c22b40078 100644 --- a/pact-python-cli/tests/test_init.py +++ b/pact-python-cli/tests/test_init.py @@ -17,7 +17,14 @@ def bin_to_sitepackages(exec_path: str | Path) -> Path: """ - Find the expected site-packages directory for the given executable. + Compute the expected site-packages directory for a Pact executable. + + Args: + exec_path: + Path to the binary whose site-packages root should be derived. + + Returns: + Path to the site-packages directory associated with the executable. """ if os.name == "nt": return Path(exec_path).parents[1] / "Lib" / "site-packages" @@ -31,10 +38,14 @@ def bin_to_sitepackages(exec_path: str | Path) -> Path: def assert_in_sys_path(p: str | Path) -> None: """ - Assert that a given path is in sys.path. + Assert that a resolved path exists in ``sys.path``. + + This performs normalization on case-insensitive filesystems to avoid + comparison errors. - This performs some normalization on platform where the filesystem is - case-insensitive. + Args: + p: + Path that should be discoverable via `sys.path`. """ if os.name == "nt": assert str(p).lower() in (path.lower() for path in sys.path) @@ -58,8 +69,7 @@ def assert_in_sys_path(p: str | Path) -> None: pytest.param("PACTFLOW_PATH", "pactflow", id="pactflow"), ], ) -def test_constants(constant: str, expected: str) -> None: - """Test the values of constants in pact.constants.""" +def test_constants_are_valid_executable_paths(constant: str, expected: str) -> None: value: str = getattr(pact_cli, constant) if os.name == "nt": # As the Windows filesystem is case insensitive, we must normalize it. @@ -80,7 +90,7 @@ def test_constants(constant: str, expected: str) -> None: pytest.param("pactflow", id="pactflow"), ], ) -def test_exec_wrapper(executable: str) -> None: +def test_cli_exec_wrapper(executable: str) -> None: exec_path = shutil.which(executable) assert exec_path @@ -105,12 +115,14 @@ def test_exec_wrapper(executable: str) -> None: assert "pact" in (result.stdout + result.stderr).lower() -def test_exec_wrapper_mock_service() -> None: +def test_cli_exec_wrapper_for_mock_service() -> None: """ - Analogous to test_exec_wrapper, but specifically for pact-mock-service. + Same as `test_cli_exec_wrapper` for the `pact-mock-server`. - This is necessary because pact-mock-service is a long running service, so we - spawn the process, terminate it after a delay, and check the output. + The Pact mock service is a long running service, as it is expected to run a + mock service which can be tested against. The test pattern above doesn't + work, and instead, we spawn the process, wait a bit, terminate it, and then + check the output. """ executable = "pact-mock-service" exec_path = shutil.which(executable) @@ -148,7 +160,7 @@ def test_exec_wrapper_mock_service() -> None: ) def test_exec_directly(executable: str) -> None: """ - Test pact_cli._exec with --help, mocking sys.argv and capturing output. + Invoke ``pact_cli._exec`` directly to confirm ``execv`` receives the command. """ cmd: str args: list[str] diff --git a/pact-python-ffi/hatch_build.py b/pact-python-ffi/hatch_build.py index 3ada4312d..f730b2192 100644 --- a/pact-python-ffi/hatch_build.py +++ b/pact-python-ffi/hatch_build.py @@ -1,7 +1,7 @@ """ Hatchling build hook. -This hook is responsible for download the Pact FFI library and building the +This hook is responsible for downloading the Pact FFI library and building the CFFI bindings for it. """ @@ -15,12 +15,15 @@ import tempfile import urllib.request from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import cffi from hatchling.builders.hooks.plugin.interface import BuildHookInterface from packaging.tags import sys_tags +if TYPE_CHECKING: + from collections.abc import Mapping, MutableMapping, Sequence + PKG_DIR = Path(__file__).parent.resolve() / "src" / "pact_ffi" PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{platform}{suffix}.{ext}" @@ -56,7 +59,7 @@ class PactBuildHook(BuildHookInterface[Any]): PLUGIN_NAME = "pact-ffi" - def __init__(self, *args: object, **kwargs: object) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 """ Initialize the build hook. @@ -73,7 +76,7 @@ def __del__(self) -> None: """ shutil.rmtree(self.tmpdir, ignore_errors=True) - def clean(self, versions: list[str]) -> None: # noqa: ARG002 + def clean(self, versions: Sequence[str]) -> None: # noqa: ARG002 """ Code called to clean. @@ -93,7 +96,7 @@ def clean(self, versions: list[str]) -> None: # noqa: ARG002 def initialize( self, version: str, # noqa: ARG002 - build_data: dict[str, object], + build_data: MutableMapping[str, object], ) -> None: """ Code called immediately before each build. @@ -103,8 +106,8 @@ def initialize( Not used (but required by the parent class). build_data: - A dictionary to modify in-place used by Hatch when creating the - final wheel. + A mutable mapping to modify in-place used by Hatch when creating + the final wheel. Raises: UnsupportedPlatformError: @@ -122,6 +125,7 @@ def initialize( except UnsupportedPlatformError as err: msg = f"Pact FFI library is not available for {err.platform}" self.app.display_error(msg) + raise self.app.display_debug(f"Wheel artefacts: {build_data['force_include']}") build_data["tag"] = self._infer_tag() @@ -134,7 +138,7 @@ def _sys_tag_platform(self) -> str: """ return next(t.platform for t in sys_tags()) - def _install(self, version: str) -> dict[str, str]: + def _install(self, version: str) -> Mapping[str, str]: """ Install the Pact library binary. @@ -376,7 +380,7 @@ def _download(self, url: str) -> Path: url: The URL to download - Return: + Returns: The path to the downloaded artefact. """ filename = url.split("/")[-1] @@ -402,7 +406,7 @@ def _infer_tag(self) -> str: While the ABI3 interface was introduced in Python 3.2, we target the earliest supported version of Python in the Python wrapper. - Return: + Returns: The tag for the current build. """ python_version = f"{sys.version_info.major}{sys.version_info.minor}" diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index 794e6649d..55d2d4266 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -1909,7 +1909,7 @@ def version() -> str: def init(log_env_var: str) -> None: """ - Initialise the mock server library. + Initialise the Pact FFI mock server library. This can provide an environment variable name to use to set the log levels. This function should only be called once, as it tries to install a global @@ -1918,6 +1918,10 @@ def init(log_env_var: str) -> None: [Rust `pactffi_init`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_init) + Args: + log_env_var: + Name of the environment variable that controls Pact logging levels. + # Safety log_env_var must be a valid NULL terminated UTF-8 string. @@ -1927,7 +1931,7 @@ def init(log_env_var: str) -> None: def init_with_log_level(level: str = "INFO") -> None: """ - Initialises logging, and sets the log level explicitly. + Initialises logging and sets the Pact FFI log level explicitly. This function should only be called once, as it tries to install a global tracing subscriber. @@ -1977,7 +1981,7 @@ def log_message( Args: message: - The contents written to the log + The contents written to the log. log_level: The verbosity at which this message should be logged. diff --git a/tests/compatibility_suite/conftest.py b/tests/compatibility_suite/conftest.py index 26b910c3b..73b61d6c1 100644 --- a/tests/compatibility_suite/conftest.py +++ b/tests/compatibility_suite/conftest.py @@ -24,7 +24,9 @@ @pytest.fixture(scope="session", autouse=True) def _submodule_init() -> None: - """Initialize the submodule.""" + """ + Initialize the compatibility suite Git submodule if required. + """ # Locate the git execute submodule_dir = Path(__file__).parent / "definition" if submodule_dir.is_dir(): @@ -42,21 +44,30 @@ def _submodule_init() -> None: @pytest.fixture def verifier() -> Verifier: - """Return a new Verifier.""" + """ + Provide a Pact verifier instance scoped to a single test. + + Returns: + Configurable verifier for compatibility suite scenarios. + """ return Verifier("provider") @pytest.fixture(scope="session") def broker_url(request: pytest.FixtureRequest) -> Generator[URL, Any, None]: """ - Fixture to run the Pact broker. + Yield the Pact Broker URL, starting a container when required. + + If Pytest has been started with an explicit `--broker-url` option, then that + URL is returned by this fixture; otherwise, a Pact Broker container is + launched to run tests against it. - This inspects whether the `--broker-url` option has been given. If it has, - it is assumed that the broker is already running and simply returns the - given URL. + Args: + request: + Active pytest request object used to inspect command-line options. - Otherwise, the Pact broker is started in a container. The URL of the - containerised broker is then returned. + Yields: + Location of the Pact Broker to use for compatibility testing. """ broker_url: str | None = request.config.getoption("--broker-url") From 10e6e49d4750a40524f0d1f018481fa377aa7f1a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 04:59:04 +0000 Subject: [PATCH 1070/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.2.6 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d34b0d622..3ad427e63 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.2.5 + rev: v2.2.6 hooks: - id: biome-check From 709a6704f20b6ed40e18961c62200b3535534261 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 08:58:06 +1100 Subject: [PATCH 1071/1376] chore(deps): update ruff to v0.14.1 (#1296) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ad427e63..e41de17ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.0 + rev: v0.14.1 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index f5be3295c..7b23639f6 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.0", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.1", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=8.0"] types = ["mypy==1.18.2"] From 1bc1a1f94ae822bd8d30e1b9a83357e60541fc74 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 08:58:18 +1100 Subject: [PATCH 1072/1376] chore(deps): update pre-commit hook google/yamlfmt to v0.18.1 (#1295) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e41de17ab..e4126e9f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/google/yamlfmt - rev: v0.17.2 + rev: v0.18.1 hooks: - id: yamlfmt From ded0c208c6a271ad3a5a269373318af783986d90 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 17 Oct 2025 09:36:30 +1100 Subject: [PATCH 1073/1376] docs: update configuration Signed-off-by: JP-Ellis --- mkdocs.yml | 28 ++++++++++++++++++---------- pyproject.toml | 22 +++++++++++++--------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 724b8f617..03a8c7069 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,33 +37,39 @@ plugins: options: # General allow_inspection: true + extensions: + - dataclasses + - griffe_generics + # - griffe_inherited_method_crossrefs # Waiting on upstream fix + - griffe_pydantic + - griffe_warnings_deprecated + show_inheritance_diagram: true show_source: true - show_bases: true # Headings heading_level: 2 - show_root_heading: false - show_root_toc_entry: true - show_root_full_path: true - show_root_members_full_path: false - show_object_full_path: false show_category_heading: true + show_symbol_type_heading: true + show_symbol_type_toc: true # Members filters: - '!^_' - '!^__' - group_by_category: true - show_submodules: false # Docstrings docstring_style: google docstring_options: ignore_init_summary: true docstring_section_style: spacy merge_init_into_class: true + relative_crossrefs: true + scoped_crossrefs: true show_if_no_docstring: true # Signature annotations_path: brief - show_signature: true + modernize_annotations: true + overloads_only: true show_signature_annotations: true + show_signature_type_parameters: true + signature_crossrefs: true - llmstxt: full_output: llms-full.txt sections: @@ -120,7 +126,7 @@ markdown_extensions: custom_checkbox: true - pymdownx.tilde -copyright: Copyright © 2023 Pact Foundation +copyright: Copyright © 2025 Pact Foundation theme: name: material @@ -133,7 +139,9 @@ theme: - content.action.view - content.code.annotate - content.code.copy + - content.code.select - content.tooltips + - content.tabs.link - navigation.indexes - navigation.instant - navigation.instant.progress diff --git a/pyproject.toml b/pyproject.toml index ffc7b934f..25503133b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,15 +82,19 @@ dev = [ ] docs = [ - "mkdocs-github-admonitions-plugin~=0.0", - "mkdocs-literate-nav~=0.6", - "mkdocs-llmstxt~=0.3", - "mkdocs-material[recommended,git,imaging]~=9.0", - "mkdocs-section-index~=0.3", - "mkdocs_gen_files~=0.5", - "mkdocstrings[python]~=0.23", - "mkdocs~=1.5", - "pathspec~=0.0", + "griffe-generics==1.0.13", + "griffe-inherited-method-crossrefs==0.0.1.4", + "griffe-pydantic==1.1.7", + "griffe-warnings-deprecated==1.1.0", + "mkdocs-gen-files==0.5.0", + "mkdocs-github-admonitions-plugin==0.1.1", + "mkdocs-literate-nav==0.6.2", + "mkdocs-llmstxt==0.4.0", + "mkdocs-material[recommended,git,imaging]==9.6.21", + "mkdocs-section-index==0.3.10", + "mkdocs==1.6.1", + "mkdocstrings[python]==0.30.1", + "pathspec==0.12.1", ] example = [ "fastapi~=0.0", From b8589addca9503d9c6be2a3363e9e72587fd5d6e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 20:35:14 +1100 Subject: [PATCH 1074/1376] chore(deps): update pre-commit hook google/yamlfmt to v0.19.0 (#1298) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4126e9f4..2275e35d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/google/yamlfmt - rev: v0.18.1 + rev: v0.19.0 hooks: - id: yamlfmt From 94872fe8aad92ac4ee31236e67b7f892b3e3f729 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 09:31:27 +1100 Subject: [PATCH 1075/1376] chore(deps): update astral-sh/setup-uv action to v7.1.1 (#1299) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index c8af60dd5..87908c4d2 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 + uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index d67aa2f97..6ae09dcdd 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 + uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1b9ecdd9..5b14f56d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 + uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ef4feb759..013ef2331 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 + uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a193c8a08..20b57b02a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 + uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 with: enable-cache: true @@ -154,7 +154,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 + uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 with: enable-cache: true @@ -197,7 +197,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 + uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 with: enable-cache: true @@ -229,7 +229,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 + uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 with: enable-cache: true @@ -262,7 +262,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 + uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 with: enable-cache: true @@ -302,7 +302,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 + uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 with: enable-cache: true cache-suffix: prek From b531a5f497d18a3af62cda1010708d180600017a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 01:09:23 +0000 Subject: [PATCH 1076/1376] chore(deps): update taiki-e/install-action action to v2.62.33 (#1300) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 87908c4d2..5329ab52a 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@e7ef886cf8f69c25ecef6bbc2858a42e273496ec # v2.62.28 + uses: taiki-e/install-action@e43a5023a747770bfcb71ae048541a681714b951 # v2.62.33 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 6ae09dcdd..9fd091158 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@e7ef886cf8f69c25ecef6bbc2858a42e273496ec # v2.62.28 + uses: taiki-e/install-action@e43a5023a747770bfcb71ae048541a681714b951 # v2.62.33 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b14f56d8..6e67ace5f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@e7ef886cf8f69c25ecef6bbc2858a42e273496ec # v2.62.28 + uses: taiki-e/install-action@e43a5023a747770bfcb71ae048541a681714b951 # v2.62.33 with: tool: git-cliff,typos From d3a1ef132260551dff487404d526a8723ff87dc3 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 22:01:43 +0000 Subject: [PATCH 1077/1376] docs: add logging documentation The FFI introduces additional complexities when it comes to logging, and as a result, how to enable and configure logging needs its own dedicated section. Fixes: #1302 --- docs/SUMMARY.md | 1 + docs/consumer.md | 18 +++++++ docs/logging.md | 131 +++++++++++++++++++++++++++++++++++++++++++++++ docs/provider.md | 18 +++++++ 4 files changed, 168 insertions(+) create mode 100644 docs/logging.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 99021f73d..61202609a 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -3,6 +3,7 @@ - [Home](README.md) - [Consumer](consumer.md) - [Provider](provider.md) + - [Logging](logging.md) - [Releases](releases.md) - [Migration Guide](MIGRATION.md) - [Changelog](CHANGELOG.md) diff --git a/docs/consumer.md b/docs/consumer.md index c42967119..637e1cb6f 100644 --- a/docs/consumer.md +++ b/docs/consumer.md @@ -186,6 +186,24 @@ The mock service can handle multiple interactions within a single test. This is When the mock service is started with `pact.serve()`, it will handle requests for all defined interactions, ensuring the client code can be tested against a realistic sequence of operations. Furthermore, for the test to pass, all defined interactions must be exercised by the client code. If any interaction is not used, the test will fail. +## Logging + +To enable logging for debugging and troubleshooting, configure the FFI (Foreign Function Interface) logging using [`pact_ffi.log_to_stderr`][pact_ffi.log_to_stderr]. This is particularly useful when you need to understand what's happening inside the mock service or diagnose issues with your tests. + +The recommended approach is to set up logging in a pytest fixture within your `conftest.py`: + +```python +import pytest +import pact_ffi + +@pytest.fixture(autouse=True, scope="session") +def pact_logging(): + """Configure Pact FFI logging for the test session.""" + pact_ffi.log_to_stderr("INFO") +``` + +For more information on logging configuration, including advanced options and troubleshooting, see the [Logging Configuration](logging.md) page. + ## Mock Service Pact provides a mock service that simulates the provider service based on the defined interactions. The mock service is started when the `pact` object is used as a context manager with `pact.serve()`, as shown in the [consumer test](#consumer-test) example above. diff --git a/docs/logging.md b/docs/logging.md new file mode 100644 index 000000000..3bbb5126f --- /dev/null +++ b/docs/logging.md @@ -0,0 +1,131 @@ +# Logging Configuration + +Pact Python uses the Rust FFI (Foreign Function Interface) library for its core functionality. While the Python code uses the standard library `logging` module, the underlying FFI cannot interface with that directly. This page explains how to configure FFI logging for debugging and troubleshooting. + +## Basic Configuration + +The simplest way to configure FFI logging is to use the [`log_to_stderr`][pact_ffi.log_to_stderr] function from the `pact_ffi` module. This directs all FFI log output to standard error. + +```python +import pact_ffi + +pact_ffi.log_to_stderr("INFO") +``` + +### Log Levels + +The following log levels are available (from least to most verbose): + +- `"OFF"` - Disable all logging +- `"ERROR"` - Only error messages +- `"WARN"` - Warnings and errors +- `"INFO"` - Informational messages, warnings, and errors +- `"DEBUG"` - Debug messages and above +- `"TRACE"` - All messages including trace-level details + +## Recommended Setup with Pytest + + +!!! warning "One-time Initialization" + + The FFI logging can only be initialized **once** per process. Attempting to configure it multiple times will result in an error. For this reason, it's recommended to set up logging in a session-scoped fixture. + + +The recommended way to configure FFI logging in your test suite is to use a pytest fixture with `autouse=True` and `scope="session"` in your `conftest.py` file: + +```python +import pytest +import pact_ffi + +@pytest.fixture(autouse=True, scope="session") +def pact_logging(): + """Configure Pact FFI logging for the test session.""" + pact_ffi.log_to_stderr("INFO") +``` + +This ensures that: + +1. Logging is configured automatically for all tests +2. It's only initialized once at the start of the test session +3. All test output includes relevant Pact FFI logs + +## Advanced Configuration + +For more advanced use cases, the `pact_ffi` module provides additional logging functions: + +### Logging to a File + + +!!! note "Not Yet Implemented" + + The `log_to_file` function is currently not implemented in the Python bindings. If you need this feature, please open an issue on the [Pact Python GitHub repository](https://github.com/pact-foundation/pact-python/issues). + + +To direct logs to a file instead of stderr, you would use: + +```python +import pact_ffi + +# This will be available in a future release +pact_ffi.log_to_file("/path/to/logfile.log", pact_ffi.LevelFilter.DEBUG) +``` + +### Logging to a Buffer + +For applications that need to capture and process logs programmatically, you can use [`log_to_buffer`][pact_ffi.log_to_buffer]: + +```python +import pact_ffi + +# Configure logging to an internal buffer +pact_ffi.log_to_buffer("DEBUG") +``` + +This is particularly useful for: + +- Capturing logs in CI/CD environments +- Including logs in test failure reports +- Processing or filtering log messages programmatically + + +!!! note "Retrieving Buffer Contents" + + The `fetch_log_buffer` function for retrieving buffered logs is currently not implemented in the Python bindings. If you need this feature, please open an issue on the [Pact Python GitHub repository](https://github.com/pact-foundation/pact-python/issues). + + +### Multiple Sinks + + +!!! note "Advanced Usage" + + The functions `logger_init`, `logger_attach_sink`, and `logger_apply` are currently not implemented in the Python bindings. If you need these features, please open an issue on the [Pact Python GitHub repository](https://github.com/pact-foundation/pact-python/issues). + + +For the most advanced scenarios, the FFI supports configuring multiple log sinks simultaneously (e.g., logging to both stderr and a file). This requires using the lower-level `logger_init`, `logger_attach_sink`, and `logger_apply` functions, which are planned for future implementation. + +## Troubleshooting + +### "Logger already initialized" Error + +If you see an error about the logger already being initialized, it means you're trying to configure FFI logging more than once. Ensure that: + +1. You're using a session-scoped fixture as shown above +2. You're not calling any of the `log_to_*` functions multiple times in your code +3. If running tests multiple times in the same process (e.g., with pytest-xdist), the fixture scope is set correctly + +### No Log Output + +If you're not seeing any log output: + +1. Check that the log level is appropriate - `"ERROR"` will only show errors, while `"INFO"` or `"DEBUG"` will show more information +2. Verify that the logging is configured before any Pact operations are performed +3. For `log_to_file`, ensure the file path is writable and the directory exists + +## Further Information + +For complete API documentation, see: + +- [`pact_ffi.log_to_stderr`][pact_ffi.log_to_stderr] +- [`pact_ffi.log_to_file`][pact_ffi.log_to_file] +- [`pact_ffi.log_to_buffer`][pact_ffi.log_to_buffer] +- [`pact_ffi.LevelFilter`][pact_ffi.LevelFilter] diff --git a/docs/provider.md b/docs/provider.md index 6344331dc..575e40feb 100644 --- a/docs/provider.md +++ b/docs/provider.md @@ -108,6 +108,24 @@ def test_provider_with_selectors(): More information on the selector options is available in the [API reference][pact.verifier.BrokerSelectorBuilder]. +## Logging + +To enable logging for debugging and troubleshooting, configure the FFI (Foreign Function Interface) logging using [`pact_ffi.log_to_stderr`][pact_ffi.log_to_stderr]. This is particularly useful when you need to understand what's happening during provider verification or diagnose issues with provider state handlers. + +The recommended approach is to set up logging in a pytest fixture within your `conftest.py`: + +```python +import pytest +import pact_ffi + +@pytest.fixture(autouse=True, scope="session") +def pact_logging(): + """Configure Pact FFI logging for the test session.""" + pact_ffi.log_to_stderr("INFO") +``` + +For more information on logging configuration, including advanced options and troubleshooting, see the [Logging Configuration](logging.md) page. + ### Publishing Results To publish verification results to the Broker: From d19bcdb9f9e632d231a999906dcceaeae7e69392 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 22:11:19 +0000 Subject: [PATCH 1078/1376] chore(deps): update pre-commit hook google/yamlfmt to v0.20.0 (#1301) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2275e35d1..6d72ae9ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/google/yamlfmt - rev: v0.19.0 + rev: v0.20.0 hooks: - id: yamlfmt From 5821f4e1d2b1ef957f20b90a5f6f1788acf5fcf9 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 23 Oct 2025 10:18:28 +1100 Subject: [PATCH 1079/1376] chore: upgrade pymdownx extensions use the newer `code.*` extensions as they are much more compatible with various other Markdown linters/parsers/syntax highlighters. Signed-off-by: JP-Ellis --- MIGRATION.md | 206 ++++---- ... sneak peek into the pact python future.md | 7 +- .../07-26 asynchronous message support.md | 7 +- .../posts/2024/12-30 functional arguments.md | 474 +++++++++--------- docs/consumer.md | 15 +- docs/logging.md | 32 +- docs/provider.md | 6 +- examples/README.md | 12 +- examples/plugins/proto/person_pb2.py | 6 +- examples/plugins/proto/person_pb2.pyi | 6 +- examples/plugins/proto/person_pb2_grpc.py | 6 +- mkdocs.yml | 11 +- pact-python-ffi/src/pact_ffi/__init__.py | 22 +- src/pact/generate/__init__.py | 25 +- .../interaction/_async_message_interaction.py | 4 - .../interaction/_sync_message_interaction.py | 4 - src/pact/match/__init__.py | 33 +- src/pact/verifier.py | 12 +- tests/compatibility_suite/util/__init__.py | 16 +- 19 files changed, 446 insertions(+), 458 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index c5186d8c3..06ff1ea7b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -242,139 +242,139 @@ verifier = ( Support for both local files and Pact Brokers is retained in v3, with the `verify_pacts` and `verify_with_broker` methods replaced by a more flexible source configuration. This allows multiple sources to be combined, and selectors to be applied. - +/// tab | Local Files -=== "Local Files" +```python title="v2" +success, logs = verifier.verify_pacts( + './pacts/consumer1-provider.json', + './pacts/consumer2-provider.json' +) +``` - ```python title="v2" - success, logs = verifier.verify_pacts( - './pacts/consumer1-provider.json', - './pacts/consumer2-provider.json' - ) - ``` +```python title="v3" +verifier = ( + Verifier('my-provider') + # It can discover all Pact files in a directory + .add_source('./pacts/') + # Or read individual files + .add_source('./pacts/specific-consumer.json') +) +``` - ```python title="v3" - verifier = ( - Verifier('my-provider') - # It can discover all Pact files in a directory - .add_source('./pacts/') - # Or read individual files - .add_source('./pacts/specific-consumer.json') - ) - ``` +/// -=== "Pact Broker" +/// tab | Pact Broker - ```python title="v2" - success, logs = verifier.verify_with_broker( - broker_url='https://pact-broker.example.com', - broker_username='username', - broker_password='password' - ) - ``` +```python title="v2" +success, logs = verifier.verify_with_broker( + broker_url='https://pact-broker.example.com', + broker_username='username', + broker_password='password' +) +``` - ```python title="v3" - verifier = ( - Verifier('my-provider') - .broker_source( - 'https://pact-broker.example.com', - username='username', - password='password' - ) +```python title="v3" +verifier = ( + Verifier('my-provider') + .broker_source( + 'https://pact-broker.example.com', + username='username', + password='password' ) +) - # Or with selectors for more control - broker_builder = ( - verifier - .broker_source( - 'https://pact-broker.example.com', - selector=True - ) - .include_pending() - .provider_branch('main') - .consumer_tags('main', 'develop') - .build() +# Or with selectors for more control +broker_builder = ( + verifier + .broker_source( + 'https://pact-broker.example.com', + selector=True ) - ``` + .include_pending() + .provider_branch('main') + .consumer_tags('main', 'develop') + .build() +) +``` - The `selector=True` argument returns a [`BrokerSelectorBuilder`][pact.verifier.BrokerSelectorBuilder] instance, which provides methods to configure which pacts to fetch. The `build()` call finalizes the configuration and returns the `Verifier` instance which can then be further configured. +The `selector=True` argument returns a [`BrokerSelectorBuilder`][pact.verifier.BrokerSelectorBuilder] instance, which provides methods to configure which pacts to fetch. The `build()` call finalizes the configuration and returns the `Verifier` instance which can then be further configured. - +/// #### Provider State Handling The old v2 API required the provider to expose an HTTP endpoint dedicated to handling provider states. This is still supported in v3, but there are now more flexible options, allowing Python functions (or mappings of state names to functions) to be used instead. - +/// tab | URL-based State Handling -=== "URL-based State Handling" +```python title="v2" +success, logs = verifier.verify_pacts( + './pacts/consumer-provider.json', + provider_states_setup_url='http://localhost:8080/_pact/provider_states' +) +``` - ```python title="v2" - success, logs = verifier.verify_pacts( - './pacts/consumer-provider.json', - provider_states_setup_url='http://localhost:8080/_pact/provider_states' +```python title="v3" +# Option 1: URL-based (similar to v2) +verifier = ( + Verifier('my-provider') + .add_transport(url='http://localhost:8080') + .state_handler( + 'http://localhost:8080/_pact/provider_states', + body=True # (1) ) - ``` + .add_source('./pacts/') +) +``` - ```python title="v3" - # Option 1: URL-based (similar to v2) - verifier = ( - Verifier('my-provider') - .add_transport(url='http://localhost:8080') - .state_handler( - 'http://localhost:8080/_pact/provider_states', - body=True # (1) - ) - .add_source('./pacts/') - ) - ``` - - 1. The `body` argument specifies whether to use a `POST` request and pass information in the body, or to use a `GET` request and pass information through HTTP headers. For more details, see the [`state_handler` API documentation][pact.verifier.Verifier.state_handler]. - -=== "Functional State Handling" - - ```python title="v2" - # Not supported - ``` - - ```python title="v3 - Function" - def handler(name, params=None): - if name == 'user exists': - # Set up user in database/mock - create_user(params.get('id', 123)) - elif name == 'no users exist': - # Clear users - clear_users() - - verifier = ( - Verifier('my-provider') - .add_transport(url='http://localhost:8080') - .state_handler(handler) - .add_source('./pacts/') - ) - ``` - - ```python title="v3 - Mapping" - state_handlers = { - 'user exists': lambda name, params: create_user(params.get('id', 123)), - 'no users exist': lambda name, params: clear_users(), - } +1. The `body` argument specifies whether to use a `POST` request and pass information in the body, or to use a `GET` request and pass information through HTTP headers. For more details, see the [`state_handler` API documentation][pact.verifier.Verifier.state_handler]. + +/// + +//// tab | Functional State Handling + +```python title="v2" +# Not supported +``` + +```python title="v3 - Function" +def handler(name, params=None): + if name == 'user exists': + # Set up user in database/mock + create_user(params.get('id', 123)) + elif name == 'no users exist': + # Clear users + clear_users() verifier = ( Verifier('my-provider') .add_transport(url='http://localhost:8080') - .state_handler(state_handlers) + .state_handler(handler) .add_source('./pacts/') ) - ``` +``` + +```python title="v3 - Mapping" +state_handlers = { + 'user exists': lambda name, params: create_user(params.get('id', 123)), + 'no users exist': lambda name, params: clear_users(), +} - More information on the state handler function signature can be found in the [`state_handler` API documentation][pact.verifier.Verifier.state_handler]. By default, the handlers only _set up_ the provider state. If you need to also _tear down_ the state after verification, you can use the `teardown=True` argument to enable this behaviour. +verifier = ( + Verifier('my-provider') + .add_transport(url='http://localhost:8080') + .state_handler(state_handlers) + .add_source('./pacts/') +) +``` - !!! warning +More information on the state handler function signature can be found in the [`state_handler` API documentation][pact.verifier.Verifier.state_handler]. By default, the handlers only _set up_ the provider state. If you need to also _tear down_ the state after verification, you can use the `teardown=True` argument to enable this behaviour. - These functions run in the test process, so any side effects must be properly shared with the provider. If using mocking libraries, ensure the provider is started in a separate thread of the same process (using `threading.Thread` or similar), rather than a separate process (e.g., using `multiprocessing.Process` or `subprocess.Popen`). +/// warning +These functions run in the test process, so any side effects must be properly shared with the provider. If using mocking libraries, ensure the provider is started in a separate thread of the same process (using `threading.Thread` or similar), rather than a separate process (e.g., using `multiprocessing.Process` or `subprocess.Popen`). +/// - +//// #### Message Verification diff --git a/docs/blog/posts/2024/04-11 a sneak peek into the pact python future.md b/docs/blog/posts/2024/04-11 a sneak peek into the pact python future.md index ec964127a..72bf1e83d 100644 --- a/docs/blog/posts/2024/04-11 a sneak peek into the pact python future.md +++ b/docs/blog/posts/2024/04-11 a sneak peek into the pact python future.md @@ -69,7 +69,8 @@ Happy testing! ---- - +/// define 1 August 2025 -: With the release of Pact Python `v3`, some hyperlinks have been removed from this blog post as they are no longer relevant. - + +- With the release of Pact Python `v3`, some hyperlinks have been removed from this blog post as they are no longer relevant. +/// diff --git a/docs/blog/posts/2024/07-26 asynchronous message support.md b/docs/blog/posts/2024/07-26 asynchronous message support.md index 321483d25..2214841c6 100644 --- a/docs/blog/posts/2024/07-26 asynchronous message support.md +++ b/docs/blog/posts/2024/07-26 asynchronous message support.md @@ -186,7 +186,8 @@ At present, it is the responsibility of the end user to set up the provider endp --- - +/// define 1 August 2025 -: With the release of Pact Python `v3` and the splitting of the CLI and FFI into standalone packages, some hyperlinks and code snippets have been updated to point to the new locations. The _text_ has been kept unchanged to preserve the original context and intent of the post. - + +- With the release of Pact Python `v3` and the splitting of the CLI and FFI into standalone packages, some hyperlinks and code snippets have been updated to point to the new locations. The _text_ has been kept unchanged to preserve the original context and intent of the post. +/// diff --git a/docs/blog/posts/2024/12-30 functional arguments.md b/docs/blog/posts/2024/12-30 functional arguments.md index 2a1925740..8e8cc439e 100644 --- a/docs/blog/posts/2024/12-30 functional arguments.md +++ b/docs/blog/posts/2024/12-30 functional arguments.md @@ -18,62 +18,68 @@ While I highly recommend everyone experiment with the new possibilities that fun - The `Verifier` initialization now requires a `name` argument which is used to identify the provider in the Pact file. This information was previously given through the `set_info` method which has been removed. The change required is: - - === "Before" +/// tab | Before - ```python - verifier = Verifier() - verifier.set_info("provider_name", ...) - ``` +```python +verifier = Verifier() +verifier.set_info("provider_name", ...) +``` - === "After" +/// - ```python - verifier = Verifier(name="provider_name") - ``` - +/// tab | After + +```python +verifier = Verifier(name="provider_name") +``` + +/// - The `Verifier.set_info` method has been entirely removed. Instead, the `Verifier` class now has a `name` attribute which is set during initialization for the provider's name, and the transport information that was previously set is now passed through the `add_transport` method: - - === "Before" +/// tab | Before - ```python - verifier = Verifier() - verifier.set_info( - "provider_name", - url="http://localhost:8123", - ) - ``` +```python +verifier = Verifier() +verifier.set_info( + "provider_name", + url="http://localhost:8123", +) +``` - === "After" +/// - ```python - verifier = Verifier("provider_name") - verifier.add_transport(url="http://localhost:8123") - ``` - +/// tab | After + +```python +verifier = Verifier("provider_name") +verifier.add_transport(url="http://localhost:8123") +``` + +/// - The `Verifier.set_state` function has been renamed to `Verifier.state_handler`. Furthermore, if you have already set up a custom endpoint to handle provider state changes, you will now need to explicitly state whether your endpoint expects data to be passed through the query string or through a `POST` body: - - === "Before" +/// tab | Before - ```python - verifier = Verifier() - verifier.set_state("http://localhost:8123/provider-states") - ``` +```python +verifier = Verifier() +verifier.set_state("http://localhost:8123/provider-states") +``` - === "After" +/// - ```python - verifier = Verifier() - verifier.state_handler( - "http://localhost:8123/provider-states", - body=False, # the previous default must be explicitly set - ) - ``` - +/// tab | After + +```python +verifier = Verifier() +verifier.state_handler( + "http://localhost:8123/provider-states", + body=False, # the previous default must be explicitly set +) +``` + +/// ## Functional State Handler @@ -81,131 +87,132 @@ When a Pact interaction is to be verified, the consumer will often expect the pr The new `state_handler` method replaces the `set_state` method and simplifies this process significantly by allowing functions to be called to set up and tear down the provider state. For example, the following code snippet demonstrates how to set up a state handler that uses a custom endpoint to handle the provider state: - -???+ example +/// details | Example + +```python +from pact import Verifier + +def provider_state_callback( + name: str, # (1) + action: Literal["setup", "teardown"], # (2) + parameters: dict[str, Any] | None, # (3) +) -> None: + """ + Callback to set up and tear down the provider state. + + Args: + name: + The name of the provider state. For example, `"a user with ID 123 + exists"` or `"no users exist"`. + + action: + The action to perform. Either `"setup"` or `"teardown"`. The setup + action should create the provider state, and the teardown action + should remove it. + + parameters: + If the provider state has additional parameters, they will be + passed here. For example, instead of `"a user with ID 123 exists"`, + the provider state might be `"a user with the given ID exists"` and + the specific ID would be passed in the params. + """ + ... + +def test_provider(): + verifier = Verifier("provider_name") + verifier.state_handler(provider_state_callback, teardown=True) +``` + +1. The `name` parameter is the name of the provider state. For example, `"a user with ID 123 exists"` or `"no users exist"`. If you instead use a mapping of provider state names to functions, this parameter is not passed to the function. +2. The `action` parameter is either `"setup"` or `"teardown"`. The setup action should create the provider state, and the teardown action should remove it. If you specify `teardown=False`, then the `action` parameter is _not_ passed to the callback function. +3. The `parameters` parameter is a dictionary of additional parameters that the provider state requires. For example, instead of `"a user with ID 123 exists"`, the provider state might be `"a user with the given ID exists"` and the specific ID would be passed in the `parameters` dictionary. Note that `parameters` is always present, but may be `None` if no parameters are specified by the consumer. + +/// + +The function arguments must include the relevant keys from the [`StateHandlerArgs`][pact.types.StateHandlerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments. + +This snippet showcases a way to set up the provider state with a function that is fully parameterized. The `state_handler` method also handles the following scenarios: + +- If teardowns are never required, then one should specify `teardown=False` in which case the `action` parameter can be omitted from the signature of the callback function. This is useful when the provider state does not require any cleanup after the test has run. + + /// details | Example ```python from pact import Verifier def provider_state_callback( - name: str, # (1) - action: Literal["setup", "teardown"], # (2) - parameters: dict[str, Any] | None, # (3) + name: str, + parameters: dict[str, Any] | None, ) -> None: - """ - Callback to set up and tear down the provider state. - - Args: - name: - The name of the provider state. For example, `"a user with ID 123 - exists"` or `"no users exist"`. - - action: - The action to perform. Either `"setup"` or `"teardown"`. The setup - action should create the provider state, and the teardown action - should remove it. - - parameters: - If the provider state has additional parameters, they will be - passed here. For example, instead of `"a user with ID 123 exists"`, - the provider state might be `"a user with the given ID exists"` and - the specific ID would be passed in the params. - """ ... def test_provider(): verifier = Verifier("provider_name") - verifier.state_handler(provider_state_callback, teardown=True) + verifier.state_handler(provider_state_callback, teardown=False) ``` - 1. The `name` parameter is the name of the provider state. For example, `"a user with ID 123 exists"` or `"no users exist"`. If you instead use a mapping of provider state names to functions, this parameter is not passed to the function. - 2. The `action` parameter is either `"setup"` or `"teardown"`. The setup action should create the provider state, and the teardown action should remove it. If you specify `teardown=False`, then the `action` parameter is _not_ passed to the callback function. - 3. The `parameters` parameter is a dictionary of additional parameters that the provider state requires. For example, instead of `"a user with ID 123 exists"`, the provider state might be `"a user with the given ID exists"` and the specific ID would be passed in the `parameters` dictionary. Note that `parameters` is always present, but may be `None` if no parameters are specified by the consumer. - + /// -The function arguments must include the relevant keys from the [`StateHandlerArgs`][pact.types.StateHandlerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments. +- A mapping can be provided to the `state_handler` method with keys as the provider state names and values as the function to call. This can help to keep the code organized and to avoid a large number of `if` statements in the callback function. -This snippet showcases a way to set up the provider state with a function that is fully parameterized. The `state_handler` method also handles the following scenarios: + /// details | Example -- If teardowns are never required, then one should specify `teardown=False` in which case the `action` parameter can be omitted from the signature of the callback function. This is useful when the provider state does not require any cleanup after the test has run. + ```python + from pact import Verifier - - ??? example + def user_state_callback( + action: Literal["setup", "teardown"], + parameters: dict[str, Any] | None, + ) -> None: + ... - ```python - from pact import Verifier + def no_users_state_callback( + action: Literal["setup", "teardown"], + parameters: dict[str, Any] | None, + ) -> None: + ... - def provider_state_callback( - name: str, - parameters: dict[str, Any] | None, - ) -> None: - ... + def test_provider(): + verifier = Verifier("provider_name") + verifier.state_handler( + { + "a user with ID 123 exists": user_state_callback, + "no users exist": no_users_state_callback, + }, + ) + ``` - def test_provider(): - verifier = Verifier("provider_name") - verifier.state_handler(provider_state_callback, teardown=False) - ``` - + /// -- A mapping can be provided to the `state_handler` method with keys as the provider state names and values as the function to call. This can help to keep the code organized and to avoid a large number of `if` statements in the callback function. +- Both scenarios can be combined, in which a mapping of provide state names to functions is provided, and the `teardown=False` option is specified. In this case, the function should expect only one argument: the `parameters` dictionary (which itself may be `None`). - - ??? example - - ```python - from pact import Verifier - - def user_state_callback( - action: Literal["setup", "teardown"], - parameters: dict[str, Any] | None, - ) -> None: - ... - - def no_users_state_callback( - action: Literal["setup", "teardown"], - parameters: dict[str, Any] | None, - ) -> None: - ... - - def test_provider(): - verifier = Verifier("provider_name") - verifier.state_handler( - { - "a user with ID 123 exists": user_state_callback, - "no users exist": no_users_state_callback, - }, - ) - ``` - + /// details | Example -- Both scenarios can be combined, in which a mapping of provide state names to functions is provided, and the `teardown=False` option is specified. In this case, the function should expect only one argument: the `parameters` dictionary (which itself may be `None`). + ```python + from pact import Verifier + + def user_state_callback( + parameters: dict[str, Any] | None, + ) -> None: + ... - - ??? example - - ```python - from pact import Verifier - - def user_state_callback( - parameters: dict[str, Any] | None, - ) -> None: - ... - - def no_users_state_callback( - parameters: dict[str, Any] | None, - ) -> None: - ... - - def test_provider(): - verifier = Verifier("provider_name") - verifier.state_handler( - { - "a user with ID 123 exists": user_state_callback, - "no users exist": no_users_state_callback, - }, - teardown=False, - ) - ``` + def no_users_state_callback( + parameters: dict[str, Any] | None, + ) -> None: + ... + + def test_provider(): + verifier = Verifier("provider_name") + verifier.state_handler( + { + "a user with ID 123 exists": user_state_callback, + "no users exist": no_users_state_callback, + }, + teardown=False, + ) + ``` + + /// ## Functional Message Producer @@ -213,120 +220,121 @@ In the messaging paradigm, the Pact consumer consumes the message produced by th With the update to 2.3.0, the `Verifier` class has a new `message_handler` method which allows the provider to pass a function that generates the message. This function is called by the `Verifier` object when it needs a message to verify. The following code snippet demonstrates how to set up a message producer that uses a custom endpoint to generate the message: - -???+ example +/// details | Example - ```python - from pact import Verifier - from pact.types import Message +```python +from pact import Verifier +from pact.types import Message - def message_producer_callback( - name: str, # (1) - metadata: dict[str, Any] | None, # (2) - ) -> Message: - """ - Callback to produce the message that the consumer expects. +def message_producer_callback( + name: str, # (1) + metadata: dict[str, Any] | None, # (2) +) -> Message: + """ + Callback to produce the message that the consumer expects. - Args: - name: - The name of the message. For example `"request to delete a user"`. + Args: + name: + The name of the message. For example `"request to delete a user"`. - metadata: - Metadata that is passed along with the message. This could include information about the queue name, message type, creation timestamp, etc. + metadata: + Metadata that is passed along with the message. This could include information about the queue name, message type, creation timestamp, etc. - Returns: - The message that the consumer expects. - """ - ... + Returns: + The message that the consumer expects. + """ + ... - def test_provider(): - verifier = Verifier("provider_name") - verifier.message_handler(message_producer_callback) - ``` +def test_provider(): + verifier = Verifier("provider_name") + verifier.message_handler(message_producer_callback) +``` - 1. The `name` parameter is the name of the message. For example, `"request to delete a user"`. If you instead use a mapping of message names to functions, this parameter is not passed to the function. - 2. The `params` parameter is a dictionary of additional parameters that the message requires. For example, one could specify the user ID to delete in the parameters instead of the message. Note that `params` is always present, but may be `None` if no parameters are specified by the consumer. - -The function arguments must include the relevant keys from the [`MessageProducerArgs`][pact.types.MessageProducerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments. +1. The `name` parameter is the name of the message. For example, `"request to delete a user"`. If you instead use a mapping of message names to functions, this parameter is not passed to the function. +2. The `params` parameter is a dictionary of additional parameters that the message requires. For example, one could specify the user ID to delete in the parameters instead of the message. Note that `params` is always present, but may be `None` if no parameters are specified by the consumer. -The output of the callback function should be an instance of the `Message` type. This is a simple [TypedDict][typing.TypedDict] that represents the message that the consumer expects and can be specified as a simple dictionary, or with typing hints through the `Message` constructor: - - -=== "With typing hints" - - ```python - from pact.types import Message +/// - def message_producer_callback( - name: str, - params: dict[str, Any] | None, - ) -> Message: - assert name == "request to delete a user" - return Message( - contents=json.dumps({ - "action": "delete_user", - "user_id": "123", - }).encode("utf-8"), - metadata=None, - content_type="application/json", - ) - ``` +The function arguments must include the relevant keys from the [`MessageProducerArgs`][pact.types.MessageProducerArgs] typed dictionary. Pact Python will then intelligently determine how to pass the arguments in to your function, whether it be through positional or keyword arguments, or through variadic arguments. -=== "Without typing hints" +The output of the callback function should be an instance of the `Message` type. This is a simple [TypedDict][typing.TypedDict] that represents the message that the consumer expects and can be specified as a simple dictionary, or with typing hints through the `Message` constructor: - ```python - def message_producer_callback( - name: str, - params: dict[str, Any] | None, - ) -> dict[str, Any]: - assert name == "request to delete a user" - return { - "contents": json.dumps({ - "action": "delete_user", - "user_id": "123", - }).encode("utf-8"), - "metadata": None, - "content_type": "application/json", - } - ``` +/// tab | With typing hints + +```python +from pact.types import Message + +def message_producer_callback( + name: str, + params: dict[str, Any] | None, +) -> Message: + assert name == "request to delete a user" + return Message( + contents=json.dumps({ + "action": "delete_user", + "user_id": "123", + }).encode("utf-8"), + metadata=None, + content_type="application/json", + ) +``` + +/// + +/// tab | Without typing hints + +```python +def message_producer_callback(name, params): + assert name == "request to delete a user" + return { + "contents": json.dumps({ + "action": "delete_user", + "user_id": "123", + }).encode("utf-8"), + "metadata": None, + "content_type": "application/json", + } +``` + +/// In much the same way as the `state_handler` method, the `message_handler` method can also accept a mapping of message names to functions or raw messages. The function should expect only one argument: the `metadata` dictionary (which itself may be `None`); or if the message is static, the message can be provided directly: - -???+ example +/// details | Example - ```python - from pact import Verifier - from pact.types import Message +```python +from pact import Verifier +from pact.types import Message - def delete_user_message(metadata: dict[str, Any] | None) -> Message: - ... +def delete_user_message(metadata: dict[str, Any] | None) -> Message: + ... - def test_provider(): - verifier = Verifier("provider_name") - verifier.message_handler( - { - "request to delete a user": delete_user_message, - "create user": { - "contents": b"some message", - "metadata": None, - "content_type": "text/plain", - }, +def test_provider(): + verifier = Verifier("provider_name") + verifier.message_handler( + { + "request to delete a user": delete_user_message, + "create user": { + "contents": b"some message", + "metadata": None, + "content_type": "text/plain", }, - ) - ``` - + }, + ) +``` + +/// ---- - +/// define 28 March 2025 -: This blog post was updated on 28 March 2025 to reflect changes to the way functional arguments are handled. Instead of requiring positional arguments, Pact Python now inspects the function signature in order to determine whether to pass the arguments as positional or keyword arguments. It will fallback to passing the arguments through variadic arguments (`*args` and `**kwargs`) if present. This was done specific to allow for functions with optional arguments. + +- This blog post was updated on 28 March 2025 to reflect changes to the way functional arguments are handled. Instead of requiring positional arguments, Pact Python now inspects the function signature in order to determine whether to pass the arguments as positional or keyword arguments. It will fallback to passing the arguments through variadic arguments (`*args` and `**kwargs`) if present. This was done specific to allow for functions with optional arguments. For this added flexibility, the function signatures must have parameters that align with the [`StateHandlerArgs`][pact.types.StateHandlerArgs] and [`MessageProducerArgs`][pact.types.MessageProducerArgs] typed dictionaries. This allows Pact Python to match a `parameters=...` argument with the `parameters` key in the dictionary. Using an alternative name (e.g., `params`) will not work. 1 August 2025 -: With the release of Pact Python `v3` and the splitting of the CLI and FFI into standalone packages, some hyperlinks and code snippets have been updated to point to the new locations. The _text_ has been kept unchanged to preserve the original context and intent of the post. - - +- With the release of Pact Python `v3` and the splitting of the CLI and FFI into standalone packages, some hyperlinks and code snippets have been updated to point to the new locations. The _text_ has been kept unchanged to preserve the original context and intent of the post. +/// diff --git a/docs/consumer.md b/docs/consumer.md index 637e1cb6f..934b1e217 100644 --- a/docs/consumer.md +++ b/docs/consumer.md @@ -2,8 +2,7 @@ Pact is a consumer-driven contract testing tool. The consumer specifies the expected interactions with the provider, which are used to create a contract. This contract is then used to verify that the provider meets the consumer's expectations. - -
+/// html | div[align="center'] ```mermaid sequenceDiagram @@ -28,8 +27,7 @@ sequenceDiagram Provider->>P2: 404 Not Found ``` -
- +/// The consumer is the client that makes requests, and the provider is the server that responds. In most cases, the consumer is a front-end application and the provider is a back-end service; however, a back-end service may also require information from another service, making it a consumer of that service. @@ -241,17 +239,20 @@ pact-broker publish \ It expects the following environment variables to be set: +/// define `PACT_BROKER_BASE_URL` -: The base URL of the Pact Broker (e.g., `https://test.pactflow.io` if using [PactFlow](https://pactflow.io), or the URL to your self-hosted Pact Broker instance). +- The base URL of the Pact Broker (e.g., `https://test.pactflow.io` if using [PactFlow](https://pactflow.io), or the URL to your self-hosted Pact Broker instance). `PACT_BROKER_USERNAME` / `PACT_BROKER_PASSWORD` -: The username and password for authenticating with the Pact Broker. +- The username and password for authenticating with the Pact Broker. `PACT_BROKER_TOKEN` -: An alternative to using username and password, this is a token that can be used for authentication (e.g., used with [PactFlow](https://pactflow.io)). +- An alternative to using username and password, this is a token that can be used for authentication (e.g., used with [PactFlow](https://pactflow.io)). + +/// ## Pattern Matching diff --git a/docs/logging.md b/docs/logging.md index 3bbb5126f..c3285555d 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -25,11 +25,9 @@ The following log levels are available (from least to most verbose): ## Recommended Setup with Pytest - -!!! warning "One-time Initialization" - - The FFI logging can only be initialized **once** per process. Attempting to configure it multiple times will result in an error. For this reason, it's recommended to set up logging in a session-scoped fixture. - +/// warning | One-time Initialization +The FFI logging can only be initialized **once** per process. Attempting to configure it multiple times will result in an error. For this reason, it's recommended to set up logging in a session-scoped fixture. +/// The recommended way to configure FFI logging in your test suite is to use a pytest fixture with `autouse=True` and `scope="session"` in your `conftest.py` file: @@ -55,11 +53,9 @@ For more advanced use cases, the `pact_ffi` module provides additional logging f ### Logging to a File - -!!! note "Not Yet Implemented" - - The `log_to_file` function is currently not implemented in the Python bindings. If you need this feature, please open an issue on the [Pact Python GitHub repository](https://github.com/pact-foundation/pact-python/issues). - +/// note | Not Yet Implemented +The `log_to_file` function is currently not implemented in the Python bindings. If you need this feature, please open an issue on the [Pact Python GitHub repository](https://github.com/pact-foundation/pact-python/issues). +/// To direct logs to a file instead of stderr, you would use: @@ -87,19 +83,15 @@ This is particularly useful for: - Including logs in test failure reports - Processing or filtering log messages programmatically - -!!! note "Retrieving Buffer Contents" - - The `fetch_log_buffer` function for retrieving buffered logs is currently not implemented in the Python bindings. If you need this feature, please open an issue on the [Pact Python GitHub repository](https://github.com/pact-foundation/pact-python/issues). - +/// note | Retrieving Buffer Contents +The `fetch_log_buffer` function for retrieving buffered logs is currently not implemented in the Python bindings. If you need this feature, please open an issue on the [Pact Python GitHub repository](https://github.com/pact-foundation/pact-python/issues). +/// ### Multiple Sinks - -!!! note "Advanced Usage" - - The functions `logger_init`, `logger_attach_sink`, and `logger_apply` are currently not implemented in the Python bindings. If you need these features, please open an issue on the [Pact Python GitHub repository](https://github.com/pact-foundation/pact-python/issues). - +/// note | Advanced Usage +The functions `logger_init`, `logger_attach_sink`, and `logger_apply` are currently not implemented in the Python bindings. If you need these features, please open an issue on the [Pact Python GitHub repository](https://github.com/pact-foundation/pact-python/issues). +/// For the most advanced scenarios, the FFI supports configuring multiple log sinks simultaneously (e.g., logging to both stderr and a file). This requires using the lower-level `logger_init`, `logger_attach_sink`, and `logger_apply` functions, which are planned for future implementation. diff --git a/docs/provider.md b/docs/provider.md index 575e40feb..7edcd010a 100644 --- a/docs/provider.md +++ b/docs/provider.md @@ -2,8 +2,7 @@ Pact is a consumer-driven contract testing tool. The consumer specifies the expected interactions with the provider, which are used to create a contract. This contract is then used to verify that the provider behaves as expected. - -
+/// html | div[align="center"] ```mermaid sequenceDiagram @@ -28,8 +27,7 @@ sequenceDiagram Provider->>P2: 404 Not Found ``` -
- +/// The provider verification process works by replaying the interactions from the consumer against the provider and checking that the responses match what was expected. This is done using the Pact files created by the consumer tests, either by reading them from the local file system or by fetching them from a Pact Broker. diff --git a/examples/README.md b/examples/README.md index 40ccb91dd..9e79c71f2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -40,8 +40,7 @@ Each example can be run independently. Navigate to the specific example director Pact is a contract testing tool. Contract testing is a way to ensure that services (such as an API provider and a client) can communicate with each other. An interaction between a _consumer_ (i.e., a HTTP client, mobile app, website, microservice, etc.) and a _provider_ (i.e., a web server, microservice, etc.) would typically look like this: - -
+/// html | div[align="center"] ```mermaid sequenceDiagram @@ -53,13 +52,9 @@ sequenceDiagram Provider ->> Consumer: 404 Not Found ``` -
- - Pact allows for each side of the interaction to be tested independently. Pact achieves this by mocking the other side of the interaction: - -
+/// html | div[align="center"] ```mermaid sequenceDiagram @@ -84,9 +79,6 @@ sequenceDiagram Provider->>P2: 404 Not Found ``` -
- - Pact is **consumer driven**. This means that the consumer is responsible for defining the interactions it expects from the provider through the pattern of diff --git a/examples/plugins/proto/person_pb2.py b/examples/plugins/proto/person_pb2.py index fd2f14a03..3d0c20d40 100644 --- a/examples/plugins/proto/person_pb2.py +++ b/examples/plugins/proto/person_pb2.py @@ -7,9 +7,9 @@ This module is auto-generated from the person.proto file using the protobuf compiler. It provides Python classes for all messages and services defined in the proto file, and is intended for use in educational and demonstration contexts. -!!! note - - This file is generated code. Manual changes (except for documentation improvements) will be overwritten if the file is regenerated. +/// note +This file is generated code. Manual changes (except for documentation improvements) will be overwritten if the file is regenerated. +/// """ from __future__ import annotations diff --git a/examples/plugins/proto/person_pb2.pyi b/examples/plugins/proto/person_pb2.pyi index a2cbccc96..a8545eabe 100644 --- a/examples/plugins/proto/person_pb2.pyi +++ b/examples/plugins/proto/person_pb2.pyi @@ -3,9 +3,9 @@ Type stubs for protocol buffer messages and services for the AddressBook pedagog This module is auto-generated from the person.proto file and provides type hints for all messages and services defined in the proto file. It is intended for use in educational and demonstration contexts, and helps with static analysis and editor support. -!!! note - - This file is generated code. Manual changes (except for documentation improvements) will be overwritten if the file is regenerated. +/// note +This file is generated code. Manual changes (except for documentation improvements) will be overwritten if the file is regenerated. +/// """ # ruff: noqa: PGH004 diff --git a/examples/plugins/proto/person_pb2_grpc.py b/examples/plugins/proto/person_pb2_grpc.py index 1e0ebc4e1..75e72221b 100644 --- a/examples/plugins/proto/person_pb2_grpc.py +++ b/examples/plugins/proto/person_pb2_grpc.py @@ -6,9 +6,9 @@ This module is generated by the protobuf compiler plugin and demonstrates how to use gRPC in Python. It is intended for pedagogical purposes, showing how to implement and interact with gRPC services. -!!! note - - This file is generated and should not be modified manually, except for documentation improvements. +/// note +This file is generated and should not be modified manually, except for documentation improvements. +/// """ from __future__ import annotations diff --git a/mkdocs.yml b/mkdocs.yml index 03a8c7069..faadb9ea8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -86,9 +86,7 @@ plugins: markdown_extensions: # Python Markdown - abbr - - admonition - attr_list - - def_list - footnotes - meta - md_in_html @@ -101,8 +99,13 @@ markdown_extensions: generic: true - pymdownx.betterem: smart_enable: all + - pymdownx.blocks.admonition + - pymdownx.blocks.definition + - pymdownx.blocks.details + - pymdownx.blocks.html: + - pymdownx.blocks.tab: + alternate_style: true - pymdownx.caret - - pymdownx.details - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg @@ -120,8 +123,6 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format - - pymdownx.tabbed: - alternate_style: true - pymdownx.tasklist: custom_checkbox: true - pymdownx.tilde diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index 55d2d4266..b9b7c3a5d 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -5,11 +5,12 @@ around the C API, and is intended to be used by the Pact Python client library to provide a Pythonic interface to Pact. -!!! warning +/// warning +This module is not intended to be used directly by Pact users. Pact users should +use the Pact Python client library instead. No guarantees are made about the +stability of this module's API. +/// - This module is not intended to be used directly by Pact users. Pact users - should use the Pact Python client library instead. No guarantees are made - about the stability of this module's API. ## Developer Notes @@ -6223,13 +6224,12 @@ def with_binary_file( """ Adds a binary file as the body with the expected content type and contents. - !!! warning - - This function is deprecated. Use - [`with_binary_body`][pact_ffi.with_binary_body] in order to set the - binary body, and use - [`with_matching_rules`][pact_ffi.with_matching_rules] to set the - matching rules to ensure that only the content type is being matched. + /// warning + This function is deprecated. Use + [`with_binary_body`][pact_ffi.with_binary_body] in order to set the binary + body, and use [`with_matching_rules`][pact_ffi.with_matching_rules] to set + the matching rules to ensure that only the content type is being matched. + /// Will use a mime type matcher to match the body. Returns false if the interaction or Pact can't be modified (i.e. the mock server for it has diff --git a/src/pact/generate/__init__.py b/src/pact/generate/__init__.py index 4104de3c5..26741ff7f 100644 --- a/src/pact/generate/__init__.py +++ b/src/pact/generate/__init__.py @@ -12,23 +12,24 @@ are resilient to changes and do not rely on hardcoded values, which can lead to brittle tests. -!!! warning +/// warning +Do not import functions directly from `pact.generate` to avoid shadowing Python +built-in types. Instead, import the `generate` module and use its functions as +`generate.int`, `generate.str`, etc. - Do not import functions directly from `pact.generate` to avoid shadowing - Python built-in types. Instead, import the `generate` module and use its - functions as `generate.int`, `generate.str`, etc. +```python +# Recommended +from pact import generate - ```python - # Recommended - from pact import generate +generate.int(...) - generate.int(...) +# Not recommended +from pact.generate import int - # Not recommended - from pact.generate import int +int(...) +``` +/// - int(...) - ``` Many functions in this module are named after the type they generate (e.g., `int`, `str`, `bool`). Importing directly from this module may shadow Python diff --git a/src/pact/interaction/_async_message_interaction.py b/src/pact/interaction/_async_message_interaction.py index a76a5d32a..1595c32f7 100644 --- a/src/pact/interaction/_async_message_interaction.py +++ b/src/pact/interaction/_async_message_interaction.py @@ -16,10 +16,6 @@ class AsyncMessageInteraction(Interaction): and a provider. It defines the kind of messages a consumer can accept, and the is agnostic of the underlying protocol, be it a message queue, Apache Kafka, or some other asynchronous protocol. - - !!! warning - - This class is not yet fully implemented and is not yet usable. """ def __init__(self, pact_handle: pact_ffi.PactHandle, description: str) -> None: diff --git a/src/pact/interaction/_sync_message_interaction.py b/src/pact/interaction/_sync_message_interaction.py index 92258d7b1..60df05418 100644 --- a/src/pact/interaction/_sync_message_interaction.py +++ b/src/pact/interaction/_sync_message_interaction.py @@ -21,10 +21,6 @@ class SyncMessageInteraction(Interaction): a provider. As with [`HttpInteraction`][pact.pact.HttpInteraction], it defines a specific request that the consumer makes to the provider, and the response that the provider should return. - - !!! warning - - This class is not yet fully implemented and is not yet usable. """ def __init__(self, pact_handle: pact_ffi.PactHandle, description: str) -> None: diff --git a/src/pact/match/__init__.py b/src/pact/match/__init__.py index 5b98af334..8ad083303 100644 --- a/src/pact/match/__init__.py +++ b/src/pact/match/__init__.py @@ -11,22 +11,23 @@ creation timestamp. The contract can require the ID to match a specific format (e.g., integer or UUID) and the timestamp to be ISO 8601. -!!! warning +/// warning +Do not import functions directly from this module. Instead, import the `match` +module and use its functions: - Do not import functions directly from this module. Instead, import the - `match` module and use its functions: +```python +# Recommended +from pact import match - ```python - # Recommended - from pact import match +match.int(...) - match.int(...) +# Not recommended +from pact.match import int - # Not recommended - from pact.match import int +int(...) +``` +/// - int(...) - ``` Many functions in this module are named after the types they match (e.g., `int`, `str`, `bool`). Importing directly from this module may shadow Python built-in @@ -37,12 +38,12 @@ generator is used; if a `value` is provided, a generator is not used. This is _not_ advised, as leads to non-deterministic tests. -!!! note +/// note +You do not need to specify everything that will be returned from the provider in +a JSON response. Any extra data that is received will be ignored and the tests +will still pass, as long as the expected fields match the defined patterns. +/// - You do not need to specify everything that will be returned from the - provider in a JSON response. Any extra data that is received will be - ignored and the tests will still pass, as long as the expected fields - match the defined patterns. For more information about the Pact matching specification, see [Matching](https://docs.pact.io/getting_started/matching). diff --git a/src/pact/verifier.py b/src/pact/verifier.py index 50487ca69..35dcfe476 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -15,12 +15,12 @@ consumer against the provider and ensure that the provider's responses match the expectations set by the consumer. -!!! info - - The interface provided by this module could be improved. If you have any - suggestions, please consider creating a new [GitHub - discussion](https://github.com/pact-foundation/pact-python/discussions) or - reaching out over [Slack](https://slack.pact.io). +/// info +The interface provided by this module could be improved. If you have any +suggestions, please consider creating a new [GitHub +discussion](https://github.com/pact-foundation/pact-python/discussions) or +reaching out over [Slack](https://slack.pact.io). +/// ## Usage diff --git a/tests/compatibility_suite/util/__init__.py b/tests/compatibility_suite/util/__init__.py index e227e9186..70de802a1 100644 --- a/tests/compatibility_suite/util/__init__.py +++ b/tests/compatibility_suite/util/__init__.py @@ -50,14 +50,14 @@ class PactInteractionTuple(Generic[_T]): and an `Interaction` subclass. This named tuple is used to pass these objects around more easily. - !!! note - - This should be simplified in the future to simply being a - [`NamedTuple`][typing.NamedTuple]; however, earlier versions of Python - do not support inheriting from multiple classes, thereby preventing - `class PactInteractionTuple(NamedTuple, Generic[_T])` (even if - [`Generic[_T]`][typing.Generic] serves no purpose other than to allow - type hinting). + /// note + This should be simplified in the future to simply being a + [`NamedTuple`][typing.NamedTuple]; however, earlier versions of Python do + not support inheriting from multiple classes, thereby preventing `class + PactInteractionTuple(NamedTuple, Generic[_T])` (even if + [`Generic[_T]`][typing.Generic] serves no purpose other than to allow type + hinting). + /// """ def __init__(self, pact: Pact, interaction: _T) -> None: From c5eeed33650be1468a912513edb235c40a1bb586 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:09:35 +1100 Subject: [PATCH 1080/1376] chore(deps): update ruff to v0.14.2 (#1306) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d72ae9ad..144ed4ff9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.1 + rev: v0.14.2 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 7b23639f6..5f0b63568 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.1", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.2", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=8.0"] types = ["mypy==1.18.2"] From de6752e28fa5b7d78517f4b5d5eaacf71f616d77 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:14:45 +1100 Subject: [PATCH 1081/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.2.7 (#1305) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 144ed4ff9..240b2a81f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.2.6 + rev: v2.2.7 hooks: - id: biome-check From 9da4fb23170997e359b4df53afd29921346a6190 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 11:05:11 +1100 Subject: [PATCH 1082/1376] chore(deps): update github artifact actions (major) (#1307) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 6 +++--- .github/workflows/build-ffi.yml | 6 +++--- .github/workflows/build.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 5329ab52a..d25873c0e 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -70,7 +70,7 @@ jobs: run: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: wheels-sdist path: pact-python-cli/dist/*.tar* @@ -106,7 +106,7 @@ jobs: CIBW_BUILD: cp${{ env.STABLE_PYTHON_VERSION }}-* - name: Upload wheels - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: wheels-${{ matrix.os }} path: wheelhouse/*.whl @@ -179,7 +179,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - name: Download wheels and sdist - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: path: wheelhouse merge-multiple: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 9fd091158..4543e8b38 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -70,7 +70,7 @@ jobs: run: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: wheels-sdist path: pact-python-ffi/dist/*.tar* @@ -107,7 +107,7 @@ jobs: HATCH_VERBOSE: '1' - name: Upload wheels - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: wheels-${{ matrix.os }} path: wheelhouse/*.whl @@ -180,7 +180,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - name: Download wheels and sdist - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: path: wheelhouse merge-multiple: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6e67ace5f..48e054579 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: run: hatch build - name: Upload sdist - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: wheels-sdist path: ./dist/*.tar* @@ -76,7 +76,7 @@ jobs: compression-level: 0 - name: Upload wheel - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: wheels-whl path: ./dist/*.whl @@ -146,7 +146,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - name: Download wheels and sdist - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: path: wheelhouse merge-multiple: true From 781e98c23b28e8ee54839a6039db39918bd342e4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:29:19 +1100 Subject: [PATCH 1083/1376] chore(deps): update astral-sh/setup-uv action to v7.1.2 (#1309) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index d25873c0e..78569e132 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 4543e8b38..9752941f8 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48e054579..edf063707 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 013ef2331..6d9168708 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 20b57b02a..47e753450 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: true @@ -154,7 +154,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: true @@ -197,7 +197,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: true @@ -229,7 +229,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: true @@ -262,7 +262,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: true @@ -302,7 +302,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: true cache-suffix: prek From 5fd946974787c310542789d1bf48b586787a0f5f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:29:32 +1100 Subject: [PATCH 1084/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.0 (#1308) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 240b2a81f..948fc3d66 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.2.7 + rev: v2.3.0 hooks: - id: biome-check From f7863a82c164bdf0a0f5d299e918923e18cb6a08 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:09:23 +1100 Subject: [PATCH 1085/1376] chore(deps): update taiki-e/install-action action to v2.62.38 (#1310) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 78569e132..aa6fe9e0c 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@e43a5023a747770bfcb71ae048541a681714b951 # v2.62.33 + uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2.62.38 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 9752941f8..74abb9a71 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@e43a5023a747770bfcb71ae048541a681714b951 # v2.62.33 + uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2.62.38 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index edf063707..551a49d57 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@e43a5023a747770bfcb71ae048541a681714b951 # v2.62.33 + uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2.62.38 with: tool: git-cliff,typos From da29864657ff860fa57915202f0a32ade1fbb783 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:40:59 +1100 Subject: [PATCH 1086/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.1 (#1311) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 948fc3d66..4a1cf90bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.0 + rev: v2.3.1 hooks: - id: biome-check From 2bb3faf7c81d9fb40f670ca73404f25588f22675 Mon Sep 17 00:00:00 2001 From: Nikhil Arora Date: Wed, 29 Oct 2025 09:25:36 +0530 Subject: [PATCH 1087/1376] chore: set telemetry environment variables Set a couple of environment variables to give context to the underlying CLIs about the environment that is executing these CLIs. Co-authored-by: JP-Ellis --- pact-python-cli/src/pact_cli/__init__.py | 27 +++++++++++++++++- pact-python-cli/tests/test_telemetry.py | 35 ++++++++++++++++++++++++ src/pact/v2/broker.py | 6 ++-- src/pact/v2/message_pact.py | 4 ++- src/pact/v2/pact.py | 4 ++- src/pact/v2/verify_wrapper.py | 10 +++---- tests/v2/test_broker.py | 19 +++++++------ tests/v2/test_message_pact.py | 5 ++-- tests/v2/test_pact.py | 7 +++-- 9 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 pact-python-cli/tests/test_telemetry.py diff --git a/pact-python-cli/src/pact_cli/__init__.py b/pact-python-cli/src/pact_cli/__init__.py index 0e1184700..34b577523 100644 --- a/pact-python-cli/src/pact_cli/__init__.py +++ b/pact-python-cli/src/pact_cli/__init__.py @@ -30,8 +30,10 @@ import os import shutil +import sys import warnings from pathlib import Path +from typing import TYPE_CHECKING from pact_cli.__version__ import ( __version__ as __version__, @@ -40,10 +42,33 @@ __version_tuple__ as __version_tuple__, ) +if TYPE_CHECKING: + from collections.abc import Mapping + _USE_SYSTEM_BINS = os.getenv("PACT_USE_SYSTEM_BINS", "").upper() in ("TRUE", "YES") _BIN_DIR = Path(__file__).parent.resolve() / "bin" +def _telemetry_env() -> Mapping[str, str]: + """ + Get environment variables with Pact telemetry data. + + Returns a copy of the current environment with the following two keys added: + + - `PACT_EXECUTING_LANGUAGE`: Set to "python". + - `PACT_EXECUTING_LANGUAGE_VERSION`: Set to the current Python version + in "major.minor" format. + + Returns: + Environment dictionary Pact telemetry added. + """ + env = os.environ.copy() + env["PACT_EXECUTING_LANGUAGE"] = "python" + version = f"{sys.version_info.major}.{sys.version_info.minor}" + env["PACT_EXECUTING_LANGUAGE_VERSION"] = version + return env + + def _exec() -> None: """ Execute Pact CLI tools routed through the generated entry points. @@ -92,7 +117,7 @@ def _exec() -> None: print(f"Command '{command}' not found.", file=sys.stderr) # noqa: T201 sys.exit(1) - os.execv(executable, [executable, *args]) # noqa: S606 + os.execve(executable, [executable, *args], _telemetry_env()) # noqa: S606 def _find_executable(executable: str) -> str | None: diff --git a/pact-python-cli/tests/test_telemetry.py b/pact-python-cli/tests/test_telemetry.py new file mode 100644 index 000000000..0437dd22a --- /dev/null +++ b/pact-python-cli/tests/test_telemetry.py @@ -0,0 +1,35 @@ +"""Tests for telemetry environment variables.""" + +from __future__ import annotations + +import sys +from unittest.mock import patch + +from pact_cli import _telemetry_env + + +def test_telemetry_env_sets_language() -> None: + env = _telemetry_env() + assert env["PACT_EXECUTING_LANGUAGE"] == "python" + + +def test_telemetry_env_sets_version() -> None: + env = _telemetry_env() + expected_version = f"{sys.version_info.major}.{sys.version_info.minor}" + assert env["PACT_EXECUTING_LANGUAGE_VERSION"] == expected_version + + +def test_telemetry_env_preserves_existing_env() -> None: + mock_env = {"EXISTING_VAR": "existing_value", "PATH": "/usr/bin"} + with patch("os.environ", mock_env): + env = _telemetry_env() + assert env["EXISTING_VAR"] == "existing_value" + assert env["PATH"] == "/usr/bin" + assert "PACT_EXECUTING_LANGUAGE" in env + assert "PACT_EXECUTING_LANGUAGE_VERSION" in env + + +def test_telemetry_env_returns_copy() -> None: + env1 = _telemetry_env() + env2 = _telemetry_env() + assert env1 is not env2 diff --git a/src/pact/v2/broker.py b/src/pact/v2/broker.py index 2d166945b..229c33bbd 100644 --- a/src/pact/v2/broker.py +++ b/src/pact/v2/broker.py @@ -2,12 +2,14 @@ from __future__ import unicode_literals import fnmatch +import logging import os from subprocess import Popen +from pact_cli import _telemetry_env + from .constants import BROKER_CLIENT_PATH -import logging log = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -106,7 +108,7 @@ def publish(self, consumer_name, version, pact_dir=None, log.debug(f"PactBroker publish command: {log_command}") - publish_process = Popen(command) + publish_process = Popen(command, env=_telemetry_env()) publish_process.wait() if publish_process.returncode != 0: url = self._get_broker_base_url() diff --git a/src/pact/v2/message_pact.py b/src/pact/v2/message_pact.py index d074f328d..2eaee6ff0 100644 --- a/src/pact/v2/message_pact.py +++ b/src/pact/v2/message_pact.py @@ -5,6 +5,8 @@ import os from subprocess import Popen +from pact_cli import _telemetry_env + from .broker import Broker from .constants import MESSAGE_PATH from .matchers import from_term @@ -179,7 +181,7 @@ def write_to_pact_file(self): "--provider", f"{self.provider.name}", ] - self._message_process = Popen(command) + self._message_process = Popen(command, env=_telemetry_env()) self._message_process.wait() def _insert_message_if_complete(self): diff --git a/src/pact/v2/pact.py b/src/pact/v2/pact.py index 96cef1702..da5113217 100644 --- a/src/pact/v2/pact.py +++ b/src/pact/v2/pact.py @@ -10,6 +10,8 @@ from requests.adapters import HTTPAdapter from requests.packages.urllib3 import Retry +from pact_cli import _telemetry_env + from .broker import Broker from .constants import MOCK_SERVICE_PATH from .matchers import from_term @@ -217,7 +219,7 @@ def start_service(self): if self.cors: command.extend(['--cors']) - self._process = Popen(command) + self._process = Popen(command, env=_telemetry_env()) self._wait_for_server_start() def stop_service(self): diff --git a/src/pact/v2/verify_wrapper.py b/src/pact/v2/verify_wrapper.py index a65c20e79..7ead8b01d 100644 --- a/src/pact/v2/verify_wrapper.py +++ b/src/pact/v2/verify_wrapper.py @@ -1,13 +1,13 @@ """Wrapper to verify previously created pacts.""" -from pact_cli import VERIFIER_PATH -import sys import os import platform - import subprocess -from os.path import isdir, join, isfile +import sys from os import listdir +from os.path import isdir, isfile, join + +from pact_cli import VERIFIER_PATH, _telemetry_env def capture_logs(process, verbose): @@ -98,7 +98,7 @@ def rerun_command(): " PACT_PROVIDER_STATE=''" " {command}".format(command=' '.join(sys.argv))) - env = os.environ.copy() + env = _telemetry_env() env['PACT_INTERACTION_RERUN_COMMAND'] = command return env diff --git a/tests/v2/test_broker.py b/tests/v2/test_broker.py index dbb81786f..ab5bf2435 100644 --- a/tests/v2/test_broker.py +++ b/tests/v2/test_broker.py @@ -1,5 +1,6 @@ import os from unittest import TestCase +from unittest.mock import ANY from mock import patch @@ -48,7 +49,7 @@ def test_publish_fails(self): '--broker-username=username', '--broker-password=password', '--broker-token=token', - './TestConsumer-TestProvider.json']) + './TestConsumer-TestProvider.json'], env=ANY) def test_publish_with_broker_url_environment_variable(self): BROKER_URL_ENV = 'http://broker.url' @@ -67,7 +68,7 @@ def test_publish_with_broker_url_environment_variable(self): f"--broker-base-url={BROKER_URL_ENV}", '--broker-username=username', '--broker-password=password', - './TestConsumer-TestProvider.json']) + './TestConsumer-TestProvider.json'], env=ANY) del os.environ["PACT_BROKER_BASE_URL"] @@ -86,7 +87,7 @@ def test_basic_authenticated_publish(self): '--broker-base-url=http://localhost', '--broker-username=username', '--broker-password=password', - './TestConsumer-TestProvider.json']) + './TestConsumer-TestProvider.json'], env=ANY) def test_token_authenticated_publish(self): broker = Broker(broker_base_url="http://localhost", @@ -105,7 +106,7 @@ def test_token_authenticated_publish(self): '--broker-username=username', '--broker-password=password', '--broker-token=token', - './TestConsumer-TestProvider.json']) + './TestConsumer-TestProvider.json'], env=ANY) def test_git_tagged_publish(self): broker = Broker(broker_base_url="http://localhost") @@ -120,7 +121,7 @@ def test_git_tagged_publish(self): '--consumer-app-version=2.0.1', '--broker-base-url=http://localhost', './TestConsumer-TestProvider.json', - '--tag-with-git-branch']) + '--tag-with-git-branch'], env=ANY) def test_manual_tagged_publish(self): broker = Broker(broker_base_url="http://localhost") @@ -136,7 +137,7 @@ def test_manual_tagged_publish(self): '--broker-base-url=http://localhost', './TestConsumer-TestProvider.json', '-t', 'tag1', - '-t', 'tag2']) + '-t', 'tag2'], env=ANY) def test_branch_publish(self): broker = Broker(broker_base_url="http://localhost") @@ -151,7 +152,7 @@ def test_branch_publish(self): '--consumer-app-version=2.0.1', '--broker-base-url=http://localhost', './TestConsumer-TestProvider.json', - '--branch=consumer-branch']) + '--branch=consumer-branch'], env=ANY) def test_build_url_publish(self): broker = Broker(broker_base_url="http://localhost") @@ -166,7 +167,7 @@ def test_build_url_publish(self): '--consumer-app-version=2.0.1', '--broker-base-url=http://localhost', './TestConsumer-TestProvider.json', - '--build-url=http://ci']) + '--build-url=http://ci'], env=ANY) def test_auto_detect_version_properties_publish(self): broker = Broker(broker_base_url="http://localhost") @@ -181,4 +182,4 @@ def test_auto_detect_version_properties_publish(self): '--consumer-app-version=2.0.1', '--broker-base-url=http://localhost', './TestConsumer-TestProvider.json', - '--auto-detect-version-properties']) + '--auto-detect-version-properties'], env=ANY) diff --git a/tests/v2/test_message_pact.py b/tests/v2/test_message_pact.py index e60d176d3..ef35b011c 100644 --- a/tests/v2/test_message_pact.py +++ b/tests/v2/test_message_pact.py @@ -1,8 +1,9 @@ import os import json +from unittest import TestCase +from unittest.mock import ANY from mock import patch -from unittest import TestCase from pact.v2.message_consumer import MessageConsumer, Provider from pact.v2.message_pact import MessagePact @@ -264,4 +265,4 @@ def test_call_pact_message_to_generate_pact_file(self): '--pact-specification-version=3.0.0', '--consumer', 'TestConsumer', '--provider', 'TestProvider', - ]) + ], env=ANY) diff --git a/tests/v2/test_pact.py b/tests/v2/test_pact.py index d65d43314..27ae88d27 100644 --- a/tests/v2/test_pact.py +++ b/tests/v2/test_pact.py @@ -1,6 +1,7 @@ import os from subprocess import Popen from unittest import TestCase +from unittest.mock import ANY from mock import patch, call, Mock from psutil import Process @@ -317,7 +318,7 @@ def test_start_fails(self): '--pact-file-write-mode', 'overwrite', '--pact-specification-version=2.0.0', '--consumer', 'consumer', - '--provider', 'provider']) + '--provider', 'provider'], env=ANY) def test_start_no_ssl(self): pact = Pact(Consumer('consumer'), Provider('provider'), @@ -333,7 +334,7 @@ def test_start_no_ssl(self): '--pact-file-write-mode', 'overwrite', '--pact-specification-version=2.0.0', '--consumer', 'consumer', - '--provider', 'provider']) + '--provider', 'provider'], env=ANY) def test_start_with_ssl(self): pact = Pact(Consumer('consumer'), Provider('provider'), @@ -353,7 +354,7 @@ def test_start_with_ssl(self): '--provider', 'provider', '--ssl', '--sslcert', '/ssl.cert', - '--sslkey', '/ssl.key']) + '--sslkey', '/ssl.key'], env=ANY) def test_stop_posix(self): self.mock_publish.return_value.returncode = 0 From ef9259373f07c0b188c813ff974536105625197c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:25:39 +1100 Subject: [PATCH 1088/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.2 (#1313) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a1cf90bd..423b076eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.1 + rev: v2.3.2 hooks: - id: biome-check From 253e97226bde23f47c4726c8075c211eda88e2a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:05:13 +1100 Subject: [PATCH 1089/1376] chore(deps): update ruff to v0.14.3 (#1314) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 423b076eb..40cb3276f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - id: taplo-lint - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.2 + rev: v0.14.3 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 5f0b63568..788cae5c5 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.2", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.3", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=8.0"] types = ["mypy==1.18.2"] From d613cf44afd8d756e9581898fc47b148144093c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 15:19:53 +1100 Subject: [PATCH 1090/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.39.0 (#1317) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 40cb3276f..9520d0667 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -77,7 +77,7 @@ repos: )$ - repo: https://github.com/crate-ci/typos - rev: v1.38.1 + rev: v1.39.0 hooks: - id: typos exclude: | From 723a031985317146d0d60869d518c16bee9712dd Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 31 Oct 2025 15:17:21 +0000 Subject: [PATCH 1091/1376] chore(docs): api docs link on pact-python site is case sensitive https://pact-foundation.github.io/pact-python/API/ and https://pact-foundation.github.io/pact-python/api/ don't route to the same page, unsure if it should, so update link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 486c68e20..fafde71d2 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ This readme provides a high-level overview of the Pact Python library. For detai - [Provider testing](docs/provider.md) - [Examples](examples/README.md) -Documentation for the API is generated from the docstrings in the code which you can view at [`pact-foundation.github.io/pact-python/pact`](https://pact-foundation.github.io/pact-python/API). +Documentation for the API is generated from the docstrings in the code which you can view at [`pact-foundation.github.io/pact-python/pact`](https://pact-foundation.github.io/pact-python/api). ### Need Help From 27390ae4c021ddbc7fd2a1ba3e8eee2c05dbcbc5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 07:48:07 +1100 Subject: [PATCH 1092/1376] chore(deps): update taiki-e/install-action action to v2.62.45 (#1318) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index aa6fe9e0c..2183b3f9c 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2.62.38 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 74abb9a71..cec017835 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2.62.38 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 551a49d57..21ccd1ac5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2.62.38 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: git-cliff,typos From a6c6263fc16ae2b34fbb69db0a0032f7aa0902e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:28:26 +1100 Subject: [PATCH 1093/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.3 (#1320) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9520d0667..d91dfec13 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.2 + rev: v2.3.3 hooks: - id: biome-check From 6f2819b517e66b2f9293a7d4fd9527f4f72f5e61 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:42:35 +1100 Subject: [PATCH 1094/1376] chore(deps): update softprops/action-gh-release action to v2.4.2 (#1324) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 2183b3f9c..7f4e6f0a6 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -186,7 +186,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 + uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index cec017835..785a48c2c 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -187,7 +187,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 + uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 21ccd1ac5..dae5001fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,7 +153,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 + uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md From b031eb7677aeb46e1962d86b991191fa21a63b5c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 10 Nov 2025 16:37:31 +1100 Subject: [PATCH 1095/1376] chore: fix json schema url Schema Store seem to have changed the URL; and while the previous 'json' subdomain should redirect, it seems flakey and results in failures in CI. In addition, removing the Taplo pre-commit hook as it is no longer maintained. --- .pre-commit-config.yaml | 6 ------ cliff.toml | 2 +- docs/scripts/.ruff.toml | 2 +- examples/.ruff.toml | 2 +- examples/http/aiohttp_and_flask/pyproject.toml | 2 +- examples/http/requests_and_fastapi/pyproject.toml | 2 +- pact-python-cli/cliff.toml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-cli/tests/.ruff.toml | 2 +- pact-python-ffi/cliff.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pact-python-ffi/tests/.ruff.toml | 2 +- pyproject.toml | 2 +- tests/.ruff.toml | 2 +- 14 files changed, 13 insertions(+), 19 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d91dfec13..e963a8473 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,12 +38,6 @@ repos: hooks: - id: biome-check - - repo: https://github.com/ComPWA/taplo-pre-commit - rev: v0.9.3 - hooks: - - id: taplo-format - - id: taplo-lint - - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.3 hooks: diff --git a/cliff.toml b/cliff.toml index 1301c49d4..49677c920 100644 --- a/cliff.toml +++ b/cliff.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/any.json +#:schema https://www.schemastore.org/any.json # git-cliff configuration file # https://git-cliff.org/docs/configuration diff --git a/docs/scripts/.ruff.toml b/docs/scripts/.ruff.toml index 1dfa5d3e3..21a6c84ee 100644 --- a/docs/scripts/.ruff.toml +++ b/docs/scripts/.ruff.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/ruff.json +#:schema https://www.schemastore.org/ruff.json extend = "../../pyproject.toml" [lint] diff --git a/examples/.ruff.toml b/examples/.ruff.toml index 6a9b80925..2de3b8854 100644 --- a/examples/.ruff.toml +++ b/examples/.ruff.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/ruff.json +#:schema https://www.schemastore.org/ruff.json extend = "../pyproject.toml" [lint.per-file-ignores] diff --git a/examples/http/aiohttp_and_flask/pyproject.toml b/examples/http/aiohttp_and_flask/pyproject.toml index b68d34d7c..755b39e02 100644 --- a/examples/http/aiohttp_and_flask/pyproject.toml +++ b/examples/http/aiohttp_and_flask/pyproject.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/pyproject.json +#:schema https://www.schemastore.org/pyproject.json [project] name = "example-aiohttp-and-flask" diff --git a/examples/http/requests_and_fastapi/pyproject.toml b/examples/http/requests_and_fastapi/pyproject.toml index 9b03fc883..66d4a2487 100644 --- a/examples/http/requests_and_fastapi/pyproject.toml +++ b/examples/http/requests_and_fastapi/pyproject.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/pyproject.json +#:schema https://www.schemastore.org/pyproject.json [project] name = "example-requests-and-fastapi" diff --git a/pact-python-cli/cliff.toml b/pact-python-cli/cliff.toml index 67031d710..8791e03d5 100644 --- a/pact-python-cli/cliff.toml +++ b/pact-python-cli/cliff.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/any.json +#:schema https://www.schemastore.org/any.json # git-cliff configuration file # https://git-cliff.org/docs/configuration diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 788cae5c5..6d3de766f 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/pyproject.json +#:schema https://www.schemastore.org/pyproject.json [project] description = "Pact CLI bundle for Python" name = "pact-python-cli" diff --git a/pact-python-cli/tests/.ruff.toml b/pact-python-cli/tests/.ruff.toml index 393a4247b..e88fc30b9 100644 --- a/pact-python-cli/tests/.ruff.toml +++ b/pact-python-cli/tests/.ruff.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/ruff.json +#:schema https://www.schemastore.org/ruff.json extend = "../pyproject.toml" [lint] diff --git a/pact-python-ffi/cliff.toml b/pact-python-ffi/cliff.toml index afc3a7863..8cc69a39a 100644 --- a/pact-python-ffi/cliff.toml +++ b/pact-python-ffi/cliff.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/any.json +#:schema https://www.schemastore.org/any.json # git-cliff configuration file # https://git-cliff.org/docs/configuration diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index bc0374cd8..977e8588f 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/pyproject.json +#:schema https://www.schemastore.org/pyproject.json [project] description = "Python bindings for the Pact FFI library" name = "pact-python-ffi" diff --git a/pact-python-ffi/tests/.ruff.toml b/pact-python-ffi/tests/.ruff.toml index 393a4247b..e88fc30b9 100644 --- a/pact-python-ffi/tests/.ruff.toml +++ b/pact-python-ffi/tests/.ruff.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/ruff.json +#:schema https://www.schemastore.org/ruff.json extend = "../pyproject.toml" [lint] diff --git a/pyproject.toml b/pyproject.toml index 25503133b..73f5aa2a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/pyproject.json +#:schema https://www.schemastore.org/pyproject.json [project] description = "Tool for creating and verifying consumer-driven contracts using the Pact framework." name = "pact-python" diff --git a/tests/.ruff.toml b/tests/.ruff.toml index c5797dca2..cc96e2421 100644 --- a/tests/.ruff.toml +++ b/tests/.ruff.toml @@ -1,4 +1,4 @@ -#:schema https://json.schemastore.org/ruff.json +#:schema https://www.schemastore.org/ruff.json extend = "../pyproject.toml" [lint] From 2bc2ab643bb72d9ddd24c57f25b1cf4148ad2dc7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 05:59:37 +0000 Subject: [PATCH 1096/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.4 (#1321) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e963a8473..74f96f14d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.3 + rev: v2.3.4 hooks: - id: biome-check From 5ec4e2f90ee585d48eae129b106630f1a3a1f7c4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 06:00:58 +0000 Subject: [PATCH 1097/1376] chore(deps): update ruff to v0.14.4 (#1322) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74f96f14d..df5c00f94 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.3 + rev: v0.14.4 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 6d3de766f..5cbe3e324 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.3", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.4", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=8.0"] types = ["mypy==1.18.2"] From 575b6569d2efce49d3396ea3f5630e689e29f692 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 06:05:42 +0000 Subject: [PATCH 1098/1376] chore(deps): update taiki-e/install-action action to v2.62.49 (#1325) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 7f4e6f0a6..e91045ec4 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 + uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 785a48c2c..6363f6098 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 + uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dae5001fe..1095ffa30 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 + uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 with: tool: git-cliff,typos From 188b47090ef952eead7497373e5e7dd324066b1d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:05:33 +1100 Subject: [PATCH 1099/1376] chore(deps): update astral-sh/setup-uv action to v7.1.3 (#1327) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index e91045ec4..54297ea91 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 6363f6098..2b7ba11f8 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1095ffa30..bff9141c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6d9168708..289612b67 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47e753450..fd9612534 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 with: enable-cache: true @@ -154,7 +154,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 with: enable-cache: true @@ -197,7 +197,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 with: enable-cache: true @@ -229,7 +229,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 with: enable-cache: true @@ -262,7 +262,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 with: enable-cache: true @@ -302,7 +302,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 with: enable-cache: true cache-suffix: prek From 12330c9bd856a1f1ed0bd7701dbe41d52a1e498d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 03:44:50 +0000 Subject: [PATCH 1100/1376] chore(deps): update dependency pytest to v9 (#1323) Co-authored-by: JP-Ellis --- pact-python-cli/pyproject.toml | 24 +++++++-------- pact-python-ffi/pyproject.toml | 22 +++++++------- pyproject.toml | 53 +++++++++++++++++----------------- 3 files changed, 47 insertions(+), 52 deletions(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 5cbe3e324..38332434d 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -52,7 +52,7 @@ requires-python = ">=3.10" # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = ["ruff==0.14.4", { include-group = "test" }, { include-group = "types" }] -test = ["pytest-cov~=7.0", "pytest~=8.0"] +test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.18.2"] ################################################################################ @@ -134,19 +134,17 @@ requires = ["hatch-vcs", "hatchling", "packaging"] ## PyTest Configuration ################################################################################ [tool.pytest] +addopts = [ + "--import-mode=importlib", + # Coverage options + "--cov-config=pyproject.toml", + "--cov-report=xml", + "--cov=pact_cli", +] - [tool.pytest.ini_options] - addopts = [ - "--import-mode=importlib", - # Coverage options - "--cov-config=pyproject.toml", - "--cov-report=xml", - "--cov=pact_cli", - ] - - log_date_format = "%H:%M:%S" - log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" - log_level = "NOTSET" +log_date_format = "%H:%M:%S" +log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" +log_level = "NOTSET" ################################################################################ ## Coverage diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 977e8588f..48680a6cf 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -131,19 +131,17 @@ requires = ["hatch-vcs", "hatchling", "packaging", "cffi"] ## PyTest Configuration ################################################################################ [tool.pytest] +addopts = [ + "--import-mode=importlib", + # Coverage options + "--cov-config=pyproject.toml", + "--cov-report=xml", + "--cov=pact_ffi", +] - [tool.pytest.ini_options] - addopts = [ - "--import-mode=importlib", - # Coverage options - "--cov-config=pyproject.toml", - "--cov-report=xml", - "--cov=pact_ffi", - ] - - log_date_format = "%H:%M:%S" - log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" - log_level = "NOTSET" +log_date_format = "%H:%M:%S" +log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" +log_level = "NOTSET" ################################################################################ ## Coverage diff --git a/pyproject.toml b/pyproject.toml index 73f5aa2a9..18ff603d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -284,36 +284,35 @@ requires = ["hatch-vcs", "hatchling"] ################################################################################ [tool.pytest] - [tool.pytest.ini_options] - addopts = [ - "--import-mode=importlib", - # Coverage options - "--cov-config=pyproject.toml", - "--cov-report=xml", - "--cov=pact", - ] - asyncio_default_fixture_loop_scope = "session" - filterwarnings = [ - "ignore::DeprecationWarning:examples", - "ignore::DeprecationWarning:pact", - "ignore::DeprecationWarning:tests", - ] - pythonpath = "." - reruns = 3 +addopts = [ + "--import-mode=importlib", + # Coverage options + "--cov-config=pyproject.toml", + "--cov-report=xml", + "--cov=pact", +] +asyncio_default_fixture_loop_scope = "session" +filterwarnings = [ + "ignore::DeprecationWarning:examples", + "ignore::DeprecationWarning:pact", + "ignore::DeprecationWarning:tests", +] +pythonpath = "." +reruns = 3 - log_date_format = "%H:%M:%S" - log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" - log_level = "NOTSET" +log_date_format = "%H:%M:%S" +log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" +log_level = "NOTSET" - markers = [ - # Marker for tests that require a container - "container", +markers = [ + # Marker for tests that require a container + "container", - # Markers for the compatibility suite - "consumer", - "message", - "provider", - ] + # Markers for the compatibility suite + "consumer", + "message", + "provider", +] ################################################################################ ## Coverage From df8558b0ea9b7dd1a07dad9c34d9e58e38d8fee3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 31 Oct 2025 13:27:50 +1100 Subject: [PATCH 1101/1376] feat: add consumer_version method The new `consumer_version` replaces the (now deprecated) `consumer_versions` method within the broker source selector class. This replaces the explicit JSON-serialisation of an untyped dictionary into a much more structured call with documented keyword argument. Signed-off-by: JP-Ellis --- pyproject.toml | 1 + src/pact/verifier.py | 183 ++++++++++++++++++++++++++++++++++++++++- tests/test_verifier.py | 117 ++++++++++++++++++++++++++ 3 files changed, 297 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 18ff603d7..b9be33c4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ "pact-python-ffi~=0.4.0", # External dependencies "yarl~=1.0", + "typing-extensions~=4.0 ; python_version < '3.13'", ] [project.urls] diff --git a/src/pact/verifier.py b/src/pact/verifier.py index 35dcfe476..9c3b79340 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -76,6 +76,7 @@ import json import logging import os +import sys from collections.abc import Callable, Mapping from contextlib import nullcontext from datetime import date @@ -96,6 +97,12 @@ Unset, ) +if sys.version_info < (3, 13): + from typing_extensions import deprecated +else: + from warnings import deprecated + + if TYPE_CHECKING: from collections.abc import Iterable @@ -1386,8 +1393,8 @@ def __init__( self._provider_branch: str | None = None "The provider branch." - self._consumer_versions: list[str] | None = None - "List of consumer version regex patterns." + self._consumer_versions: list[str | dict[str, Any]] | None = None + "List of consumer version selectors." self._consumer_tags: list[str] | None = None "List of consumer tags to match." @@ -1442,11 +1449,174 @@ def provider_branch(self, branch: str) -> Self: self._verifier._branch = branch # type: ignore # noqa: PGH003, SLF001 return self + def consumer_version( # noqa: PLR0913 + self, + *, + consumer: str | None = None, + tag: str | None = None, + fallback_tag: str | None = None, + latest: bool | None = None, + deployed_or_released: Literal[True] | None = None, + deployed: Literal[True] | None = None, + released: Literal[True] | None = None, + environment: str | None = None, + main_branch: Literal[True] | None = None, + branch: str | None = None, + matching_branch: Literal[True] | None = None, + fallback_branch: str | None = None, + ) -> Self: + """ + Add a consumer version selector. + + This method allows specifying consumer version selection criteria to + filter which consumer pacts are verified from the broker. + + This function can be called multiple times to add multiple selectors. + The resulting selectors are combined with a logical OR, meaning that + pacts matching any of the selectors will be included in the + verification. + + Args: + consumer: + Application name to filter the results on. + + Allows a selector to only be applied to a certain consumer. + + tag: + The tag name(s) of the consumer versions to get the pacts for. + + This field is still supported but it is recommended to use the + `branch` in preference now. + + fallback_tag: + The name of the tag to fallback to if the specified `tag` does + not exist. + + This is useful when the consumer and provider use matching + branch names to coordinate the development of new features. This + field is still supported but it is recommended to use two + separate selectors - one with the main branch name and one with + the feature branch name. + + latest: + Only select the latest (if false, this selects all pacts for a + tag). + + Used in conjunction with the tag property. If a tag is + specified, and latest is true, then the latest pact for each of + the consumers with that tag will be returned. If a tag is + specified and the latest flag is not set to true, all the pacts + with the specified tag will be returned. + + deployed_or_released: + Applications that have been deployed or released. + + If the key is specified, can only be set to `True`. Returns the + pacts for all versions of the consumer that are currently + deployed or released and currently supported in any environment. + Use of this selector requires that the deployment of the + consumer application is recorded in the Pact Broker using the + `pact-broker record-deployment` or `pact-broker record-release` + CLI. + + deployed: + Applications that have been deployed. + + If the key is specified, can only be set to `True`. Returns the + pacts for all versions of the consumer that are currently + deployed to any environment. Use of this selector requires that + the deployment of the consumer application is recorded in the + Pact Broker using the `pact-broker record-deployment` CLI. + + released: + Applications that have been released. + + If the key is specified, can only be set to `True`. Returns the + pacts for all versions of the consumer that are released and + currently supported in any environment. Use of this selector + requires that the deployment of the consumer application is + recorded in the Pact Broker using the `pact-broker + record-release` CLI. + + environment: + Applications in a given environment. + + The name of the environment containing the consumer versions for + which to return the pacts. Used to further qualify `{ + "deployed": true }` or `{ "released": true }`. Normally, this + would not be needed, as it is recommended to verify the pacts + for all currently deployed/currently supported released + versions. + + main_branch: + Applications with the default branch set in the broker. + + If the key is specified, can only be set to `True`. Return the + pacts for the configured `mainBranch` of each consumer. Use of + this selector requires that the consumer has configured the + `mainBranch` property, and has set a branch name when publishing + the pacts. + + branch: + Applications with the given branch. + + The branch name of the consumer versions to get the pacts for. + Use of this selector requires that the consumer has configured a + branch name when publishing the pacts. + + matching_branch: + Applications that match the provider version branch sent during + verification. + + If the key is specified, can only be set to `True`. When true, + returns the latest pact for any branch with the same name as the + specified `provider_version_branch`. + + fallback_branch: + Fallback branch if branch doesn't exist. + + The name of the branch to fallback to if the specified branch + does not exist. Use of this property is discouraged as it may + allow a pact to pass on a feature branch while breaking + backwards compatibility with the main branch, which is generally + not desired. It is better to use two separate consumer version + selectors, one with the main branch name, and one with the + feature branch name, rather than use this property. + + Returns: + The builder instance for method chaining. + """ + if self._consumer_versions is None: + self._consumer_versions = [] + + param_mapping = [ + ("consumer", consumer), + ("tag", tag), + ("fallbackTag", fallback_tag), + ("latest", latest), + ("deployedOrReleased", deployed_or_released), + ("deployed", deployed), + ("released", released), + ("environment", environment), + ("mainBranch", main_branch), + ("branch", branch), + ("matchingBranch", matching_branch), + ("fallbackBranch", fallback_branch), + ] + + self._consumer_versions.append({ + key: value for key, value in param_mapping if value is not None + }) + return self + + @deprecated("Use `consumer_version` method with keyword arguments instead.") def consumer_versions(self, *versions: str) -> Self: """ Set the consumer versions. """ - self._consumer_versions = list(versions) + if self._consumer_versions is None: + self._consumer_versions = [] + self._consumer_versions.extend(versions) return self def consumer_tags(self, *tags: str) -> Self: @@ -1463,6 +1633,11 @@ def build(self) -> Verifier: Returns: The Verifier instance with the broker source added. """ + consumer_versions = [ + json.dumps(cv) if not isinstance(cv, str) else cv + for cv in (self._consumer_versions or []) + ] + self._verifier._broker_source_hook = ( # noqa: SLF001 lambda: pact_ffi.verifier_broker_source_with_selectors( self._verifier._handle, # noqa: SLF001 @@ -1474,7 +1649,7 @@ def build(self) -> Verifier: self._include_wip_since, self._provider_tags or [], self._provider_branch or self._verifier._branch, # noqa: SLF001 - self._consumer_versions or [], + consumer_versions, self._consumer_tags or [], ) ) diff --git a/tests/test_verifier.py b/tests/test_verifier.py index 0de481b0a..b9aa4fc3b 100644 --- a/tests/test_verifier.py +++ b/tests/test_verifier.py @@ -8,8 +8,11 @@ from __future__ import annotations +import json import re from pathlib import Path +from typing import Any +from unittest.mock import patch import pytest @@ -166,3 +169,117 @@ def test_logs(verifier: Verifier) -> None: def test_output(verifier: Verifier) -> None: output = verifier.output() assert output == "" + + +@pytest.mark.parametrize( + ("selector_calls", "expected_selectors"), + [ + pytest.param( + [{"consumer": "test-consumer"}], + [{"consumer": "test-consumer"}], + id="single_parameter", + ), + pytest.param( + [{"consumer": "test-consumer", "branch": "main", "latest": True}], + [{"consumer": "test-consumer", "branch": "main", "latest": True}], + id="multiple_parameters", + ), + pytest.param( + [{"deployed_or_released": True, "fallback_tag": "latest"}], + [{"deployedOrReleased": True, "fallbackTag": "latest"}], + id="camelcase_conversion", + ), + pytest.param( + [ + {"branch": "main", "latest": True}, + {"branch": "feature-branch", "latest": True}, + {"deployed": True}, + ], + [ + {"branch": "main", "latest": True}, + {"branch": "feature-branch", "latest": True}, + {"deployed": True}, + ], + id="multiple_selectors", + ), + pytest.param( + [ + { + "consumer": "test-consumer", + "tag": "v1.0", + "fallback_tag": "latest", + "latest": True, + "deployed_or_released": True, + "deployed": True, + "released": True, + "environment": "staging", + "main_branch": True, + "branch": "feature-123", + "matching_branch": True, + "fallback_branch": "develop", + } + ], + [ + { + "consumer": "test-consumer", + "tag": "v1.0", + "fallbackTag": "latest", + "latest": True, + "deployedOrReleased": True, + "deployed": True, + "released": True, + "environment": "staging", + "mainBranch": True, + "branch": "feature-123", + "matchingBranch": True, + "fallbackBranch": "develop", + } + ], + id="all_parameters", + ), + pytest.param( + [ + { + "consumer": "test-consumer", + "branch": "main", + "tag": None, + "latest": None, + } + ], + [{"consumer": "test-consumer", "branch": "main"}], + id="none_values_excluded", + ), + ], +) +def test_consumer_version( + verifier: Verifier, + selector_calls: list[dict[str, Any]], + expected_selectors: list[dict[str, Any]], +) -> None: + """Test consumer_version with various parameter combinations and selector counts.""" + with patch("pact_ffi.verifier_broker_source_with_selectors") as mock_ffi: + selector_builder = verifier.broker_source( + "http://localhost:8080", + selector=True, + ) + + # Call consumer_version for each set of parameters + for params in selector_calls: + selector_builder.consumer_version(**params) + + selector_builder.build() + # We call the hook explicitly to trigger the FFI call + assert verifier._broker_source_hook is not None # noqa: SLF001 + verifier._broker_source_hook() # noqa: SLF001 + + # Verify FFI was called with correct selectors + mock_ffi.assert_called_once() + selectors = [json.loads(s) for s in mock_ffi.call_args[0][9]] + + assert len(selectors) == len(expected_selectors) + for actual, expected in zip(selectors, expected_selectors, strict=True): + assert actual == expected + # For None value test case, verify excluded keys + if "tag" not in expected and "latest" not in expected: + assert "tag" not in actual + assert "latest" not in actual From f71f30437f29af7e128715a96793cad40ee1adfc Mon Sep 17 00:00:00 2001 From: Nikhil Arora Date: Thu, 13 Nov 2025 05:28:26 +0530 Subject: [PATCH 1102/1376] chore(tests): fix skipped tests on windows Fixes tests which were skipped on Windows. Replacing the use of subprocess with threads works much more reliably. Fixes: #639 --- tests/compatibility_suite/test_v1_provider.py | 105 ----------- tests/compatibility_suite/test_v2_provider.py | 18 -- .../test_v3_http_matching.py | 29 --- .../test_v3_message_producer.py | 73 -------- tests/compatibility_suite/test_v3_provider.py | 10 - .../test_v4_message_provider.py | 10 - tests/compatibility_suite/test_v4_provider.py | 10 - tests/test_match.py | 173 +++++++----------- 8 files changed, 70 insertions(+), 358 deletions(-) diff --git a/tests/compatibility_suite/test_v1_provider.py b/tests/compatibility_suite/test_v1_provider.py index 15821e601..050618ecc 100644 --- a/tests/compatibility_suite/test_v1_provider.py +++ b/tests/compatibility_suite/test_v1_provider.py @@ -5,7 +5,6 @@ from __future__ import annotations import logging -import sys import pytest from pytest_bdd import given, parsers, scenario @@ -45,10 +44,6 @@ ################################################################################ -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Verifying a simple HTTP request", @@ -57,10 +52,6 @@ def test_verifying_a_simple_http_request() -> None: """Verifying a simple HTTP request.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Verifying multiple Pact files", @@ -69,10 +60,6 @@ def test_verifying_multiple_pact_files() -> None: """Verifying multiple Pact files.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Incorrect request is made to provider", @@ -81,10 +68,6 @@ def test_incorrect_request_is_made_to_provider() -> None: """Incorrect request is made to provider.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @pytest.mark.container @scenario( "definition/features/V1/http_provider.feature", @@ -94,10 +77,6 @@ def test_verifying_a_simple_http_request_via_a_pact_broker() -> None: """Verifying a simple HTTP request via a Pact broker.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @pytest.mark.container @scenario( "definition/features/V1/http_provider.feature", @@ -107,10 +86,6 @@ def test_verifying_a_simple_http_request_via_a_pact_broker_with_publishing() -> """Verifying a simple HTTP request via a Pact broker with publishing.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @pytest.mark.container @scenario( "definition/features/V1/http_provider.feature", @@ -120,10 +95,6 @@ def test_verifying_multiple_pact_files_via_a_pact_broker() -> None: """Verifying multiple Pact files via a Pact broker.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @pytest.mark.container @scenario( "definition/features/V1/http_provider.feature", @@ -133,10 +104,6 @@ def test_incorrect_request_is_made_to_provider_via_a_pact_broker() -> None: """Incorrect request is made to provider via a Pact broker.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Verifying an interaction with a defined provider state", @@ -145,10 +112,6 @@ def test_verifying_an_interaction_with_a_defined_provider_state() -> None: """Verifying an interaction with a defined provider state.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Verifying an interaction with no defined provider state", @@ -157,10 +120,6 @@ def test_verifying_an_interaction_with_no_defined_provider_state() -> None: """Verifying an interaction with no defined provider state.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Verifying an interaction where the provider state callback fails", @@ -169,10 +128,6 @@ def test_verifying_an_interaction_where_the_provider_state_callback_fails() -> N """Verifying an interaction where the provider state callback fails.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Verifying an interaction where a provider state callback is not configured", @@ -181,10 +136,6 @@ def test_verifying_an_interaction_where_no_provider_state_callback_configured() """Verifying an interaction where a provider state callback is not configured.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Verifying a HTTP request with a request filter configured", @@ -193,10 +144,6 @@ def test_verifying_a_http_request_with_a_request_filter_configured() -> None: """Verifying a HTTP request with a request filter configured.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Verifies the response status code", @@ -205,10 +152,6 @@ def test_verifies_the_response_status_code() -> None: """Verifies the response status code.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Verifies the response headers", @@ -217,10 +160,6 @@ def test_verifies_the_response_headers() -> None: """Verifies the response headers.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Response with plain text body (positive case)", @@ -229,10 +168,6 @@ def test_response_with_plain_text_body_positive_case() -> None: """Response with plain text body (positive case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Response with plain text body (negative case)", @@ -241,10 +176,6 @@ def test_response_with_plain_text_body_negative_case() -> None: """Response with plain text body (negative case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Response with JSON body (positive case)", @@ -253,10 +184,6 @@ def test_response_with_json_body_positive_case() -> None: """Response with JSON body (positive case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Response with JSON body (negative case)", @@ -265,10 +192,6 @@ def test_response_with_json_body_negative_case() -> None: """Response with JSON body (negative case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Response with XML body (positive case)", @@ -277,10 +200,6 @@ def test_response_with_xml_body_positive_case() -> None: """Response with XML body (positive case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Response with XML body (negative case)", @@ -289,10 +208,6 @@ def test_response_with_xml_body_negative_case() -> None: """Response with XML body (negative case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Response with binary body (positive case)", @@ -301,10 +216,6 @@ def test_response_with_binary_body_positive_case() -> None: """Response with binary body (positive case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Response with binary body (negative case)", @@ -313,10 +224,6 @@ def test_response_with_binary_body_negative_case() -> None: """Response with binary body (negative case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Response with form post body (positive case)", @@ -325,10 +232,6 @@ def test_response_with_form_post_body_positive_case() -> None: """Response with form post body (positive case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Response with form post body (negative case)", @@ -337,10 +240,6 @@ def test_response_with_form_post_body_negative_case() -> None: """Response with form post body (negative case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Response with multipart body (positive case)", @@ -349,10 +248,6 @@ def test_response_with_multipart_body_positive_case() -> None: """Response with multipart body (positive case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V1/http_provider.feature", "Response with multipart body (negative case)", diff --git a/tests/compatibility_suite/test_v2_provider.py b/tests/compatibility_suite/test_v2_provider.py index 18d5d7acf..61170e89f 100644 --- a/tests/compatibility_suite/test_v2_provider.py +++ b/tests/compatibility_suite/test_v2_provider.py @@ -5,9 +5,7 @@ from __future__ import annotations import logging -import sys -import pytest from pytest_bdd import given, parsers, scenario from tests.compatibility_suite.util import parse_horizontal_table @@ -30,10 +28,6 @@ ################################################################################ -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V2/http_provider.feature", "Supports matching rules for the response headers (positive case)", @@ -44,10 +38,6 @@ def test_supports_matching_rules_for_the_response_headers_positive_case() -> Non """ -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V2/http_provider.feature", "Supports matching rules for the response headers (negative case)", @@ -58,10 +48,6 @@ def test_supports_matching_rules_for_the_response_headers_negative_case() -> Non """ -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V2/http_provider.feature", "Verifies the response body (positive case)", @@ -72,10 +58,6 @@ def test_verifies_the_response_body_positive_case() -> None: """ -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V2/http_provider.feature", "Verifies the response body (negative case)", diff --git a/tests/compatibility_suite/test_v3_http_matching.py b/tests/compatibility_suite/test_v3_http_matching.py index 6de96aa2b..e01c2569e 100644 --- a/tests/compatibility_suite/test_v3_http_matching.py +++ b/tests/compatibility_suite/test_v3_http_matching.py @@ -3,7 +3,6 @@ from __future__ import annotations import re -import sys from typing import TYPE_CHECKING import pytest @@ -32,10 +31,6 @@ ################################################################################ -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/http_matching.feature", "Comparing accept headers where the actual has additional parameters", @@ -44,10 +39,6 @@ def test_comparing_accept_headers_where_the_actual_has_additional_parameters() - """Comparing accept headers where the actual has additional parameters.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/http_matching.feature", "Comparing accept headers where the actual has is missing a value", @@ -56,10 +47,6 @@ def test_comparing_accept_headers_where_the_actual_has_is_missing_a_value() -> N """Comparing accept headers where the actual has is missing a value.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/http_matching.feature", "Comparing content type headers where the actual has a charset", @@ -68,10 +55,6 @@ def test_comparing_content_type_headers_where_the_actual_has_a_charset() -> None """Comparing content type headers where the actual has a charset.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/http_matching.feature", "Comparing content type headers where the actual has a different charset", @@ -82,10 +65,6 @@ def test_comparing_content_type_headers_where_the_actual_has_a_different_charset """Comparing content type headers where the actual has a different charset.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/http_matching.feature", "Comparing content type headers where the actual is missing a charset", @@ -94,10 +73,6 @@ def test_comparing_content_type_headers_where_the_actual_is_missing_a_charset() """Comparing content type headers where the actual is missing a charset.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/http_matching.feature", "Comparing content type headers where they have the same charset", @@ -106,10 +81,6 @@ def test_comparing_content_type_headers_where_they_have_the_same_charset() -> No """Comparing content type headers where they have the same charset.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/http_matching.feature", "Comparing content type headers which are equal", diff --git a/tests/compatibility_suite/test_v3_message_producer.py b/tests/compatibility_suite/test_v3_message_producer.py index b157529ca..fe8ec008e 100644 --- a/tests/compatibility_suite/test_v3_message_producer.py +++ b/tests/compatibility_suite/test_v3_message_producer.py @@ -5,7 +5,6 @@ import json import logging import re -import sys from pathlib import Path from typing import TYPE_CHECKING, Any @@ -45,10 +44,6 @@ logger = logging.getLogger(__name__) -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Incorrect message is generated by the provider", @@ -57,10 +52,6 @@ def test_incorrect_message_is_generated_by_the_provider() -> None: """Incorrect message is generated by the provider.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Message with JSON body (negative case)", @@ -69,10 +60,6 @@ def test_message_with_json_body_negative_case() -> None: """Message with JSON body (negative case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Message with JSON body (positive case)", @@ -81,10 +68,6 @@ def test_message_with_json_body_positive_case() -> None: """Message with JSON body (positive case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Message with XML body (negative case)", @@ -93,10 +76,6 @@ def test_message_with_xml_body_negative_case() -> None: """Message with XML body (negative case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Message with XML body (positive case)", @@ -105,10 +84,6 @@ def test_message_with_xml_body_positive_case() -> None: """Message with XML body (positive case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Message with binary body (negative case)", @@ -117,10 +92,6 @@ def test_message_with_binary_body_negative_case() -> None: """Message with binary body (negative case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Message with binary body (positive case)", @@ -129,10 +100,6 @@ def test_message_with_binary_body_positive_case() -> None: """Message with binary body (positive case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Message with plain text body (negative case)", @@ -141,10 +108,6 @@ def test_message_with_plain_text_body_negative_case() -> None: """Message with plain text body (negative case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Message with plain text body (positive case)", @@ -153,10 +116,6 @@ def test_message_with_plain_text_body_positive_case() -> None: """Message with plain text body (positive case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Supports matching rules for the message body (negative case)", @@ -165,10 +124,6 @@ def test_supports_matching_rules_for_the_message_body_negative_case() -> None: """Supports matching rules for the message body (negative case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Supports matching rules for the message body (positive case)", @@ -177,10 +132,6 @@ def test_supports_matching_rules_for_the_message_body_positive_case() -> None: """Supports matching rules for the message body (positive case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Supports matching rules for the message metadata (negative case)", @@ -189,10 +140,6 @@ def test_supports_matching_rules_for_the_message_metadata_negative_case() -> Non """Supports matching rules for the message metadata (negative case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Supports matching rules for the message metadata (positive case)", @@ -201,10 +148,6 @@ def test_supports_matching_rules_for_the_message_metadata_positive_case() -> Non """Supports matching rules for the message metadata (positive case).""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @pytest.mark.skip("Currently unable to implement") @scenario( "definition/features/V3/message_provider.feature", @@ -214,10 +157,6 @@ def test_supports_messages_with_body_formatted_for_the_kafka_schema_registry() - """Supports messages with body formatted for the Kafka schema registry.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Verifies the message metadata", @@ -226,10 +165,6 @@ def test_verifies_the_message_metadata() -> None: """Verifies the message metadata.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Verifying a simple message", @@ -238,10 +173,6 @@ def test_verifying_a_simple_message() -> None: """Verifying a simple message.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Verifying an interaction with a defined provider state", @@ -250,10 +181,6 @@ def test_verifying_an_interaction_with_a_defined_provider_state() -> None: """Verifying an interaction with a defined provider state.""" -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/message_provider.feature", "Verifying multiple Pact files", diff --git a/tests/compatibility_suite/test_v3_provider.py b/tests/compatibility_suite/test_v3_provider.py index 864f118c4..5470bd531 100644 --- a/tests/compatibility_suite/test_v3_provider.py +++ b/tests/compatibility_suite/test_v3_provider.py @@ -5,9 +5,7 @@ from __future__ import annotations import logging -import sys -import pytest from pytest_bdd import given, parsers, scenario from tests.compatibility_suite.util import parse_horizontal_table @@ -33,10 +31,6 @@ ################################################################################ -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/http_provider.feature", "Verifying an interaction with multiple defined provider states", @@ -47,10 +41,6 @@ def test_verifying_an_interaction_with_multiple_defined_provider_states() -> Non """ -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V3/http_provider.feature", "Verifying an interaction with a provider state with parameters", diff --git a/tests/compatibility_suite/test_v4_message_provider.py b/tests/compatibility_suite/test_v4_message_provider.py index 0a4e366ae..3be1dd9ee 100644 --- a/tests/compatibility_suite/test_v4_message_provider.py +++ b/tests/compatibility_suite/test_v4_message_provider.py @@ -5,9 +5,7 @@ from __future__ import annotations import logging -import sys -import pytest from pytest_bdd import scenario from tests.compatibility_suite.util.provider import ( @@ -30,10 +28,6 @@ ################################################################################ -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V4/message_provider.feature", "Verifying a pending message interaction", @@ -44,10 +38,6 @@ def test_verifying_a_pending_message_interaction() -> None: """ -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V4/message_provider.feature", "Verifying a message interaction with comments", diff --git a/tests/compatibility_suite/test_v4_provider.py b/tests/compatibility_suite/test_v4_provider.py index 9395586de..c466ebcfb 100644 --- a/tests/compatibility_suite/test_v4_provider.py +++ b/tests/compatibility_suite/test_v4_provider.py @@ -5,9 +5,7 @@ from __future__ import annotations import logging -import sys -import pytest from pytest_bdd import given, parsers, scenario from tests.compatibility_suite.util import parse_horizontal_table @@ -35,10 +33,6 @@ ################################################################################ -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V4/http_provider.feature", "Verifying a pending HTTP interaction", @@ -49,10 +43,6 @@ def test_verifying_a_pending_http_interaction() -> None: """ -@pytest.mark.skipif( - sys.platform.startswith("win"), - reason="See pact-foundation/pact-python#639", -) @scenario( "definition/features/V4/http_provider.feature", "Verifying a HTTP interaction with comments", diff --git a/tests/test_match.py b/tests/test_match.py index f533fd6ab..e078cc42b 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -6,20 +6,19 @@ import logging import re -import subprocess -import sys import time from contextlib import contextmanager from datetime import datetime from pathlib import Path from random import randint, uniform from threading import Thread -from typing import TYPE_CHECKING, NoReturn +from typing import TYPE_CHECKING import requests from flask import Flask, Response, make_response from yarl import URL +import pact._util from pact import Pact, Verifier, generate, match if TYPE_CHECKING: @@ -29,43 +28,24 @@ @contextmanager -def start_provider() -> Generator[URL, None, None]: # noqa: C901 +def start_provider() -> Generator[URL, None, None]: """ - Start the provider app. + Start the provider app using a daemon thread. + + Yields: + The base URL of the running Flask server. """ - process = subprocess.Popen( # noqa: S603 - [ - sys.executable, - Path(__file__), - ], - cwd=Path.cwd(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="utf-8", - ) + hostname = "127.0.0.1" + port = pact._util.find_free_port() # noqa: SLF001 - pattern = re.compile(r" \* Running on (?P[^ ]+)") - while True: - if process.poll() is not None: - logger.error("Provider process exited with code %d", process.returncode) - logger.error( - "Provider stdout: %s", process.stdout.read() if process.stdout else "" - ) - logger.error( - "Provider stderr: %s", process.stderr.read() if process.stderr else "" - ) - msg = f"Provider process exited with code {process.returncode}" - raise RuntimeError(msg) - if ( - process.stderr - and (line := process.stderr.readline()) - and (match := pattern.match(line)) - ): - break - time.sleep(0.1) + Thread( + target=app.run, + kwargs={"host": hostname, "port": port, "use_reloader": False}, + daemon=True, + ).start() + + url = URL(f"http://{hostname}:{port}") - url = URL(match.group("url")) - logger.debug("Provider started on %s", url) for _ in range(50): try: response = requests.get(str(url / "_test" / "ping"), timeout=1) @@ -73,82 +53,69 @@ def start_provider() -> Generator[URL, None, None]: # noqa: C901 break except (requests.RequestException, AssertionError): time.sleep(0.1) - continue else: msg = "Failed to ping provider" raise RuntimeError(msg) - def redirect() -> NoReturn: - while True: - if process.stdout: - while line := process.stdout.readline(): - logger.debug("Provider stdout: %s", line.strip()) - if process.stderr: - while line := process.stderr.readline(): - logger.debug("Provider stderr: %s", line.strip()) + yield url + + +app = Flask(__name__) + + +@app.route("/path/to/") +def hello_world(test_id: int) -> Response: + random_regex_matches = "1-8 digits: 12345678, 1-8 random letters abcdefgh" + response = make_response({ + "response": { + "id": test_id, + "regexMatches": "must end with 'hello world'", + "randomRegexMatches": random_regex_matches, + "integerMatches": test_id, + "decimalMatches": round(uniform(0, 9), 3), # noqa: S311 + "booleanMatches": True, + "randomIntegerMatches": randint(1, 100), # noqa: S311 + "randomDecimalMatches": round(uniform(0, 9), 1), # noqa: S311 + "randomStringMatches": "hi there", + "includeMatches": "hello world", + "includeWithGeneratorMatches": "say 'hello world' for me", + "minMaxArrayMatches": [ + round(uniform(0, 9), 1) # noqa: S311 + for _ in range(randint(3, 5)) # noqa: S311 + ], + "arrayContainingMatches": [randint(1, 100), randint(1, 100)], # noqa: S311 + "numbers": { + "intMatches": 42, + "floatMatches": 3.1415, + "intGeneratorMatches": randint(1, 100), # noqa: S311, + "decimalGeneratorMatches": round(uniform(10, 99), 2), # noqa: S311 + }, + "dateMatches": "1999-12-31", + "randomDateMatches": "1999-12-31", + "timeMatches": "12:34:56", + "timestampMatches": datetime.now().isoformat(), # noqa: DTZ005 + "nullMatches": None, + "eachKeyMatches": { + "id_1": { + "name": "John Doe", + }, + "id_2": { + "name": "Jane Doe", + }, + }, + } + }) + response.headers["SpecialHeader"] = "Special: Hi" + return response - thread = Thread(target=redirect, daemon=True) - thread.start() - try: - yield url - finally: - process.terminate() +@app.get("/_test/ping") +def ping() -> str: + """Simple ping endpoint for testing.""" + return "pong" if __name__ == "__main__": - app = Flask(__name__) - - @app.route("/path/to/") - def hello_world(test_id: int) -> Response: - random_regex_matches = "1-8 digits: 12345678, 1-8 random letters abcdefgh" - response = make_response({ - "response": { - "id": test_id, - "regexMatches": "must end with 'hello world'", - "randomRegexMatches": random_regex_matches, - "integerMatches": test_id, - "decimalMatches": round(uniform(0, 9), 3), # noqa: S311 - "booleanMatches": True, - "randomIntegerMatches": randint(1, 100), # noqa: S311 - "randomDecimalMatches": round(uniform(0, 9), 1), # noqa: S311 - "randomStringMatches": "hi there", - "includeMatches": "hello world", - "includeWithGeneratorMatches": "say 'hello world' for me", - "minMaxArrayMatches": [ - round(uniform(0, 9), 1) # noqa: S311 - for _ in range(randint(3, 5)) # noqa: S311 - ], - "arrayContainingMatches": [randint(1, 100), randint(1, 100)], # noqa: S311 - "numbers": { - "intMatches": 42, - "floatMatches": 3.1415, - "intGeneratorMatches": randint(1, 100), # noqa: S311, - "decimalGeneratorMatches": round(uniform(10, 99), 2), # noqa: S311 - }, - "dateMatches": "1999-12-31", - "randomDateMatches": "1999-12-31", - "timeMatches": "12:34:56", - "timestampMatches": datetime.now().isoformat(), # noqa: DTZ005 - "nullMatches": None, - "eachKeyMatches": { - "id_1": { - "name": "John Doe", - }, - "id_2": { - "name": "Jane Doe", - }, - }, - } - }) - response.headers["SpecialHeader"] = "Special: Hi" - return response - - @app.get("/_test/ping") - def ping() -> str: - """Simple ping endpoint for testing.""" - return "pong" - app.run() From 3db2eb97dcc9c879b41d9e0d3c43c2006ef0f5ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:44:43 +1100 Subject: [PATCH 1103/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.39.2 (#1331) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index df5c00f94..46ca6eca5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -71,7 +71,7 @@ repos: )$ - repo: https://github.com/crate-ci/typos - rev: v1.39.0 + rev: v1.39.2 hooks: - id: typos exclude: | From 39b9b3b9dc602e04ed1219787640ff17ce74814e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:45:31 +1100 Subject: [PATCH 1104/1376] chore(deps): update ruff to v0.14.5 (#1332) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 46ca6eca5..7acd7c8d0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.4 + rev: v0.14.5 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 38332434d..9c3b90563 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.4", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.5", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.18.2"] From c3041ce8147847c3af41e0a0006cc033980fb3f7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:47:09 +1100 Subject: [PATCH 1105/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.5 (#1329) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7acd7c8d0..d75b4edb3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.4 + rev: v2.3.5 hooks: - id: biome-check From c3eefb67256fd42d65795d680a02b0fd271dffba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:47:21 +1100 Subject: [PATCH 1106/1376] chore(deps): update pypa/cibuildwheel action to v3.3.0 (#1330) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 54297ea91..f3904b57d 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@9c00cb4f6b517705a3794b22395aedc36257242c # v3.2.1 + uses: pypa/cibuildwheel@63fd63b352a9a8bdcc24791c9dbee952ee9a8abc # v3.3.0 with: package-dir: pact-python-cli env: diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 2b7ba11f8..3d5d6dc79 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@9c00cb4f6b517705a3794b22395aedc36257242c # v3.2.1 + uses: pypa/cibuildwheel@63fd63b352a9a8bdcc24791c9dbee952ee9a8abc # v3.3.0 with: package-dir: pact-python-ffi env: From fe233a9e231622cdc534168c152f00da1289d9b1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:11:28 +1100 Subject: [PATCH 1107/1376] chore(deps): update taiki-e/install-action action to v2.62.52 (#1334) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index f3904b57d..5dc7b24b4 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 + uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 3d5d6dc79..f1a139225 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 + uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bff9141c1..b5c5cad17 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 + uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52 with: tool: git-cliff,typos From 515b084a13c25ced78b484b91bb18e2699fbcf63 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 02:16:54 +0000 Subject: [PATCH 1108/1376] chore(deps): update actions/checkout action to v5.0.1 (#1335) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 6 +++--- .github/workflows/build-ffi.yml | 6 +++--- .github/workflows/build.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 5dc7b24b4..c8ab7130c 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 @@ -94,7 +94,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 @@ -135,7 +135,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index f1a139225..5dd94b396 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 @@ -94,7 +94,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 @@ -136,7 +136,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5c5cad17..d44488e73 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 @@ -104,7 +104,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 289612b67..cb0a5f608 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index d238dda01..e6e6924fd 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Synchronize labels uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd9612534..e0c3cde6b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 submodules: true @@ -149,7 +149,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 @@ -192,7 +192,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 @@ -224,7 +224,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 @@ -257,7 +257,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 @@ -285,7 +285,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Cache prek uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 From 9a92e0df73a8a59ccd04991b91547ef9bfe3b994 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 19 Nov 2025 10:51:51 +1100 Subject: [PATCH 1109/1376] chore(ci): update macos runners Signed-off-by: JP-Ellis --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index c8ab7130c..c15d2e332 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -85,7 +85,7 @@ jobs: fail-fast: false matrix: include: - - os: macos-13 + - os: macos-15-intel - os: macos-latest - os: ubuntu-24.04-arm - os: ubuntu-latest diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 5dd94b396..d039acd66 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -85,7 +85,7 @@ jobs: fail-fast: false matrix: include: - - os: macos-13 + - os: macos-15-intel - os: macos-latest - os: ubuntu-24.04-arm - os: ubuntu-latest From 039ca7e22c689e6887ddc9d6c0f816f3774d82a1 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 18 Nov 2025 20:28:54 +1100 Subject: [PATCH 1110/1376] chore: remove unused pytest config Signed-off-by: JP-Ellis --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b9be33c4c..32896a41c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -298,8 +298,6 @@ filterwarnings = [ "ignore::DeprecationWarning:pact", "ignore::DeprecationWarning:tests", ] -pythonpath = "." -reruns = 3 log_date_format = "%H:%M:%S" log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" From 8e205a60366881ab3763122d3c3a890a1264873a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 19 Nov 2025 10:27:46 +1100 Subject: [PATCH 1111/1376] chore: remove ruff sub-configs These can be managed more easily by apply configs to `test_*.py` files. Signed-off-by: JP-Ellis --- examples/.ruff.toml | 13 ------------- examples/http/__init__.py | 2 +- examples/http/aiohttp_and_flask/pyproject.toml | 16 +++++++--------- .../http/requests_and_fastapi/pyproject.toml | 17 ++++++++--------- pact-python-cli/tests/.ruff.toml | 11 ----------- pact-python-cli/tests/__init__.py | 0 pact-python-ffi/tests/.ruff.toml | 11 ----------- pact-python-ffi/tests/__init__.py | 0 pyproject.toml | 13 ++++++++++++- tests/.ruff.toml | 4 +++- 10 files changed, 31 insertions(+), 56 deletions(-) delete mode 100644 examples/.ruff.toml delete mode 100644 pact-python-cli/tests/.ruff.toml delete mode 100644 pact-python-cli/tests/__init__.py delete mode 100644 pact-python-ffi/tests/.ruff.toml delete mode 100644 pact-python-ffi/tests/__init__.py diff --git a/examples/.ruff.toml b/examples/.ruff.toml deleted file mode 100644 index 2de3b8854..000000000 --- a/examples/.ruff.toml +++ /dev/null @@ -1,13 +0,0 @@ -#:schema https://www.schemastore.org/ruff.json -extend = "../pyproject.toml" - -[lint.per-file-ignores] -"test_*.py" = [ - "D103", # Require docstring in public function - "D104", # Require docstring in public package - "INP001", # Forbid implicit namespaces - "PLR2004", # Forbid Magic Numbers - "PLR2004", # Forbid magic values - "S101", # Forbid assert statements - "TID252", # Require absolute imports -] diff --git a/examples/http/__init__.py b/examples/http/__init__.py index b1a036caf..6e031999e 100644 --- a/examples/http/__init__.py +++ b/examples/http/__init__.py @@ -1 +1 @@ -# noqa: A005, D104 +# noqa: D104 diff --git a/examples/http/aiohttp_and_flask/pyproject.toml b/examples/http/aiohttp_and_flask/pyproject.toml index 755b39e02..a6d15be82 100644 --- a/examples/http/aiohttp_and_flask/pyproject.toml +++ b/examples/http/aiohttp_and_flask/pyproject.toml @@ -9,20 +9,18 @@ requires-python = ">=3.10" version = "1.0.0" [dependency-groups] -test = ["pact-python", "pytest-asyncio~=1.0", "pytest~=8.0"] +test = ["pact-python", "pytest-asyncio~=1.0", "pytest~=9.0"] [tool.uv.sources] pact-python = { path = "../../../" } [tool.ruff] -extend = "../../.ruff.toml" +extend = "../../../pyproject.toml" [tool.pytest] +addopts = ["--import-mode=importlib"] +asyncio_default_fixture_loop_scope = "session" - [tool.pytest.ini_options] - addopts = ["--import-mode=importlib"] - asyncio_default_fixture_loop_scope = "session" - - log_date_format = "%H:%M:%S" - log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" - log_level = "NOTSET" +log_date_format = "%H:%M:%S" +log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" +log_level = "NOTSET" diff --git a/examples/http/requests_and_fastapi/pyproject.toml b/examples/http/requests_and_fastapi/pyproject.toml index 66d4a2487..fd3953a2e 100644 --- a/examples/http/requests_and_fastapi/pyproject.toml +++ b/examples/http/requests_and_fastapi/pyproject.toml @@ -10,19 +10,18 @@ version = "1.0.0" [dependency-groups] -test = ["pact-python", "pytest~=8.0", "uvicorn~=0.29"] +test = ["pact-python", "pytest~=9.0", "uvicorn~=0.29"] + [tool.uv.sources] pact-python = { path = "../../../" } [tool.ruff] -extend = "../../.ruff.toml" +extend = "../../../pyproject.toml" [tool.pytest] +addopts = ["--import-mode=importlib"] +log_date_format = "%H:%M:%S" +log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" +log_level = "NOTSET" - [tool.pytest.ini_options] - addopts = ["--import-mode=importlib"] - asyncio_default_fixture_loop_scope = "session" - - log_date_format = "%H:%M:%S" - log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" - log_level = "NOTSET" +asyncio_default_fixture_loop_scope = "session" diff --git a/pact-python-cli/tests/.ruff.toml b/pact-python-cli/tests/.ruff.toml deleted file mode 100644 index e88fc30b9..000000000 --- a/pact-python-cli/tests/.ruff.toml +++ /dev/null @@ -1,11 +0,0 @@ -#:schema https://www.schemastore.org/ruff.json -extend = "../pyproject.toml" - -[lint] -ignore = [ - "D103", # Require docstring in public function - "D104", # Require docstring in public package - "PLR2004", # Forbid Magic Numbers - "S101", # Forbid assert statements - "TID252", # Require absolute imports -] diff --git a/pact-python-cli/tests/__init__.py b/pact-python-cli/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pact-python-ffi/tests/.ruff.toml b/pact-python-ffi/tests/.ruff.toml deleted file mode 100644 index e88fc30b9..000000000 --- a/pact-python-ffi/tests/.ruff.toml +++ /dev/null @@ -1,11 +0,0 @@ -#:schema https://www.schemastore.org/ruff.json -extend = "../pyproject.toml" - -[lint] -ignore = [ - "D103", # Require docstring in public function - "D104", # Require docstring in public package - "PLR2004", # Forbid Magic Numbers - "S101", # Forbid assert statements - "TID252", # Require absolute imports -] diff --git a/pact-python-ffi/tests/__init__.py b/pact-python-ffi/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pyproject.toml b/pyproject.toml index 32896a41c..bd86d49bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -284,7 +284,6 @@ requires = ["hatch-vcs", "hatchling"] ## PyTest Configuration ################################################################################ [tool.pytest] - addopts = [ "--import-mode=importlib", # Coverage options @@ -363,6 +362,18 @@ extend-exclude = ["src/pact/v2/*", "tests/v2/*", "examples/v2/*"] [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" + [tool.ruff.lint.per-file-ignores] + "test_*.py" = [ + "D103", # Require docstring in public function + "D104", # Require docstring in public package + "INP001", # Forbid implicit namespaces + "PLR2004", # Forbid magic values + "RUF018", # Forbid assignment in assertions + "S101", # Forbid assert statements + "TID252", # Require absolute imports + ] + + [tool.ruff.format] docstring-code-format = true preview = true diff --git a/tests/.ruff.toml b/tests/.ruff.toml index cc96e2421..5bb2e803d 100644 --- a/tests/.ruff.toml +++ b/tests/.ruff.toml @@ -1,12 +1,14 @@ #:schema https://www.schemastore.org/ruff.json extend = "../pyproject.toml" +# We have a number of helper files which contain assertions/magic values, etc. + [lint] ignore = [ "D103", # Require docstring in public function "D104", # Require docstring in public package "INP001", # Forbid implicit namespaces - "PLR2004", # Forbid Magic Numbers + "PLR2004", # Forbid magic values "RUF018", # Forbid assignment in assertions "S101", # Forbid assert statements "TID252", # Require absolute imports From 5a373fea68e745a26c745e33d7c354caef1d01a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 00:30:29 +0000 Subject: [PATCH 1112/1376] chore(deps): update dependency mkdocs-material to v9.7.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd86d49bb..d7986f16d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,7 @@ docs = [ "mkdocs-github-admonitions-plugin==0.1.1", "mkdocs-literate-nav==0.6.2", "mkdocs-llmstxt==0.4.0", - "mkdocs-material[recommended,git,imaging]==9.6.21", + "mkdocs-material[recommended,git,imaging]==9.7.0", "mkdocs-section-index==0.3.10", "mkdocs==1.6.1", "mkdocstrings[python]==0.30.1", From 4905a163ac49acf4fe1ac937f343162f794644d4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 00:30:26 +0000 Subject: [PATCH 1113/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.6 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d75b4edb3..77824742d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.5 + rev: v2.3.6 hooks: - id: biome-check From 4e8e049f4c9f33c9e87c7b23d5f18bfef847e3a4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 00:30:23 +0000 Subject: [PATCH 1114/1376] chore(deps): update dependency griffe-pydantic to v1.1.8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d7986f16d..4cf063cb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ dev = [ docs = [ "griffe-generics==1.0.13", "griffe-inherited-method-crossrefs==0.0.1.4", - "griffe-pydantic==1.1.7", + "griffe-pydantic==1.1.8", "griffe-warnings-deprecated==1.1.0", "mkdocs-gen-files==0.5.0", "mkdocs-github-admonitions-plugin==0.1.1", From fb5dd23a96aed3b452c378080456e70c52ba3a3b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 00:30:33 +0000 Subject: [PATCH 1115/1376] chore(deps): update dependency ruff to v0.14.5 --- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 48680a6cf..a674d855a 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.13.3", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.5", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=8.0"] types = ["mypy==1.18.2", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 4cf063cb6..a2d37ce37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.13.3", + "ruff==0.14.5", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From 98d1c19f1b807ccfc9339678f6ee4167c3fb0f55 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 18 Nov 2025 23:30:36 +0000 Subject: [PATCH 1116/1376] chore(deps): update dependency pytest to v9 --- examples/http/aiohttp_and_flask/pyproject.toml | 3 ++- examples/http/requests_and_fastapi/pyproject.toml | 7 ++++--- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 10 ++++++---- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/examples/http/aiohttp_and_flask/pyproject.toml b/examples/http/aiohttp_and_flask/pyproject.toml index a6d15be82..c783c3f1f 100644 --- a/examples/http/aiohttp_and_flask/pyproject.toml +++ b/examples/http/aiohttp_and_flask/pyproject.toml @@ -18,7 +18,8 @@ pact-python = { path = "../../../" } extend = "../../../pyproject.toml" [tool.pytest] -addopts = ["--import-mode=importlib"] +addopts = ["--import-mode=importlib"] + asyncio_default_fixture_loop_scope = "session" log_date_format = "%H:%M:%S" diff --git a/examples/http/requests_and_fastapi/pyproject.toml b/examples/http/requests_and_fastapi/pyproject.toml index fd3953a2e..be469ec87 100644 --- a/examples/http/requests_and_fastapi/pyproject.toml +++ b/examples/http/requests_and_fastapi/pyproject.toml @@ -19,9 +19,10 @@ pact-python = { path = "../../../" } extend = "../../../pyproject.toml" [tool.pytest] -addopts = ["--import-mode=importlib"] +addopts = ["--import-mode=importlib"] + +asyncio_default_fixture_loop_scope = "session" + log_date_format = "%H:%M:%S" log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" log_level = "NOTSET" - -asyncio_default_fixture_loop_scope = "session" diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index a674d855a..c674596f6 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -42,7 +42,7 @@ dependencies = ["cffi~=2.0"] [dependency-groups] dev = ["ruff==0.14.5", { include-group = "test" }, { include-group = "types" }] -test = ["pytest-cov~=7.0", "pytest~=8.0"] +test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.18.2", "typing-extensions~=4.0"] ################################################################################ diff --git a/pyproject.toml b/pyproject.toml index a2d37ce37..708552168 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,7 @@ example = [ "protobuf~=6.0", "pydantic~=2.0", "pytest-cov~=7.0", - "pytest~=8.0", + "pytest~=9.0", "testcontainers~=4.0", "uvicorn[standard]~=0.0", ] @@ -116,7 +116,7 @@ test = [ "pytest-bdd~=8.0", "pytest-cov~=7.0", "pytest-rerunfailures~=16.0", - "pytest~=8.0", + "pytest~=9.0", "requests~=2.0", "testcontainers~=4.0", ] @@ -134,7 +134,7 @@ example-v2 = [ "fastapi~=0.0", "flask[async]~=3.0", "pytest-cov~=7.0", - "pytest~=8.0", + "pytest~=9.0", "testcontainers~=4.0", "uvicorn[standard]~=0.0", ] @@ -143,7 +143,7 @@ test-v2 = [ "httpx~=0.0", "mock~=5.0", "pytest-cov~=7.0", - "pytest~=8.0", + "pytest~=9.0", "uvicorn[standard]~=0.0", ] @@ -291,7 +291,9 @@ addopts = [ "--cov-report=xml", "--cov=pact", ] + asyncio_default_fixture_loop_scope = "session" + filterwarnings = [ "ignore::DeprecationWarning:examples", "ignore::DeprecationWarning:pact", From 4bf428c100737cc2d4dbaa390a49e07f2ded3b82 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 19 Nov 2025 20:19:59 +1100 Subject: [PATCH 1117/1376] feat: add content type matcher This matcher validates the body by content type only, typically through the magic bytes. Signed-off-by: JP-Ellis --- src/pact/match/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/pact/match/__init__.py b/src/pact/match/__init__.py index 8ad083303..c6af3ceda 100644 --- a/src/pact/match/__init__.py +++ b/src/pact/match/__init__.py @@ -968,6 +968,24 @@ def type( return GenericMatcher("type", value, min=min, max=max, generator=generator) +def content_type(content_type: builtins.str) -> AbstractMatcher[Any]: + """ + Match a value by content type. + + Unlike other matchers, this matcher does not take a `value` argument, as it + is typically used to match binary data (e.g., images, files) where the + actual content is impractical to specify. + + Args: + content_type: + Content type to match (e.g., "image/jpeg", "application/pdf"). + + Returns: + Matcher for the given content type. + """ + return GenericMatcher("contentType", value=content_type) + + def like( value: _T, /, From 7be0ff61cd8124d84d684e22146c03e34ddb5b99 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 19 Nov 2025 20:22:39 +1100 Subject: [PATCH 1118/1376] feat: add 'and' matcher The `AndMatcher` allows for matchers to be combined. This is achieved in practice through the overloaded `&` operator. Signed-off-by: JP-Ellis --- src/pact/match/matcher.py | 108 ++++++++++++++++++++++++++++++++++---- 1 file changed, 99 insertions(+), 9 deletions(-) diff --git a/src/pact/match/matcher.py b/src/pact/match/matcher.py index 4d845df43..18526055b 100644 --- a/src/pact/match/matcher.py +++ b/src/pact/match/matcher.py @@ -71,6 +71,43 @@ def to_matching_rule(self) -> dict[str, Any]: The matcher as a matching rule. """ + def has_value(self) -> bool: + """ + Check if the matcher has a value. + + If a value is present, it _must_ be accessible via the `value` + attribute. + + Returns: + True if the matcher has a value, otherwise False. + """ + return not isinstance(getattr(self, "value", UNSET), Unset) + + def __and__(self, other: object) -> AndMatcher[Any]: + """ + Combine two matchers using a logical AND. + + This allows for combining multiple matchers into a single matcher that + requires all conditions to be met. + + Only a single example value is supported when combining matchers. The + first value found will be used. + + Args: + other: + The other matcher to combine with. + + Returns: + An `AndMatcher` that combines both matchers. + """ + if isinstance(self, AndMatcher) and isinstance(other, AbstractMatcher): + return AndMatcher(*self._matchers, other) # type: ignore[attr-defined] + if isinstance(other, AndMatcher): + return AndMatcher(self, *other._matchers) # type: ignore[attr-defined] + if isinstance(other, AbstractMatcher): + return AndMatcher(self, other) + return NotImplemented + class GenericMatcher(AbstractMatcher[_T_co]): """ @@ -134,15 +171,6 @@ def __init__( chain((extra_fields or {}).items(), kwargs.items()) ) - def has_value(self) -> bool: - """ - Check if the matcher has a value. - - Returns: - True if the matcher has a value, otherwise False. - """ - return not isinstance(self.value, Unset) - def to_integration_json(self) -> dict[str, Any]: """ Convert the matcher to an integration JSON object. @@ -316,6 +344,68 @@ def to_matching_rule(self) -> dict[str, Any]: return self._matcher.to_matching_rule() +class AndMatcher(AbstractMatcher[_T_co]): + """ + And matcher. + + A matcher that combines multiple matchers using a logical AND. + """ + + def __init__( + self, + *matchers: AbstractMatcher[Any], + value: _T_co | Unset = UNSET, + ) -> None: + """ + Initialize the matcher. + + It is best practice to provide a value. This may be set when creating + the `AndMatcher`, or it may be inferred from one of the constituent + matchers. In the latter case, the value from the first matcher that has + a value will be used. + + Args: + matchers: + List of matchers to combine. + + value: + Example value to match against. If not provided, the value + from the first matcher that has a value will be used. + """ + self._matchers = matchers + self._value: _T_co | Unset = value + + if isinstance(self._value, Unset): + for matcher in matchers: + if matcher.has_value(): + # If `has_value` is true, `value` must be present + self._value = matcher.value # type: ignore[attr-defined] + break + + def to_integration_json(self) -> dict[str, Any]: + """ + Convert the matcher to an integration JSON object. + + See + [`AbstractMatcher.to_integration_json`][pact.match.matcher.AbstractMatcher.to_integration_json] + for more information. + """ + return {"pact:matcher:type": [m.to_integration_json() for m in self._matchers]} + + def to_matching_rule(self) -> dict[str, Any]: + """ + Convert the matcher to a matching rule. + + See + [`AbstractMatcher.to_matching_rule`][pact.match.matcher.AbstractMatcher.to_matching_rule] + for more information. + """ + return { + "combine": "AND", + "matchers": [m.to_matching_rule() for m in self._matchers], + } + + class MatchingRuleJSONEncoder(JSONEncoder): """ JSON encoder class for matching rules. From 4dc54b35323effed7eee40eac5d63099443ae5e5 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 19 Nov 2025 20:28:18 +1100 Subject: [PATCH 1119/1376] fix: use correct matching rule serialisation The serialisation used internally for `with_matching_rules` used the default JSON encoder. This now has been fixed and allows for matchers to be used directly. Signed-off-by: JP-Ellis --- src/pact/interaction/_base.py | 4 ++-- src/pact/match/matcher.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pact/interaction/_base.py b/src/pact/interaction/_base.py index 7b0363571..bb962ce46 100644 --- a/src/pact/interaction/_base.py +++ b/src/pact/interaction/_base.py @@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, Any, Literal import pact_ffi -from pact.match.matcher import IntegrationJSONEncoder +from pact.match.matcher import IntegrationJSONEncoder, MatchingRuleJSONEncoder if TYPE_CHECKING: from pathlib import Path @@ -556,7 +556,7 @@ def with_matching_rules( response. """ if isinstance(rules, dict): - rules = json.dumps(rules) + rules = json.dumps(rules, cls=MatchingRuleJSONEncoder) pact_ffi.with_matching_rules( self._handle, diff --git a/src/pact/match/matcher.py b/src/pact/match/matcher.py index 18526055b..d770a9f26 100644 --- a/src/pact/match/matcher.py +++ b/src/pact/match/matcher.py @@ -424,8 +424,12 @@ def default(self, o: Any) -> Any: # noqa: ANN401 Returns: The encoded object. """ - if isinstance(o, AbstractMatcher): + if isinstance(o, AndMatcher): return o.to_matching_rule() + if isinstance(o, AbstractMatcher): + # We need to convert all matchers in AndMatchers (even if there is + # only one). + return AndMatcher(o).to_matching_rule() return super().default(o) From 587579ae6c47b34f9de363bb3c7ba87c34badeed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:27:20 +1100 Subject: [PATCH 1120/1376] chore(deps): update pre-commit hook crate-ci/committed to v1.1.8 (#1351) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77824742d..32b37f481 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,7 +56,7 @@ repos: )$ - repo: https://github.com/crate-ci/committed - rev: v1.1.7 + rev: v1.1.8 hooks: - id: committed From 8e5303234751dae0ec5f33b012c20d48661ad263 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 20 Nov 2025 10:21:42 +1100 Subject: [PATCH 1121/1376] chore: switch to markdownlint-cli2 Signed-off-by: JP-Ellis --- .markdownlint-cli2.yaml | 33 +++++++++++++++++++++++++++++++++ .markdownlint.yml | 23 ----------------------- .pre-commit-config.yaml | 11 +++-------- 3 files changed, 36 insertions(+), 31 deletions(-) create mode 100644 .markdownlint-cli2.yaml delete mode 100644 .markdownlint.yml diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml new file mode 100644 index 000000000..cad769c4a --- /dev/null +++ b/.markdownlint-cli2.yaml @@ -0,0 +1,33 @@ +--- +ignores: + - .github/PULL_REQUEST_TEMPLATE.md + +config: + default: true + + # Do not enforce line length + line-length: false + + # Adjust list indentation for 4 spaces + list-marker-space: + ul_single: 3 + ul_multi: 3 + ol_single: 2 + ol_multi: 2 + + ul-indent: + indent: 4 + + # Require fenced code blocks + code-block-style: + style: fenced + + # Disable checking for reference links, as MkDocs generates additional ones that + # are not visible to MarkdownLint. + reference-links-images: false + + strong-style: + style: asterisk + + emphasis-style: + style: underscore diff --git a/.markdownlint.yml b/.markdownlint.yml deleted file mode 100644 index 0d2ba1c3f..000000000 --- a/.markdownlint.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -default: true - -# Do not enforce line length -line-length: false - -# Adjust list indentation for 4 spaces -list-marker-space: - ul_single: 3 - ul_multi: 3 - ol_single: 2 - ol_multi: 2 - -ul-indent: - indent: 4 - -# Require fenced code blocks -code-block-style: - style: fenced - -# Disable checking for reference links, as MkDocs generates additional ones that -# are not visible to MarkdownLint. -reference-links-images: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32b37f481..1a926d74e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -60,15 +60,10 @@ repos: hooks: - id: committed - - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.45.0 + - repo: https://github.com/DavidAnson/markdownlint-cli2 + rev: v0.19.0 hooks: - - id: markdownlint - exclude: | - (?x)^( - .github/PULL_REQUEST_TEMPLATE\.md | - CHANGELOG.md - )$ + - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos rev: v1.39.2 From 48e2173068b3520644347ac8c22d99539e3d9959 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 08:09:25 +1100 Subject: [PATCH 1122/1376] chore(deps): update dependency mkdocs-llmstxt to v0.5.0 (#1355) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 708552168..36a026d5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,7 @@ docs = [ "mkdocs-gen-files==0.5.0", "mkdocs-github-admonitions-plugin==0.1.1", "mkdocs-literate-nav==0.6.2", - "mkdocs-llmstxt==0.4.0", + "mkdocs-llmstxt==0.5.0", "mkdocs-material[recommended,git,imaging]==9.7.0", "mkdocs-section-index==0.3.10", "mkdocs==1.6.1", From 5f6949e065f471f06992663f5273356ead030756 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 08:09:36 +1100 Subject: [PATCH 1123/1376] chore(deps): update actions/checkout action to v6 (#1356) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 6 +++--- .github/workflows/build-ffi.yml | 6 +++--- .github/workflows/build.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index c15d2e332..0455ad388 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 @@ -94,7 +94,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 @@ -135,7 +135,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index d039acd66..b5907aec7 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 @@ -94,7 +94,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 @@ -136,7 +136,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d44488e73..45dcc7535 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 @@ -104,7 +104,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cb0a5f608..179247ca6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index e6e6924fd..776661454 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Synchronize labels uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0c3cde6b..eac188ee1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 submodules: true @@ -149,7 +149,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 @@ -192,7 +192,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 @@ -224,7 +224,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 @@ -257,7 +257,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 @@ -285,7 +285,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Cache prek uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 From db2167318ecb3ee8ef0061738afabcc5bd78e5ec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 22 Nov 2025 04:18:03 +0000 Subject: [PATCH 1124/1376] chore(deps): update astral-sh/setup-uv action to v7.1.4 (#1357) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 0455ad388..d2f7fcb85 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index b5907aec7..290780ef3 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45dcc7535..d8e140c83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 179247ca6..b28178efa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eac188ee1..b91d19bc2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: enable-cache: true @@ -154,7 +154,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: enable-cache: true @@ -197,7 +197,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: enable-cache: true @@ -229,7 +229,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: enable-cache: true @@ -262,7 +262,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: enable-cache: true @@ -302,7 +302,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: enable-cache: true cache-suffix: prek From a8278130c95e96302811100649b15b573463102f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 22 Nov 2025 15:19:08 +1100 Subject: [PATCH 1125/1376] chore(deps): update ruff to v0.14.6 (#1358) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a926d74e..6171964c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.5 + rev: v0.14.6 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 9c3b90563..5f881746f 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.5", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.6", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.18.2"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index c674596f6..006adea0d 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.14.5", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.6", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.18.2", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 36a026d5e..50c2cf229 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.14.5", + "ruff==0.14.6", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From 9714af4b3d611023f1238915c844104389dcf9de Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 22 Nov 2025 16:09:47 +1100 Subject: [PATCH 1126/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.7 (#1359) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6171964c6..2be2db2b6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.6 + rev: v2.3.7 hooks: - id: biome-check From 33b025eb80986533d5cf4dca2f7b8e3ea3e86337 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 22 Nov 2025 16:09:58 +1100 Subject: [PATCH 1127/1376] chore(deps): update pre-commit hook davidanson/markdownlint-cli2 to v0.19.1 (#1360) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2be2db2b6..6e895861f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,7 +61,7 @@ repos: - id: committed - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.19.0 + rev: v0.19.1 hooks: - id: markdownlint-cli2 From 62bf96cb161fb37a5db389664e2cb6c254bddeb7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 23 Nov 2025 23:28:41 +0000 Subject: [PATCH 1128/1376] chore(deps): update dependency mkdocs-gen-files to v0.6.0 (#1362) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 50c2cf229..a950d6b88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ docs = [ "griffe-inherited-method-crossrefs==0.0.1.4", "griffe-pydantic==1.1.8", "griffe-warnings-deprecated==1.1.0", - "mkdocs-gen-files==0.5.0", + "mkdocs-gen-files==0.6.0", "mkdocs-github-admonitions-plugin==0.1.1", "mkdocs-literate-nav==0.6.2", "mkdocs-llmstxt==0.5.0", From 2c0ac0b1c294b626e0f12dbaab2c9ee2e19fb19f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:00:17 +0000 Subject: [PATCH 1129/1376] chore(deps): update peter-evans/create-pull-request action to v7.0.9 (#1361) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index d2f7fcb85..9671e2589 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -201,7 +201,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 290780ef3..a80d7505f 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -202,7 +202,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8e140c83..c98c732cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -168,7 +168,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' From 0f025a3bb4b1a36d45e336fa2ab24f0b0049a3b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:28:01 +1100 Subject: [PATCH 1130/1376] chore(deps): update taiki-e/install-action action to v2.62.57 (#1363) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 9671e2589..0be7b0ece 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52 + uses: taiki-e/install-action@763e3324d4fd026c9bd284c504378585777a87d5 # v2.62.57 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index a80d7505f..330089f2f 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52 + uses: taiki-e/install-action@763e3324d4fd026c9bd284c504378585777a87d5 # v2.62.57 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c98c732cf..59966bb99 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52 + uses: taiki-e/install-action@763e3324d4fd026c9bd284c504378585777a87d5 # v2.62.57 with: tool: git-cliff,typos From 816e0c2266cadd7a486586921c6861c372ea754a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:05:04 +1100 Subject: [PATCH 1131/1376] chore(deps): update dependency mkdocstrings to v1 (#1366) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a950d6b88..f6d153a39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ docs = [ "mkdocs-material[recommended,git,imaging]==9.7.0", "mkdocs-section-index==0.3.10", "mkdocs==1.6.1", - "mkdocstrings[python]==0.30.1", + "mkdocstrings[python]==1.0.0", "pathspec==0.12.1", ] example = [ From 61f6997f664387ed5c3018f79295d636deedc7a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 03:13:02 +0000 Subject: [PATCH 1132/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.40.0 (#1365) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e895861f..04ab4401d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.39.2 + rev: v1.40.0 hooks: - id: typos exclude: | From e612d38bbadcd0c028aff1ec700f4a4d73aec5e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:12:14 +0000 Subject: [PATCH 1133/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.8 (#1368) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04ab4401d..0b60645a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.7 + rev: v2.3.8 hooks: - id: biome-check From ae5a91cb9240348ee50fa4517af51db72b423b2a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 08:27:49 +1100 Subject: [PATCH 1134/1376] chore(deps): update ruff to v0.14.7 (#1370) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b60645a9..73731b223 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.6 + rev: v0.14.7 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 5f881746f..2bd1872c7 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.6", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.7", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.18.2"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 006adea0d..ed583880d 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.14.6", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.7", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.18.2", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index f6d153a39..9dca404bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.14.6", + "ruff==0.14.7", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From 1b76a9d10504b567814c4897b0132a38cb3279a4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:29:19 +0000 Subject: [PATCH 1135/1376] chore(deps): update softprops/action-gh-release action to v2.5.0 (#1372) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 0be7b0ece..799b323ab 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -186,7 +186,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 330089f2f..5fabd16b3 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -187,7 +187,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59966bb99..c9f433478 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,7 +153,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md From 853ba1d73722167459bb6782abab41932b31b103 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:29:54 +0000 Subject: [PATCH 1136/1376] chore(deps): update dependency mypy to v1.19.0 (#1369) Signed-off-by: JP-Ellis Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: JP-Ellis --- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- src/pact/generate/__init__.py | 3 ++- src/pact/match/__init__.py | 3 ++- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 2bd1872c7..f4da4aac3 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -53,7 +53,7 @@ requires-python = ">=3.10" # developper consistency. All other dependencies are as above. dev = ["ruff==0.14.7", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] -types = ["mypy==1.18.2"] +types = ["mypy==1.19.0"] ################################################################################ ## Build System diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index ed583880d..de606ee59 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -43,7 +43,7 @@ dependencies = ["cffi~=2.0"] [dependency-groups] dev = ["ruff==0.14.7", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] -types = ["mypy==1.18.2", "typing-extensions~=4.0"] +types = ["mypy==1.19.0", "typing-extensions~=4.0"] ################################################################################ ## Build System diff --git a/pyproject.toml b/pyproject.toml index 9dca404bd..c7e34ed90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,7 +121,7 @@ test = [ "testcontainers~=4.0", ] types = [ - "mypy==1.18.2", + "mypy==1.19.0", "types-grpcio~=1.0", "types-protobuf~=6.0", "types-requests~=2.0", diff --git a/src/pact/generate/__init__.py b/src/pact/generate/__init__.py index 26741ff7f..5067c3a9c 100644 --- a/src/pact/generate/__init__.py +++ b/src/pact/generate/__init__.py @@ -146,7 +146,7 @@ def __import__( # noqa: N807 name: builtins.str, globals: Mapping[builtins.str, object] | None = None, locals: Mapping[builtins.str, object] | None = None, - fromlist: Sequence[builtins.str] = (), + fromlist: Sequence[builtins.str] | None = None, level: builtins.int = 0, ) -> ModuleType: """ @@ -157,6 +157,7 @@ def __import__( # noqa: N807 done to avoid shadowing built-in types and functions. """ __tracebackhide__ = True + fromlist = fromlist or () if name == "pact.generate" and len(set(fromlist) - {"AbstractGenerator"}) > 0: warnings.warn( "Avoid `from pact.generate import `. " diff --git a/src/pact/match/__init__.py b/src/pact/match/__init__.py index c6af3ceda..0d0503b7c 100644 --- a/src/pact/match/__init__.py +++ b/src/pact/match/__init__.py @@ -317,7 +317,7 @@ def __import__( # noqa: N807 name: builtins.str, globals: Mapping[builtins.str, object] | None = None, locals: Mapping[builtins.str, object] | None = None, - fromlist: Sequence[builtins.str] = (), + fromlist: Sequence[builtins.str] | None = None, level: builtins.int = 0, ) -> ModuleType: """ @@ -328,6 +328,7 @@ def __import__( # noqa: N807 avoid shadowing built-in types and functions. """ __tracebackhide__ = True + fromlist = fromlist or () if name == "pact.match" and len(set(fromlist) - {"AbstractMatcher"}) > 0: warnings.warn( "Avoid `from pact.match import `. " From 6cec3ad03e0632e9c3ad43643fad77fbd65ec67e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 08:31:43 +1100 Subject: [PATCH 1137/1376] chore(deps): update taiki-e/install-action action to v2.62.60 (#1371) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 799b323ab..8541001d1 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@763e3324d4fd026c9bd284c504378585777a87d5 # v2.62.57 + uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 5fabd16b3..c9e15a31b 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@763e3324d4fd026c9bd284c504378585777a87d5 # v2.62.57 + uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c9f433478..b0f56c562 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@763e3324d4fd026c9bd284c504378585777a87d5 # v2.62.57 + uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 with: tool: git-cliff,typos From 0dda3f36aa6317d410d2faaf492d8bc864f02279 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 19 Nov 2025 16:46:36 +1100 Subject: [PATCH 1138/1376] docs: add multipart/form-data matching rule example Add an example which showcases how matching rules can be used to validate multipart/form-data payloads. Signed-off-by: JP-Ellis --- examples/README.md | 8 + examples/catalog/README.md | 34 ++++ examples/catalog/__init__.py | 1 + .../multipart_matching_rules/README.md | 11 ++ .../multipart_matching_rules/__init__.py | 1 + .../multipart_matching_rules/test_consumer.py | 187 ++++++++++++++++++ .../multipart_matching_rules/test_provider.py | 140 +++++++++++++ examples/catalog/pyproject.toml | 29 +++ mkdocs.yml | 1 + 9 files changed, 412 insertions(+) create mode 100644 examples/catalog/README.md create mode 100644 examples/catalog/__init__.py create mode 100644 examples/catalog/multipart_matching_rules/README.md create mode 100644 examples/catalog/multipart_matching_rules/__init__.py create mode 100644 examples/catalog/multipart_matching_rules/test_consumer.py create mode 100644 examples/catalog/multipart_matching_rules/test_provider.py create mode 100644 examples/catalog/pyproject.toml diff --git a/examples/README.md b/examples/README.md index 9e79c71f2..0651f9e45 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,6 +8,14 @@ The code within the examples is intended to be well-documented and you are encou ## Available Examples +### Patterns Catalog + +#### [Pact Patterns Catalog](./catalog/README.md) + +- **Location**: `examples/catalog/` +- **Purpose**: Focused code snippets demonstrating specific Pact patterns +- **Content**: Well-documented use cases and techniques without full application context + ### HTTP Examples #### [aiohttp and Flask](./http/aiohttp_and_flask/README.md) diff --git a/examples/catalog/README.md b/examples/catalog/README.md new file mode 100644 index 000000000..304549a71 --- /dev/null +++ b/examples/catalog/README.md @@ -0,0 +1,34 @@ +# Pact Patterns Catalog + +This catalog contains focused, well-documented code snippets demonstrating specific Pact patterns and use cases. Unlike the full examples in the parent directory, catalog entries are designed to showcase a single pattern or technique with minimal application context. + +## Available + +- [Multipart Form Data with Matching Rules](./multipart_matching_rules/README.md) + +## Using Catalog Entries + +Catalog entries are intended as a reference for learning and adapting Pact patterns. To get the most value: + +- **Read the documentation** in each entry's README to understand the pattern and its intended use. +- **Review the code** to see how the pattern is implemented in practice. +- **Explore the tests** to see example usages and edge cases. + +If you want to experiment or adapt a pattern, you can run the tests for any entry: + +```console +cd examples/catalog +uv run --group test pytest +``` + +## Contributing Patterns + +When adding a new catalog entry: + +1. Focus on a single pattern or technique +2. Provide minimal but complete code, emphasizing the Pact aspects over application logic +3. Document the pattern thoroughly +4. Include working pytest tests +5. Add it to this README + +For complete examples with full application context, consider adding to the main examples directory instead. diff --git a/examples/catalog/__init__.py b/examples/catalog/__init__.py new file mode 100644 index 000000000..6e031999e --- /dev/null +++ b/examples/catalog/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/examples/catalog/multipart_matching_rules/README.md b/examples/catalog/multipart_matching_rules/README.md new file mode 100644 index 000000000..7a4cfe5e0 --- /dev/null +++ b/examples/catalog/multipart_matching_rules/README.md @@ -0,0 +1,11 @@ + +# Multipart Form Data with Matching Rules + +This entry demonstrates how to use Pact matching rules with multipart/form-data requests. Specifically, it demonstrates how multipart data is specified on the consumer side, and how matching rules can be applied to different parts of the multipart body. + +For full implementation details, see the code and docstrings in this directory. For general catalog usage, prerequisites, and test-running instructions, see the [main catalog README](../README.md). + +Related documentation: + +* [Pact Matching Rules](https://docs.pact.io/getting_started/matching) +* [Multipart Form Data (RFC 7578)](https://tools.ietf.org/html/rfc7578) diff --git a/examples/catalog/multipart_matching_rules/__init__.py b/examples/catalog/multipart_matching_rules/__init__.py new file mode 100644 index 000000000..6e031999e --- /dev/null +++ b/examples/catalog/multipart_matching_rules/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/examples/catalog/multipart_matching_rules/test_consumer.py b/examples/catalog/multipart_matching_rules/test_consumer.py new file mode 100644 index 000000000..40d9975a3 --- /dev/null +++ b/examples/catalog/multipart_matching_rules/test_consumer.py @@ -0,0 +1,187 @@ +""" +Consumer test demonstrating multipart/form-data with matching rules. + +This test shows how to use Pact matching rules with multipart requests. The +examples illustrates this with a request containing both JSON metadata and +binary data (an image). The contract uses matching rules to validate +structure and types rather than exact values, allowing flexibility in the data +sent by the consumer and accepted by the provider. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import TYPE_CHECKING + +import httpx +import pytest + +from pact import Pact, match + +if TYPE_CHECKING: + from collections.abc import Generator + + +# Minimal JPEG for testing. The important part is the magic bytes. The rest is +# not strictly valid JPEG data. +# fmt: off +JPEG_BYTES = bytes([ + 0xFF, 0xD8, # Start of Image (SOI) marker + 0xFF, 0xE0, # JFIF APP0 Marker + 0x00, 0x10, # Length of APP0 segment + 0x4A, 0x46, 0x49, 0x46, 0x00, # "JFIF\0" + 0x01, 0x02, # Major and minor versions +]) +# fmt: on +""" +Some minimal JPEG bytes for testing multipart uploads. + +In this example, we only need the JPEG magic bytes to validate the file type. +This is not a complete JPEG file, but is sufficient for testing purposes. +""" + + +@pytest.fixture +def pact() -> Generator[Pact, None, None]: + """ + Set up Pact for consumer contract testing. + + This fixture initializes a Pact instance for the consumer tests, specifying + the consumer and provider names, and ensuring that the generated Pact files + are written to the appropriate directory after the tests run. + """ + pact = Pact( + "multipart-consumer", + "multipart-provider", + ) + yield pact + pact.write_file(Path(__file__).parents[1] / "pacts") + + +def test_multipart_upload_with_matching_rules(pact: Pact) -> None: + """ + Test multipart upload with matching of the contents. + + This test builds a `multipart/form-data` request by hand, and then uses a + library (`httpx`) to send the request to the mock server started by Pact. + Unlike simpler payloads, the matching rules _cannot_ be embedded within the + body itself. Instead, the body and matching rules are defined in separate + calls. + + Some key points about this example: + + - We use a matching rule for the `Content-Type` header to allow any valid + multipart boundary. This is important because many HTTP libraries + generate random boundaries automatically without user control. + - The body includes arbitrary binary data (a JPEG image) which cannot be + represented as a string. Therefore, it is critical that + `with_binary_body` is used to define the payload. + - Matching rules are defined for both the JSON metadata and the image part + to allow flexibility in the values sent by the consumer. The general + form to match a part within the multipart body is `$.`. So + to match a field in the `metadata` part, we use `$.metadata.`; or + to match the content type of the `image` part, we use `$.image`: + + ```python + from pact import match + + { + "body": { + "$.image": match.content_type("image/jpeg"), + "$.metadata.name": match.regex(regex=r"^[a-zA-Z]+$"), + }, + } + ``` + + /// warning + + Proper content types are essential when working with multipart data. This + ensures that Pact can correctly identify and apply matching rules to each + part of the multipart body. If content types are missing or incorrect, the + matching rules may not be applied as expected, leading to test failures or + incorrect behavior. + + /// + + To view the implementation, expand the source code below. + """ + # It is recommended to use a fixed boundary for testing, this ensures that + # the generated Pact is consistent across test runs. + boundary = "test-boundary-12345" + + metadata = { + "name": "test", + "size": 100, + } + + # Build multipart body with both JSON and binary parts. Note that since we + # are combining text and binary data, the strings must be encoded to bytes. + expected_body = ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="metadata"\r\n' + f"Content-Type: application/json\r\n" + f"\r\n" + f"{json.dumps(metadata)}\r\n" + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="image"; filename="test.jpg"\r\n' + f"Content-Type: image/jpeg\r\n" + f"\r\n" + ).encode() + expected_body += JPEG_BYTES + expected_body += f"\r\n--{boundary}--\r\n".encode() + + # Define the interaction with matching rules + ( + pact.upon_receiving("a multipart upload with JSON metadata and image") + .with_request("POST", "/upload") + .with_header( + "Content-Type", + # The matcher here is important if you don't have the ability to fix + # the boundary in the actual request (e.g., when using a library + # that generates it automatically). + match.regex( + f"multipart/form-data; boundary={boundary}", + regex=r"multipart/form-data;\s*boundary=.*", + ), + ) + .with_binary_body( + expected_body, + f"multipart/form-data; boundary={boundary}", + ) + # Matching rules make the contract flexible + .with_matching_rules({ + "body": { + "$.image": match.content_type("image/jpeg"), + "$.metadata": match.type({}), + "$.metadata.name": match.regex(regex=r"^[a-zA-Z]+$"), + "$.metadata.size": match.int(), + }, + }) + .will_respond_with(201) + .with_body({ + "id": "upload-1", + "message": "Upload successful", + "metadata": {"name": "test", "size": 100}, + "image_size": len(JPEG_BYTES), + }) + ) + + # Execute the test. Note that the matching rules take effect here, so we can + # send data that differs from the example in the contract. + with pact.serve() as srv: + # Simple inline consumer: just make the multipart request + files = { + "metadata": ( + None, + json.dumps({"name": "different", "size": 200}).encode(), + "application/json", + ), + "image": ("test.jpg", JPEG_BYTES, "image/jpeg"), + } + response = httpx.post(f"{srv.url}/upload", files=files, timeout=5) + + assert response.status_code == 201 + result = response.json() + assert result["message"] == "Upload successful" + assert result["id"] == "upload-1" diff --git a/examples/catalog/multipart_matching_rules/test_provider.py b/examples/catalog/multipart_matching_rules/test_provider.py new file mode 100644 index 000000000..efedc2952 --- /dev/null +++ b/examples/catalog/multipart_matching_rules/test_provider.py @@ -0,0 +1,140 @@ +""" +Provider verification for multipart/form-data contract with matching rules. + +This test demonstrates provider verification for contracts with multipart +requests and matching rules. It uses FastAPI to create a simple server that can +process multipart uploads containing JSON metadata and a JPEG image. The test +verifies that the provider complies with the contract generated by the consumer +tests, ensuring that it can handle variations in the data while maintaining +compatibility. +""" + +from __future__ import annotations + +import time +from pathlib import Path +from threading import Thread +from typing import TYPE_CHECKING, Annotated + +import pytest +import uvicorn +from fastapi import FastAPI, Form, HTTPException, UploadFile +from pydantic import BaseModel + +import pact._util +from pact import Verifier + +if TYPE_CHECKING: + from typing import Any + + +# Simple FastAPI provider for handling multipart uploads +app = FastAPI() +""" +FastAPI application to handle multipart/form-data uploads. +""" + +uploads: dict[str, dict[str, Any]] = {} +""" +In-memory store for uploaded files and metadata. +""" + + +class UploadMetadata(BaseModel): + """ + Model for the JSON metadata part of the upload. + """ + + name: str + size: int + + +class UploadResponse(BaseModel): + """ + Model for the response returned after a successful upload. + """ + + id: str + message: str + metadata: UploadMetadata + image_size: int + + +@app.post("/upload", status_code=201) +async def upload_file( + metadata: Annotated[str, Form()], + image: Annotated[UploadFile, Form()], +) -> UploadResponse: + """ + Handle multipart upload with JSON metadata and image. + + This endpoint processes a multipart/form-data request containing a JSON + metadata part and an image file part. It validates the metadata structure + and the image content type, then stores the upload in memory. + """ + metadata_dict = UploadMetadata.model_validate_json(metadata) + if image.content_type != "image/jpeg": + msg = f"Expected image/jpeg, got {image.content_type}" + raise HTTPException(status_code=400, detail=msg) + + content = await image.read() + if not content.startswith((b"\xff\xd8\xff\xdb", b"\xff\xd8\xff\xe0")): + msg = "Invalid/malformed JPEG file" + raise HTTPException(status_code=400, detail=msg) + + upload_id = f"upload-{len(uploads) + 1}" + uploads[upload_id] = { + "id": upload_id, + "metadata": metadata_dict, + "filename": image.filename, + "content_type": image.content_type, + "size": len(content), + } + + return UploadResponse( + id=upload_id, + message="Upload successful", + metadata=metadata_dict, + image_size=len(content), + ) + + +@pytest.fixture +def app_server() -> str: + """ + Start FastAPI server for provider verification. + """ + hostname = "localhost" + port = pact._util.find_free_port() # noqa: SLF001 + Thread( + target=uvicorn.run, + args=(app,), + kwargs={"host": hostname, "port": port}, + daemon=True, + ).start() + time.sleep(0.1) # Allow server time to start + return f"http://{hostname}:{port}" + + +def test_provider_multipart(app_server: str) -> None: + """ + Verify the provider against the multipart upload contract. + + In general, there are no special considerations for verifying providers with + multipart requests. The Pact verifier will read the contract file generated + by the consumer tests and ensure that the provider can handle requests that + conform to the specified matching rules. + + As with any provider verification, the test needs to ensure that provider + states are set up correctly. This example does not include any provider + states to ensure simplicity. + """ + verifier = ( + Verifier("multipart-provider") + .add_source(Path(__file__).parents[1] / "pacts") + .add_transport(url=app_server) + ) + + verifier.verify() + + assert len(uploads) > 0, "No uploads were processed by the provider" diff --git a/examples/catalog/pyproject.toml b/examples/catalog/pyproject.toml new file mode 100644 index 000000000..9e9ec1dac --- /dev/null +++ b/examples/catalog/pyproject.toml @@ -0,0 +1,29 @@ +#:schema https://www.schemastore.org/pyproject.toml +[project] +name = "pact-python-catalog" +version = "1.0.0" + +dependencies = [ + "pact-python", + "httpx~=0.0", + "fastapi~=0.0", + "python-multipart~=0.0", +] +description = "Pact Python catalog: Well-documented patterns and use cases" +requires-python = ">=3.10" + +[dependency-groups] +test = ["pact-python", "pytest~=9.0", "uvicorn~=0.30"] + +[tool.uv.sources] +pact-python = { path = "../../" } + +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.pytest] +addopts = ["--import-mode=importlib"] + +log_date_format = "%H:%M:%S" +log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" +log_level = "NOTSET" diff --git a/mkdocs.yml b/mkdocs.yml index faadb9ea8..541896fb4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,6 +28,7 @@ plugins: python: inventories: - https://docs.aiohttp.org/en/stable/objects.inv + - https://docs.pydantic.dev/latest/objects.inv - https://docs.python.org/3/objects.inv - https://fastapi.tiangolo.com/objects.inv - https://flask.palletsprojects.com/en/stable/objects.inv From c612a6770bfe2cc212616904bc8319a6b60b576a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 28 Nov 2025 14:22:41 +1100 Subject: [PATCH 1139/1376] chore: rerun flaky tests There are a number of tests which can be flaky due to internal connections. Signed-off-by: JP-Ellis --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index c7e34ed90..b76532482 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ example = [ "protobuf~=6.0", "pydantic~=2.0", "pytest-cov~=7.0", + "pytest-rerunfailures~=16.0", "pytest~=9.0", "testcontainers~=4.0", "uvicorn[standard]~=0.0", @@ -134,6 +135,7 @@ example-v2 = [ "fastapi~=0.0", "flask[async]~=3.0", "pytest-cov~=7.0", + "pytest-rerunfailures~=16.0", "pytest~=9.0", "testcontainers~=4.0", "uvicorn[standard]~=0.0", @@ -143,6 +145,7 @@ test-v2 = [ "httpx~=0.0", "mock~=5.0", "pytest-cov~=7.0", + "pytest-rerunfailures~=16.0", "pytest~=9.0", "uvicorn[standard]~=0.0", ] @@ -290,6 +293,9 @@ addopts = [ "--cov-config=pyproject.toml", "--cov-report=xml", "--cov=pact", + # Reruns + "--reruns=5", + "--rerun-except=AssertionError", ] asyncio_default_fixture_loop_scope = "session" From b87be89a2312af4835570996fa208c107e4cad38 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 20 Nov 2025 14:31:09 +1100 Subject: [PATCH 1140/1376] chore: remove unused function Signed-off-by: JP-Ellis --- examples/http/requests_and_fastapi/test_provider.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/examples/http/requests_and_fastapi/test_provider.py b/examples/http/requests_and_fastapi/test_provider.py index 6c6fe9e6d..ffd5a445b 100644 --- a/examples/http/requests_and_fastapi/test_provider.py +++ b/examples/http/requests_and_fastapi/test_provider.py @@ -39,14 +39,6 @@ logger = logging.getLogger(__name__) -def start_fastapi_server(host: str, port: int) -> None: - uvicorn.run( - app, - host=host, - port=port, - ) - - @pytest.fixture(scope="session") def app_server() -> str: """ From dd62c72858dc000712175adbe523a4c856af32b7 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 25 Nov 2025 14:26:32 +1100 Subject: [PATCH 1141/1376] docs: add consumer_version Consumer version was recently added (in 3.2). Signed-off-by: JP-Ellis --- MIGRATION.md | 5 ++++- docs/provider.md | 29 ++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 06ff1ea7b..f28020154 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -292,13 +292,16 @@ broker_builder = ( ) .include_pending() .provider_branch('main') - .consumer_tags('main', 'develop') + .consumer_version(branch='main') + .consumer_version(branch='develop') .build() ) ``` The `selector=True` argument returns a [`BrokerSelectorBuilder`][pact.verifier.BrokerSelectorBuilder] instance, which provides methods to configure which pacts to fetch. The `build()` call finalizes the configuration and returns the `Verifier` instance which can then be further configured. +The `consumer_version` method provides fine-grained control over which consumer pacts are verified and can be called multiple times to add multiple selectors (combined with a logical OR). The older `consumer_versions` method is now deprecated in favor of `consumer_version`. + /// #### Provider State Handling diff --git a/docs/provider.md b/docs/provider.md index 7edcd010a..576f201df 100644 --- a/docs/provider.md +++ b/docs/provider.md @@ -96,14 +96,38 @@ def test_provider_with_selectors(): ) .include_pending() # Include pending pacts .include_wip_since("2023-01-01") # Include WIP pacts since date - .provider_tags("main", "develop") - .consumer_tags("production", "main") + .provider_branch("main") + .consumer_version(branch="main") # Specific consumer version selectors + .consumer_version(branch="develop") # Can be called multiple times .build() # Build the selector ) verifier.verify() ``` +#### Consumer Version Selectors + +The `consumer_version` method provides fine-grained control over which consumer pacts are verified. It can be called multiple times to add multiple selectors, which are combined with a logical OR (pacts matching any selector will be included). + +Common use cases: + +```python +# Verify pacts from a specific branch +.consumer_version(branch="feature/new-api") + +# Verify pacts from deployed or released versions +.consumer_version(deployed_or_released=True) + +# Verify pacts from main branch +.consumer_version(main_branch=True) + +# Verify pacts from a specific consumer only +.consumer_version(consumer="mobile-app", branch="main") + +# Verify pacts from a specific environment +.consumer_version(deployed=True, environment="production") +``` + More information on the selector options is available in the [API reference][pact.verifier.BrokerSelectorBuilder]. ## Logging @@ -139,7 +163,6 @@ if "CI" in os.environ: verifier.set_publish_options( # (1) version="1.2.3", branch="main", - tags=["production"], ) verifier.verify() From 0b5892fdb64eac3d32fcad52c61f16f536f6d569 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 2 Dec 2025 13:34:20 +1100 Subject: [PATCH 1142/1376] chore: don't except AssertionError Signed-off-by: JP-Ellis --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b76532482..ca86d024b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -295,7 +295,6 @@ addopts = [ "--cov=pact", # Reruns "--reruns=5", - "--rerun-except=AssertionError", ] asyncio_default_fixture_loop_scope = "session" From bf54b8cb68c7fb89bd32381512ebe791f287846f Mon Sep 17 00:00:00 2001 From: Nikhil Arora Date: Tue, 2 Dec 2025 11:18:47 +0530 Subject: [PATCH 1143/1376] chore(devcontainer): add multi-arch development container support Add a devcontainer to help contributors work in a consistent and reproducible environment. --- .devcontainer/.containerignore | 30 ++++++++++++++++++++++++++++ .devcontainer/Containerfile | 27 +++++++++++++++++++++++++ .devcontainer/devcontainer.json | 35 +++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 .devcontainer/.containerignore create mode 100644 .devcontainer/Containerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/.containerignore b/.devcontainer/.containerignore new file mode 100644 index 000000000..28cf290ab --- /dev/null +++ b/.devcontainer/.containerignore @@ -0,0 +1,30 @@ +**/.git +**/.gitignore +**/.vscode +**/.idea +**/.DS_Store +**/.pytest_cache +**/.mypy_cache +**/.ruff_cache +**/__pycache__ +**/*.pyc +**/*.pyo +**/*.pyd +**/.Python +**/pip-log.txt +**/pip-delete-this-directory.txt +**/.venv +**/venv +**/ENV +**/env +**/.coverage +**/.coverage.* +**/htmlcov +**/coverage.xml +**/*.cover +**/*.log +**/.hypothesis +**/dist +**/build +**/*.egg-info +**/node_modules diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile new file mode 100644 index 000000000..25c9db8db --- /dev/null +++ b/.devcontainer/Containerfile @@ -0,0 +1,27 @@ +FROM python:3.13-slim + +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends \ + git \ + curl \ + wget \ + build-essential \ + sudo \ + libffi-dev \ + && groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ + && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +USER $USERNAME + +ENV PATH="/home/${USERNAME}/.cargo/bin:/home/${USERNAME}/.local/bin:${PATH}" + +RUN curl -LsSf https://astral.sh/uv/install.sh | sh \ + && uv tool install hatch diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..11a53d0b3 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,35 @@ +{ + "name": "Pact Python Development", + "build": { + "dockerfile": "Containerfile", + "context": ".." + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "charliermarsh.ruff" + ], + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python3", + "python.testing.pytestEnabled": true, + "python.testing.pytestArgs": ["tests/"], + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff" + } + } + } + }, + "containerEnv": { + "PYTHONUNBUFFERED": "1" + }, + "postCreateCommand": "git submodule update --init && hatch env create", + "remoteUser": "vscode", + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached", + "workspaceFolder": "/workspace" +} From 5913611e81d3bba7202e293d709fa1178e9c6c69 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:38:15 +1100 Subject: [PATCH 1144/1376] chore(deps): update actions/checkout action to v6.0.1 (#1376) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 6 +++--- .github/workflows/build-ffi.yml | 6 +++--- .github/workflows/build.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 8541001d1..c2052e91c 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 @@ -94,7 +94,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 @@ -135,7 +135,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index c9e15a31b..13691f8a4 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 @@ -94,7 +94,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 @@ -136,7 +136,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0f56c562..e4a089e30 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 @@ -104,7 +104,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b28178efa..38a5b8779 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 776661454..2c1a40f73 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Synchronize labels uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b91d19bc2..66cc8230f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 submodules: true @@ -149,7 +149,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 @@ -192,7 +192,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 @@ -224,7 +224,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 @@ -257,7 +257,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 @@ -285,7 +285,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Cache prek uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 From 9d9eb2b3308f7bf8bae0b9a74bd0bfe85f78239d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:38:19 +1100 Subject: [PATCH 1145/1376] chore(deps): pin python docker tag to 326df67 (#1374) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 25c9db8db..0834c807e 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.13-slim +FROM python:3.13-slim@sha256:326df678c20c78d465db501563f3492d17c42a4afe33a1f2bf5406a1d56b0e86 ARG USERNAME=vscode ARG USER_UID=1000 From afa65873afe3749e63370a78f43d56fd13168287 Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:51:20 +0000 Subject: [PATCH 1146/1376] docs: update changelog for pact-python/3.2.0 --- CHANGELOG.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc633af41..a683cd9de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,52 @@ All notable changes to this project will be documented in this file. +## [pact-python/3.2.0] _2025-12-02_ + +### 🚀 Features + +- Add consumer_version method +- Add content type matcher +- Add 'and' matcher + +### 🐛 Bug Fixes + +- Use correct matching rule serialisation + +### 📚 Documentation + +- Update changelog for pact-python/3.1.0 +- Add agents.md +- Update configuration +- Add logging documentation +- Add multipart/form-data matching rule example +- Add consumer_version + +### ⚙️ Miscellaneous Tasks + +- Add llm instructions +- Update non-compliant docstrings and types +- Upgrade pymdownx extensions +- Set telemetry environment variables +- _(docs)_ Api docs link on pact-python site is case sensitive +- Fix json schema url +- _(tests)_ Fix skipped tests on windows +- _(ci)_ Update macos runners +- Remove unused pytest config +- Remove ruff sub-configs +- Switch to markdownlint-cli2 +- Rerun flaky tests +- Remove unused function +- Don't except AssertionError +- _(devcontainer)_ Add multi-arch development container support + +### Contributors + +- @Nikhil172913832 +- @JP-Ellis +- @YOU54F +- @Copilot + ## [pact-python/3.1.0] _2025-10-07_ ### 🐛 Bug Fixes @@ -1624,4 +1670,4 @@ All notable changes to this project will be documented in this file. - @matthewbalvanz-wf - @mefellows - + From ad5c5fb84304a1f0a2a9088a69cd36bc692a5d5f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 03:55:36 +0000 Subject: [PATCH 1147/1376] chore(deps): update python docker tag to v3.14 (#1375) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 0834c807e..9f19c5225 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.13-slim@sha256:326df678c20c78d465db501563f3492d17c42a4afe33a1f2bf5406a1d56b0e86 +FROM python:3.14-slim@sha256:0aecac02dc3d4c5dbb024b753af084cafe41f5416e02193f1ce345d671ec966e ARG USERNAME=vscode ARG USER_UID=1000 From 46a93463ac793efbe45157d9d9dafef152e02450 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:38:35 +1100 Subject: [PATCH 1148/1376] chore(deps): update python:3.14-slim docker digest to 4451352 (#1378) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 9f19c5225..fa36ba435 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:0aecac02dc3d4c5dbb024b753af084cafe41f5416e02193f1ce345d671ec966e +FROM python:3.14-slim@sha256:44513520b81338d2d12499a59874220b05988819067a2cbac4545750a68e4b2b ARG USERNAME=vscode ARG USER_UID=1000 From e5875584a5ec6ff44a741f1e9590cd0cb5b97079 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:17:22 +1100 Subject: [PATCH 1149/1376] chore(deps): update python:3.14-slim docker digest to b823ded (#1379) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index fa36ba435..f6609f036 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:44513520b81338d2d12499a59874220b05988819067a2cbac4545750a68e4b2b +FROM python:3.14-slim@sha256:b823ded4377ebb5ff1af5926702df2284e53cecbc6e3549e93a19d8632a1897e ARG USERNAME=vscode ARG USER_UID=1000 From a7ac6a9d9f40542bf07b1893dbaadd77ac3b7032 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 4 Dec 2025 11:14:14 +1100 Subject: [PATCH 1150/1376] docs: fix internal references An update to MkDocstrings means we can use scoped references. Signed-off-by: JP-Ellis --- examples/plugins/proto/person_pb2_grpc.py | 3 +- mkdocs.yml | 2 +- src/pact/__init__.py | 8 ++-- src/pact/error.py | 4 +- src/pact/generate/__init__.py | 12 ++--- src/pact/generate/generator.py | 6 +-- src/pact/interaction/__init__.py | 2 +- src/pact/interaction/_base.py | 25 +++++----- src/pact/interaction/_http_interaction.py | 48 ++++++++----------- .../interaction/_sync_message_interaction.py | 11 ++--- src/pact/match/__init__.py | 14 +++--- src/pact/match/matcher.py | 20 ++++---- src/pact/pact.py | 14 +++--- src/pact/verifier.py | 37 +++++++------- 14 files changed, 97 insertions(+), 109 deletions(-) diff --git a/examples/plugins/proto/person_pb2_grpc.py b/examples/plugins/proto/person_pb2_grpc.py index 75e72221b..a091e6f88 100644 --- a/examples/plugins/proto/person_pb2_grpc.py +++ b/examples/plugins/proto/person_pb2_grpc.py @@ -104,7 +104,8 @@ def GetPerson( The response containing the person's details. Raises: - If the method is not implemented. + NotImplementedError: + If the method is not implemented. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") diff --git a/mkdocs.yml b/mkdocs.yml index 541896fb4..3ddc1171f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,7 +61,7 @@ plugins: ignore_init_summary: true docstring_section_style: spacy merge_init_into_class: true - relative_crossrefs: true + # relative_crossrefs: true scoped_crossrefs: true show_if_no_docstring: true # Signature diff --git a/src/pact/__init__.py b/src/pact/__init__.py index e67eaea59..7066585f2 100644 --- a/src/pact/__init__.py +++ b/src/pact/__init__.py @@ -13,13 +13,13 @@ The primary entry points for contract testing are: -- [`Pact`][pact.Pact]: For consumer-side contract testing, defining expected +- [`Pact`][Pact]: For consumer-side contract testing, defining expected interactions and generating contract files. -- [`Verifier`][pact.Verifier]: For provider-side contract verification, +- [`Verifier`][Verifier]: For provider-side contract verification, validating that a provider implementation satisfies consumer contracts. -These functions are defined in [`pact.pact`][pact.pact] and -[`pact.verifier`][pact.verifier] and re-exported for convenience. +These functions are defined in [`pact.pact`][pact] and +[`pact.verifier`][verifier] and re-exported for convenience. ### Matching and Generation diff --git a/src/pact/error.py b/src/pact/error.py index da0bd3bf1..d247d6987 100644 --- a/src/pact/error.py +++ b/src/pact/error.py @@ -77,8 +77,8 @@ class PactVerificationError(PactError): All of the errors that occurred during the verification of all of the interactions are stored in the `errors` attribute. - This is different from the [`MismatchesError`][pact.error.MismatchesError] - which is raised when there are mismatches detected by the mock server. + This is different from the [`MismatchesError`][error.MismatchesError] which + is raised when there are mismatches detected by the mock server. """ def __init__(self, errors: list[InteractionVerificationError]) -> None: diff --git a/src/pact/generate/__init__.py b/src/pact/generate/__init__.py index 5067c3a9c..58f910d86 100644 --- a/src/pact/generate/__init__.py +++ b/src/pact/generate/__init__.py @@ -202,7 +202,7 @@ def integer( max: builtins.int | None = None, ) -> AbstractGenerator: """ - Alias for [`generate.int`][pact.generate.int]. + Alias for [`generate.int`][int]. Args: min: @@ -239,7 +239,7 @@ def float(precision: builtins.int | None = None) -> AbstractGenerator: def decimal(precision: builtins.int | None = None) -> AbstractGenerator: """ - Alias for [`generate.float`][pact.generate.float]. + Alias for [`generate.float`][float]. Args: precision: @@ -270,7 +270,7 @@ def hex(digits: builtins.int | None = None) -> AbstractGenerator: def hexadecimal(digits: builtins.int | None = None) -> AbstractGenerator: """ - Alias for [`generate.hex`][pact.generate.hex]. + Alias for [`generate.hex`][hex]. Args: digits: @@ -301,7 +301,7 @@ def str(size: builtins.int | None = None) -> AbstractGenerator: def string(size: builtins.int | None = None) -> AbstractGenerator: """ - Alias for [`generate.str`][pact.generate.str]. + Alias for [`generate.str`][str]. Args: size: @@ -448,7 +448,7 @@ def timestamp( disable_conversion: builtins.bool = False, ) -> AbstractGenerator: """ - Alias for [`generate.datetime`][pact.generate.datetime]. + Alias for [`generate.datetime`][datetime]. Returns: Generator producing datetimes in the specified format. @@ -468,7 +468,7 @@ def bool() -> AbstractGenerator: def boolean() -> AbstractGenerator: """ - Alias for [`generate.bool`][pact.generate.bool]. + Alias for [`generate.bool`][bool]. Returns: Generator producing random boolean values. diff --git a/src/pact/generate/generator.py b/src/pact/generate/generator.py index 3df502107..f0cd22df3 100644 --- a/src/pact/generate/generator.py +++ b/src/pact/generate/generator.py @@ -37,7 +37,7 @@ def to_integration_json(self) -> dict[str, Any]: Convert the generator to an integration JSON object. See - [`AbstractGenerator.to_integration_json`][pact.generate.generator.AbstractGenerator.to_integration_json] + [`AbstractGenerator.to_integration_json`][AbstractGenerator.to_integration_json] for more information. """ @@ -102,7 +102,7 @@ def to_integration_json(self) -> dict[str, Any]: Convert the generator to an integration JSON object. See - [`AbstractGenerator.to_integration_json`][pact.generate.generator.AbstractGenerator.to_integration_json] + [`AbstractGenerator.to_integration_json`][AbstractGenerator.to_integration_json] for more information. """ return { @@ -115,7 +115,7 @@ def to_generator_json(self) -> dict[str, Any]: Convert the generator to a generator JSON object. See - [`AbstractGenerator.to_generator_json`][pact.generate.generator.AbstractGenerator.to_generator_json] + [`AbstractGenerator.to_generator_json`][AbstractGenerator.to_generator_json] for more information. """ return { diff --git a/src/pact/interaction/__init__.py b/src/pact/interaction/__init__.py index e5de3445d..83954346d 100644 --- a/src/pact/interaction/__init__.py +++ b/src/pact/interaction/__init__.py @@ -2,7 +2,7 @@ Interaction module. This module defines the classes that are used to define individual interactions -within a [`Pact`][pact.pact.Pact] between a consumer and a provider. These +within a [`Pact`][pact.Pact] between a consumer and a provider. These interactions can be of different types, such as HTTP requests, synchronous messages, or asynchronous messages. diff --git a/src/pact/interaction/_base.py b/src/pact/interaction/_base.py index bb962ce46..4d478b9b4 100644 --- a/src/pact/interaction/_base.py +++ b/src/pact/interaction/_base.py @@ -34,9 +34,9 @@ class Interaction(abc.ABC): provider. The concrete subclasses define the type of interaction, and include: - - [`HttpInteraction`][pact.interaction.HttpInteraction] - - [`AsyncMessageInteraction`][pact.interaction.AsyncMessageInteraction] - - [`SyncMessageInteraction`][pact.interaction.SyncMessageInteraction] + - [`HttpInteraction`][HttpInteraction] + - [`AsyncMessageInteraction`][AsyncMessageInteraction] + - [`SyncMessageInteraction`][SyncMessageInteraction] # Interaction Part @@ -244,8 +244,7 @@ def with_binary_body( Adds a binary body to the request or response. Note that for HTTP interactions, this function will overwrite the body - if it has been set using - [`with_body`][pact.interaction.Interaction.with_body]. + if it has been set using [`with_body`][with_body]. Args: body: @@ -295,8 +294,8 @@ def with_metadata( The values must be serializable to JSON using [`json.dumps`][json.dumps] and may contain matchers and generators. If you wish to use a valid JSON-encoded string as a metadata value, prefer the - [`set_metadata`][pact.interaction.Interaction.set_metadata] method as - this does not perform any additional parsing of the string. + [`set_metadata`][set_metadata] method as this does not perform any + additional parsing of the string. Args: metadata: @@ -340,10 +339,9 @@ def set_metadata( """ Add metadata for the interaction. - This function behaves exactly like - [`with_metadata`][pact.interaction.Interaction.with_metadata] but does - not perform any parsing of the value strings. The strings must be valid - JSON-encoded strings. + This function behaves exactly like [`with_metadata`][with_metadata] but + does not perform any parsing of the value strings. The strings must be + valid JSON-encoded strings. The value of `None` will remove the metadata key from the interaction. This is distinct from using an empty string or a string containing the @@ -452,7 +450,8 @@ def set_comment(self, key: str, value: Any | None) -> Self: # noqa: ANN401 # Warning This function will overwrite any existing comment with the same key. In - particular, the `text` key is used by `add_text_comment`. + particular, the `text` key is used by + [`add_text_comment`][add_text_comment]. """ if isinstance(value, str) or value is None: pact_ffi.set_comment(self._handle, key, value) @@ -475,7 +474,7 @@ def add_text_comment(self, comment: str) -> Self: Internally, the comments are appended to an array under the `text` comment key. Care should be taken to ensure that conflicts are not - introduced by [`set_comment`][pact.interaction.Interaction.set_comment]. + introduced by [`set_comment`][set_comment]. """ pact_ffi.add_text_comment(self._handle, comment) return self diff --git a/src/pact/interaction/_http_interaction.py b/src/pact/interaction/_http_interaction.py index 15b8a8e93..777abf9c3 100644 --- a/src/pact/interaction/_http_interaction.py +++ b/src/pact/interaction/_http_interaction.py @@ -32,8 +32,7 @@ class HttpInteraction(Interaction): HTTP interaction. As many elements are shared between the request and response, this class provides a common interface for both. The functions intelligently determine whether the element should be added to the request - or the response based on whether - [`will_respond_with`][pact.interaction.HttpInteraction.will_respond_with] + or the response based on whether [`will_respond_with`][will_respond_with] has been called. For example, the following two interactions are equivalent: @@ -165,9 +164,8 @@ def with_header( If `None`, then the function intelligently determines whether the header should be added to the request or the response, based - on whether the - [`will_respond_with`][pact.interaction.HttpInteraction.will_respond_with] - method has been called. + on whether the [`will_respond_with`][will_respond_with] method + has been called. """ interaction_part = self._parse_interaction_part(part) name_lower = name.lower() @@ -204,10 +202,10 @@ def with_headers( - Passing in an iterable of key-value tuples. - Make multiple calls to this function or - [`with_header`][pact.interaction.HttpInteraction.with_header]. + [`with_header`][with_header]. See - [`with_header`][pact.interaction.HttpInteraction.with_header] + [`with_header`][with_header] for more information. Args: @@ -220,9 +218,8 @@ def with_headers( If `None`, then the function intelligently determines whether the header should be added to the request or the response, based - on whether the - [`will_respond_with`][pact.interaction.HttpInteraction.will_respond_with] - method has been called. + on whether the [`will_respond_with`][will_respond_with] method + has been called. """ if isinstance(headers, dict): headers = headers.items() @@ -239,10 +236,9 @@ def set_header( r""" Add a header to the request. - Unlike - [`with_header`][pact.interaction.HttpInteraction.with_header], this - function does no additional processing of the header value. This is - useful for headers that contain a JSON object. + Unlike [`with_header`][with_header], this function does no additional + processing of the header value. This is useful for headers that contain + a JSON object. Args: name: @@ -257,9 +253,8 @@ def set_header( If `None`, then the function intelligently determines whether the header should be added to the request or the response, based - on whether the - [`will_respond_with`][pact.interaction.HttpInteraction.will_respond_with] - method has been called. + on whether the [`will_respond_with`][will_respond_with] method + has been called. """ pact_ffi.set_header( self._handle, @@ -287,10 +282,9 @@ def set_headers( - Passing in an iterable of key-value tuples. - Make multiple calls to this function or - [`with_header`][pact.interaction.HttpInteraction.with_header]. + [`with_header`][with_header]. - See [`set_header`][pact.interaction.HttpInteraction.set_header] for - more information. + See [`set_header`][set_header] for more information. Args: headers: @@ -302,8 +296,7 @@ def set_headers( If `None`, then the function intelligently determines whether the headers should be added to the request or the response, - based on whether the - [`will_respond_with`][pact.interaction.HttpInteraction.will_respond_with] + based on whether the [`will_respond_with`][will_respond_with] method has been called. """ if isinstance(headers, dict): @@ -372,11 +365,9 @@ def with_query_parameters( - Passing in an iterable of key-value tuples. - Make multiple calls to this function or - [`with_query_parameter`][pact.interaction.HttpInteraction.with_query_parameter]. + [`with_query_parameter`][with_query_parameter]. - See - [`with_query_parameter`][pact.interaction.HttpInteraction.with_query_parameter] - for more information. + See [`with_query_parameter`][with_query_parameter] for more information. Args: parameters: @@ -393,9 +384,8 @@ def will_respond_with(self, status: int) -> Self: Set the response status. Ideally, this function is called once all of the request information has - been set. This allows functions such as - [`with_header`][pact.interaction.HttpInteraction.with_header] - to intelligently determine whether this is a request or response header. + been set. This allows functions such as [`with_header`][with_header] to + intelligently determine whether this is a request or response header. Alternatively, the `part` argument can be used to explicitly specify whether the header should be added to the request or the response. diff --git a/src/pact/interaction/_sync_message_interaction.py b/src/pact/interaction/_sync_message_interaction.py index 60df05418..a74c6da90 100644 --- a/src/pact/interaction/_sync_message_interaction.py +++ b/src/pact/interaction/_sync_message_interaction.py @@ -18,7 +18,7 @@ class SyncMessageInteraction(Interaction): A synchronous message interaction. This class defines a synchronous message interaction between a consumer and - a provider. As with [`HttpInteraction`][pact.pact.HttpInteraction], it + a provider. As with [`HttpInteraction`][HttpInteraction], it defines a specific request that the consumer makes to the provider, and the response that the provider should return. """ @@ -29,8 +29,8 @@ def __init__(self, pact_handle: pact_ffi.PactHandle, description: str) -> None: This function should not be called directly. Instead, an AsyncMessageInteraction should be created using the - [`upon_receiving`][pact.Pact.upon_receiving] method of a - [`Pact`][pact.Pact] instance using the `"Sync"` interaction type. + [`upon_receiving`][Pact.upon_receiving] method of a + [`Pact`][Pact] instance using the `"Sync"` interaction type. Args: pact_handle: @@ -67,9 +67,8 @@ def will_respond_with(self) -> Self: This method is a convenience method to separate the request and response parts of the interaction. This function is analogous to the - [`will_respond_with()`][pact.pact.HttpInteraction.will_respond_with] - method of the [`HttpInteraction`][pact.pact.HttpInteraction] class, - albeit more generic for synchronous message interactions. + [`HttpInteraction.will_respond_with()`][HttpInteraction.will_respond_with] + method, albeit more generic for synchronous message interactions. For example, the following two snippets are equivalent: diff --git a/src/pact/match/__init__.py b/src/pact/match/__init__.py index 0d0503b7c..073ea7a57 100644 --- a/src/pact/match/__init__.py +++ b/src/pact/match/__init__.py @@ -383,7 +383,7 @@ def integer( max: builtins.int | None = None, ) -> AbstractMatcher[builtins.int]: """ - Alias for [`match.int`][pact.match.int]. + Alias for [`match.int`][int]. """ return int(value, min=min, max=max) @@ -428,7 +428,7 @@ def decimal( precision: builtins.int | None = None, ) -> AbstractMatcher[_NumberT]: """ - Alias for [`match.float`][pact.match.float]. + Alias for [`match.float`][float]. """ return float(value, precision=precision) @@ -592,7 +592,7 @@ def string( generator: AbstractGenerator | None = None, ) -> AbstractMatcher[builtins.str]: """ - Alias for [`match.str`][pact.match.str]. + Alias for [`match.str`][str]. """ return str(value, size=size, generator=generator) @@ -709,7 +709,7 @@ def bool(value: builtins.bool | Unset = UNSET, /) -> AbstractMatcher[builtins.bo def boolean(value: builtins.bool | Unset = UNSET, /) -> AbstractMatcher[builtins.bool]: """ - Alias for [`match.bool`][pact.match.bool]. + Alias for [`match.bool`][bool]. """ return bool(value) @@ -915,7 +915,7 @@ def timestamp( disable_conversion: builtins.bool = False, ) -> AbstractMatcher[builtins.str]: """ - Alias for [`match.datetime`][pact.match.datetime]. + Alias for [`match.datetime`][datetime]. """ return datetime(value, format, disable_conversion=disable_conversion) @@ -929,7 +929,7 @@ def none() -> AbstractMatcher[None]: def null() -> AbstractMatcher[None]: """ - Alias for [`match.none`][pact.match.none]. + Alias for [`match.none`][none]. """ return none() @@ -996,7 +996,7 @@ def like( generator: AbstractGenerator | None = None, ) -> AbstractMatcher[_T]: """ - Alias for [`match.type`][pact.match.type]. + Alias for [`match.type`][type]. """ return type(value, min=min, max=max, generator=generator) diff --git a/src/pact/match/matcher.py b/src/pact/match/matcher.py index d770a9f26..74c747eff 100644 --- a/src/pact/match/matcher.py +++ b/src/pact/match/matcher.py @@ -176,7 +176,7 @@ def to_integration_json(self) -> dict[str, Any]: Convert the matcher to an integration JSON object. See - [`AbstractMatcher.to_integration_json`][pact.match.matcher.AbstractMatcher.to_integration_json] + [`AbstractMatcher.to_integration_json`][AbstractMatcher.to_integration_json] for more information. """ return { @@ -195,7 +195,7 @@ def to_matching_rule(self) -> dict[str, Any]: Convert the matcher to a matching rule. See - [`AbstractMatcher.to_matching_rule`][pact.match.matcher.AbstractMatcher.to_matching_rule] + [`AbstractMatcher.to_matching_rule`][AbstractMatcher.to_matching_rule] for more information. """ return { @@ -230,7 +230,7 @@ def to_integration_json(self) -> dict[str, Any]: Convert the matcher to an integration JSON object. See - [`AbstractMatcher.to_integration_json`][pact.match.matcher.AbstractMatcher.to_integration_json] + [`AbstractMatcher.to_integration_json`][AbstractMatcher.to_integration_json] for more information. """ return self._matcher.to_integration_json() @@ -240,7 +240,7 @@ def to_matching_rule(self) -> dict[str, Any]: Convert the matcher to a matching rule. See - [`AbstractMatcher.to_matching_rule`][pact.match.matcher.AbstractMatcher.to_matching_rule] + [`AbstractMatcher.to_matching_rule`][AbstractMatcher.to_matching_rule] for more information. """ return self._matcher.to_matching_rule() @@ -279,7 +279,7 @@ def to_integration_json(self) -> dict[str, Any]: Convert the matcher to an integration JSON object. See - [`AbstractMatcher.to_integration_json`][pact.match.matcher.AbstractMatcher.to_integration_json] + [`AbstractMatcher.to_integration_json`][AbstractMatcher.to_integration_json] for more information. """ return self._matcher.to_integration_json() @@ -289,7 +289,7 @@ def to_matching_rule(self) -> dict[str, Any]: Convert the matcher to a matching rule. See - [`AbstractMatcher.to_matching_rule`][pact.match.matcher.AbstractMatcher.to_matching_rule] + [`AbstractMatcher.to_matching_rule`][AbstractMatcher.to_matching_rule] for more information. """ return self._matcher.to_matching_rule() @@ -328,7 +328,7 @@ def to_integration_json(self) -> dict[str, Any]: Convert the matcher to an integration JSON object. See - [`AbstractMatcher.to_integration_json`][pact.match.matcher.AbstractMatcher.to_integration_json] + [`AbstractMatcher.to_integration_json`][AbstractMatcher.to_integration_json] for more information. """ return self._matcher.to_integration_json() @@ -338,7 +338,7 @@ def to_matching_rule(self) -> dict[str, Any]: Convert the matcher to a matching rule. See - [`AbstractMatcher.to_matching_rule`][pact.match.matcher.AbstractMatcher.to_matching_rule] + [`AbstractMatcher.to_matching_rule`][AbstractMatcher.to_matching_rule] for more information. """ return self._matcher.to_matching_rule() @@ -387,7 +387,7 @@ def to_integration_json(self) -> dict[str, Any]: Convert the matcher to an integration JSON object. See - [`AbstractMatcher.to_integration_json`][pact.match.matcher.AbstractMatcher.to_integration_json] + [`AbstractMatcher.to_integration_json`][AbstractMatcher.to_integration_json] for more information. """ return {"pact:matcher:type": [m.to_integration_json() for m in self._matchers]} @@ -397,7 +397,7 @@ def to_matching_rule(self) -> dict[str, Any]: Convert the matcher to a matching rule. See - [`AbstractMatcher.to_matching_rule`][pact.match.matcher.AbstractMatcher.to_matching_rule] + [`AbstractMatcher.to_matching_rule`][AbstractMatcher.to_matching_rule] for more information. """ return { diff --git a/src/pact/pact.py b/src/pact/pact.py index f8aff745c..4fc922f37 100644 --- a/src/pact/pact.py +++ b/src/pact/pact.py @@ -112,8 +112,8 @@ class Pact: to the Pact. Each interaction between the consumer and the provider is defined through - the [`upon_receiving`][pact.pact.Pact.upon_receiving] method, which - returns a sub-class of [`Interaction`][pact.interaction.Interaction]. + the [`upon_receiving`][Pact.upon_receiving] method, which + returns a sub-class of [`Interaction`][interaction.Interaction]. """ def __init__( @@ -479,7 +479,7 @@ def verify( Returns: `None` if raises is True and no errors occurred, otherwise a list of - [`InteractionVerificationError`][pact.error.InteractionVerificationError]. + [`InteractionVerificationError`][error.InteractionVerificationError]. Raises: TypeError: @@ -569,7 +569,7 @@ class PactServer: stopping the mock server when the block is exited. Note that the server should not be started directly, but rather through the - [`serve`][pact.Pact.serve] method of a [`Pact`][pact.Pact]: + [`Pact.serve`][Pact.serve] method: ```python pact = Pact("consumer", "provider") @@ -578,9 +578,9 @@ class PactServer: ... ``` - The URL for the server can be accessed through its - [`url`][pact.pact.PactServer.url] attribute, which will be required in - order to point the consumer client to the mock server: + The URL for the server can be accessed through its [`url`][PactServer.url] + attribute, which will be required in order to point the consumer client to + the mock server: ```python pact = Pact("consumer", "provider") diff --git a/src/pact/verifier.py b/src/pact/verifier.py index 9c3b79340..529202444 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -355,12 +355,12 @@ def message_handler( 1. A fully fledged function that will be called for all messages. This is the most powerful option as it allows for full control over the message generation. The function's signature must be compatible with - the [`MessageProducerArgs`][pact.types.MessageProducerArgs] type. + the [`MessageProducerArgs`][types.MessageProducerArgs] type. 2. A dictionary mapping message names to either (a) producer functions, - (b) [`Message`][pact.types.Message] dictionaries, or (c) raw - bytes. If using a producer function, it must be compatible with the - [`MessageProducerArgs`][pact.types.MessageProducerArgs] type. + (b) [`Message`][types.Message] dictionaries, or (c) raw bytes. If + using a producer function, it must be compatible with the + [`MessageProducerArgs`][types.MessageProducerArgs] type. ## Implementation @@ -558,8 +558,8 @@ def state_handler( in Python. The function signature must be compatible with the - [`StateHandlerArgs`][pact.types.StateHandlerArgs]. If the function - has additional arguments, these must either have default values, or be + [`StateHandlerArgs`][types.StateHandlerArgs]. If the function has + additional arguments, these must either have default values, or be filled by using the [`partial`][functools.partial] function. Pact also uses a special state denoted with the empty string `""`. This @@ -879,7 +879,7 @@ def set_publish_options( Name of the branch used for verification. The first time a branch is set here or through - [`provider_branch`][pact.verifier.BrokerSelectorBuilder.provider_branch], + [`provider_branch`][BrokerSelectorBuilder.provider_branch], the value will be saved and use as a default for both. """ if not self._branch and branch: @@ -1189,12 +1189,11 @@ def broker_source( # noqa: PLR0913 By default, or if `selector=False`, this function returns the verifier instance to allow for method chaining. If `selector=True` is given, this - function returns a - [`BrokerSelectorBuilder`][pact.verifier.BrokerSelectorBuilder] instance - which allows for further configuration of the broker source in a fluent - interface. The [`build()`][pact.verifier.BrokerSelectorBuilder.build] - call is then used to finalise the broker source and return the verifier - instance for further configuration. + function returns a [`BrokerSelectorBuilder`][BrokerSelectorBuilder] + instance which allows for further configuration of the broker source in + a fluent interface. The [`build()`][BrokerSelectorBuilder.build] call is + then used to finalise the broker source and return the verifier instance + for further configuration. Args: url: @@ -1216,10 +1215,10 @@ def broker_source( # noqa: PLR0913 selector: Whether to return a - [BrokerSelectorBuilder][pact.verifier.BrokerSelectorBuilder] - instance. The builder instance allows for further configuration - of the broker source and must be finalised with a call to - [`build()`][pact.verifier.BrokerSelectorBuilder.build]. + [BrokerSelectorBuilder][BrokerSelectorBuilder] instance. The + builder instance allows for further configuration of the broker + source and must be finalised with a call to + [`build()`][BrokerSelectorBuilder.build]. use_env: Whether to read missing values from the environment variables. @@ -1441,8 +1440,8 @@ def provider_branch(self, branch: str) -> Self: Set the provider branch. The first time a branch is set here or through - [`set_publish_options`][pact.verifier.Verifier.set_publish_options], the - value will be saved and use as a default for both. + [`set_publish_options`][Verifier.set_publish_options], the value will be + saved and use as a default for both. """ self._provider_branch = branch if not self._verifier._branch: # type: ignore # noqa: PGH003, SLF001 From c4bc3e5a666ad50f335f41c2ebd8f3a4782d9f35 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 4 Dec 2025 11:18:16 +1100 Subject: [PATCH 1151/1376] chore(ci): use strict docs building This prevents warnings from going unnoticed. Signed-off-by: JP-Ellis --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 38a5b8779..67de7426f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -42,7 +42,7 @@ jobs: - name: Build docs run: | - hatch run mkdocs build + hatch run mkdocs build --strict - name: Upload artifact uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 From 1082b698b4dfdc4093dcf23f9839e53e11673525 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 4 Dec 2025 09:42:21 +1100 Subject: [PATCH 1152/1376] docs: add v3 blog post Signed-off-by: JP-Ellis --- .../2025/12-04 pact-python-v3-release.md | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 docs/blog/posts/2025/12-04 pact-python-v3-release.md diff --git a/docs/blog/posts/2025/12-04 pact-python-v3-release.md b/docs/blog/posts/2025/12-04 pact-python-v3-release.md new file mode 100644 index 000000000..702b31b96 --- /dev/null +++ b/docs/blog/posts/2025/12-04 pact-python-v3-release.md @@ -0,0 +1,158 @@ +--- +authors: + - JP-Ellis +date: + created: 2025-12-04 +--- + +# Announcing: Pact Python v3 + +It's been a couple of months since we released Pact Python v3, and after ironing out a couple of early issues, I think it's finally time to reflect on this milestone and its implications. This post is a look back at the journey, some of the challenges, the people, and the future of this project within the Pact ecosystem. + + + +Pact is an approach to contract testing that sits neatly between traditional unit tests (which check individual components) and end-to-end tests (which exercise the whole system). With Pact, you can verify that your services communicate correctly, without needing to spin up every dependency. By capturing the expected interactions between consumer and provider, Pact allows you to test each side in isolation and replay those interactions, giving you fast, reliable feedback and confidence that your APIs and microservices will work together in the real world. Pact Python brings this powerful workflow to the Python ecosystem, making it easy to test everything from REST APIs to event-driven systems. + +## Looking Back: Why v3? + +Pact has a diverse ecosystem, with SDKs in all major languages. Pact Python was (and still is) the most popular implementation of Pact for Python. As with many of the early Pact SDKs, Pact Python was built on top of the Pact Ruby codebase, as that was _the_ reference implementation of Pact. + +This came with a few problems: + +1. The reference implementation of Pact moved to [Rust](https://github.com/pact-foundation/pact-reference), and development for versions 3 and 4 of the Pact specification took place there, with limited features being backported to Pact Ruby. +2. It required bundling Ruby as part of the Python wheels, which significantly bloated distributions and slowed down Pact Python. +3. The Python code served primarily as a wrapper to calling the Ruby-based CLIs, and some aspects of that implementation were exposed to end-users, such as manually checking process exit codes, resulting in a non-"pythonic" experience. + +As the Pact specification evolved and the needs of our users grew, it became clear that the old architecture was starting to show its age. Supporting new features, keeping up with upstream changes, and maintaining compatibility across platforms was becoming increasingly difficult. + +Version 3 of Pact Python was an opportunity to do things differently. Not only could we move away from the Ruby dependency and unlock support for the latest Pact specifications, but we could also make Pact Python much more "pythonic." This meant embracing modern Python best practices: proper exception handling, context management, full typing, and a more intuitive API. The goal was to make the library feel natural for Python developers, whether they were new to Pact or contract testing veterans. + +With these objectives in mind, the development of v3 commenced. + +## The Journey: From Idea to Release + +Very early in the development of v3, it was clear to me that this was an opportunity to fundamentally rethink the library's architecture. While the core Pact idioms from the broader ecosystem have been retained, the internal flow and structure of Pact Python were comprehensively overhauled. This decision was not made lightly, as it does introduce a burden on end-users; however, I hoped this would provide significant long-term benefits for maintainability, extensibility, and user experience. Now looking back, I do think this was the right decision, and I'm glad that I was allowed to implement these changes even though I was a newcomer to Pact's ecosystem. + +Migrating a large codebase from Pact Python v2 to v3 is an onerous task. Accordingly, considerable effort was invested in ensuring compatibility and a smooth transition. This included the preparation of detailed migration guides, the parallel support of both v2 and v3 for an extended period, and the incorporation of feedback from early adopters who trialed the new version in production environments. The ongoing support for v2 alongside v3 is intended to allow users to migrate incrementally and at their own pace. + +The development process for v3 was iterative and, at times, complex. There were periods of rapid progress, such as the initial successful execution of contract tests using the new Rust core, as well as periods where platform-specific issues or subtle bugs required significant investigation and resolution (sometimes making me question my most basic reasoning abilities). Throughout, the primary objective remained to ensure that the new implementation not only matched the previous feature set, but also delivered tangible improvements in usability, reliability, and performance. + +The support of the PactFlow team at SmartBear was instrumental throughout this process, providing code reviews, testing, and guidance. The broader community also played a crucial role, contributing issues, pull requests, and practical insights that informed many of the design decisions. In particular, feedback and real-world testing from early adopters were invaluable during the preview and stabilization phases, helping to shape the final release. + +## What's New in v3? + +### Faster, Leaner, and More Reliable + +The move to a Rust FFI core is a game changer. Tests run faster, memory usage is lower, and the behaviour is aligned with most Pact SDKs in the ecosystem. I have already noticed significant speed-ups in test suites, and I hope end-users will notice this too. And with full support for both v3 and v4 of the Pact specification, you get access to the latest features, like asynchronous message support and improved matching rules, right out of the box. + +### A Truly Pythonic Experience + +Pact Python v3 is designed to feel like it belongs in the Python ecosystem. The API has been completely reimagined: context management, proper exception handling, and full type hints are now first-class citizens. The new interface is more intuitive, with less boilerplate and clearer error messages. + +Matchers are more expressive and flexible, making it easier to write robust, maintainable tests for even the most complex data structures. Provider state handling is now much more flexible too: you can use plain Python functions to manage test data and state, instead of relying on bespoke HTTP endpoints. + +All of this means writing and verifying contracts should feel natural, whether you're new to Pact or a seasoned pro. + +What does this look like in practice? Here's a side-by-side comparison of a simple Pact test in v2 and v3: + +```python title="Pact Python v2" +from pact.v2 import Consumer, Provider +import requests + +consumer = Consumer('my-web-front-end') +provider = Provider('my-backend-service') + +pact = consumer.has_pact_with(provider, pact_dir='/path/to/pacts') +( + pact + .given('user exists') # 1 + .upon_receiving('a request for user data') + .with_request( + 'GET', + '/users/123', + headers={'Accept': 'application/json'}, + query={'include': 'profile'} + ) + .will_respond_with( + 200, + headers={'Content-Type': 'application/json'}, + body={'id': 123, 'name': 'Alice'} + ) +) + +pact.start_service() # 2 +pact.setup() +response = requests.get(pact.uri + '/users/123') +assert response.json() == {'id': 123, 'name': 'Alice'} +pact.verify() # 3 +pact.stop_service() # 4 +# Pact file is written as part of verify() or when the service stops +``` + +1. Provider states in v2 are simple strings, which can lead to duplication if you need to test similar states with different parameters. +2. The mock service must be started manually before running tests. +3. Verification and Pact file writing are triggered explicitly. +4. The mock service must be stopped manually after tests. + +```python title="Pact Python v3" +from pact import Pact +import requests + +pact = Pact('my-web-front-end', 'my-backend-service') +( + pact + .upon_receiving('a request for user data') + .given('user exists', id=123, name='Alice') # 1 + .with_request('GET', '/users/123') + .with_header('Accept', 'application/json') + .with_query_parameter('include', 'profile') + .will_respond_with(200) + .with_header('Content-Type', 'application/json') + .with_body({'id': 123, 'name': 'Alice'}, content_type='application/json') +) + +with pact.serve() as srv: # 2 + response = requests.get(f"{srv.url}/users/123") + assert response.json() == {'id': 123, 'name': 'Alice'} +pact.write_file('/path/to/pacts') # 3 +``` + +1. In v3, provider states can be parameterized, making it easier to reuse and manage test data across different scenarios. +2. The new `serve()` method provides a more Pythonic and flexible way to run the mock service, automatically handling setup and teardown. +3. Pact files are written explicitly, giving you more control over when and where they are saved. + +### Migration, with You in Mind + +We know big upgrades can be daunting, especially for teams with large codebases. That's why v3 includes a backwards compatibility module: you can keep your old tests running while you gradually adopt the new API, at your own pace. Many of the changes in v3 came directly from user feedback; feature requests, bug reports, and discussions on Slack have all shaped this release. The transition is designed to be as smooth as possible, so you can take advantage of new features without disrupting your workflow. + +## Reflections and Gratitude + +No open source project is a solo effort, and Pact Python v3 is no exception. This release stands on the shoulders of a vibrant community, and I want to take a moment to recognize the many people who have shaped this project. + +**Special thanks to the contributors to the v3 codebase:** + +- **[valkolovos](https://github.com/valkolovos):** for his work on matchers, generators, asynchronous message support, and being an early adopter of Pact Python v3. +- **[Nikhil Arora](https://github.com/Nikhil172913832):** for a number of recent improvements, including improvements to the developer experience. +- **[Amit Singh](https://github.com/amit828as):** for expanding v3 HTTP interaction examples and real-world testing. +- **[Kevin Rohan Vaz](https://github.com/kevinrvaz):** For fixing and improving the v3 verifier. + +I would also like to acknowledge contributors to the (now legacy) v2 codebase and the original project: + +- **Core architecture and early development:** [Elliott Murray](https://github.com/elliottmurray), [Matthew Balvanz](https://github.com/matthewbalvanz-wf). +- **Message pact and provider support:** [Tuan Pham](https://github.com/tuan-pham), [William Infante](https://github.com/williaminfante), [Fabio Pulvirenti](https://github.com/pulphix). +- **Testing and verification:** [Peter Yasi](https://github.com/pyasi), S[imon Nizov](https://github.com/thatguysimon), [mikahjc](https://github.com/mikahjc), [Maciej Olko](https://github.com/m-aciek), [Matt Fellows](https://github.com/mefellows), [Janneck Wullschleger](https://github.com/jawu), [Rory Hart](https://github.com/hartror), [simkev2](https://github.com/SimKev2). +- **Other features, fixes, and support:** [B3nnyL](https://github.com/B3nnyL), [Yousaf Nabi](https://github.com/YOU54F), [Beth Skurrie](https://github.com/bethesque), [Francois Campbell](https://github.com/francoiscampbell), [Serghei Iakovlev](https://github.com/sergeyklay), and many others over the years. + +And certainly not least, a huge thank you to those who have helped with documentation, onboarding, and community support: + +- **Documentation and onboarding:** [Elliott Murray](https://github.com/elliottmurray), [Matthew Balvanz](https://github.com/matthewbalvanz-wf), [Yousaf Nabi](https://github.com/YOU54F), [Beth Skurrie](https://github.com/bethesque), [Matt Fellows](https://github.com/mefellows), [Serghei Iakovlev](https://github.com/sergeyklay). +- **Examples and reliability:** [mikegeeves](https://github.com/mikegeeves), [William Infante](https://github.com/williaminfante), [Artur Neumann](https://github.com/individual-it). +- **Community support and feedback:** and everyone who has opened issues, submitted PRs, or shared their experiences, on Slack, GitHub, and elsewhere! + +## Where to Next? + +With v3 as our new foundation, we are already seeing new features and integrations become possible that would have been out of reach before. If you are using Pact Python in your projects, I would welcome your stories, whether it's a simple 'we're using this', or more feedback on what is working, what could be improved, and what you would like to see next. + +If you are ready to get started, you will find everything you need in the [documentation](https://pact-foundation.github.io/pact-python/), including a [migration guide](https://pact-foundation.github.io/pact-python/MIGRATION/) for those moving from v2. The [GitHub repository](https://github.com/pact-foundation/pact-python) is always open for issues, discussions, and contributions. If you are new to Pact entirely, you can read more about it on [`pact.io`](https://pact.io/). + +Thank you for being part of this journey. Here's to a new chapter in contract testing for Python. Happy testing! From b958162e0fb7f5c2ecf81c47ba27e323f438de36 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 4 Dec 2025 18:24:16 +1100 Subject: [PATCH 1153/1376] docs: fix partial url highlight Signed-off-by: JP-Ellis --- docs/blog/posts/2025/12-04 pact-python-v3-release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blog/posts/2025/12-04 pact-python-v3-release.md b/docs/blog/posts/2025/12-04 pact-python-v3-release.md index 702b31b96..d649acfb4 100644 --- a/docs/blog/posts/2025/12-04 pact-python-v3-release.md +++ b/docs/blog/posts/2025/12-04 pact-python-v3-release.md @@ -140,7 +140,7 @@ I would also like to acknowledge contributors to the (now legacy) v2 codebase an - **Core architecture and early development:** [Elliott Murray](https://github.com/elliottmurray), [Matthew Balvanz](https://github.com/matthewbalvanz-wf). - **Message pact and provider support:** [Tuan Pham](https://github.com/tuan-pham), [William Infante](https://github.com/williaminfante), [Fabio Pulvirenti](https://github.com/pulphix). -- **Testing and verification:** [Peter Yasi](https://github.com/pyasi), S[imon Nizov](https://github.com/thatguysimon), [mikahjc](https://github.com/mikahjc), [Maciej Olko](https://github.com/m-aciek), [Matt Fellows](https://github.com/mefellows), [Janneck Wullschleger](https://github.com/jawu), [Rory Hart](https://github.com/hartror), [simkev2](https://github.com/SimKev2). +- **Testing and verification:** [Peter Yasi](https://github.com/pyasi), [Simon Nizov](https://github.com/thatguysimon), [mikahjc](https://github.com/mikahjc), [Maciej Olko](https://github.com/m-aciek), [Matt Fellows](https://github.com/mefellows), [Janneck Wullschleger](https://github.com/jawu), [Rory Hart](https://github.com/hartror), [simkev2](https://github.com/SimKev2). - **Other features, fixes, and support:** [B3nnyL](https://github.com/B3nnyL), [Yousaf Nabi](https://github.com/YOU54F), [Beth Skurrie](https://github.com/bethesque), [Francois Campbell](https://github.com/francoiscampbell), [Serghei Iakovlev](https://github.com/sergeyklay), and many others over the years. And certainly not least, a huge thank you to those who have helped with documentation, onboarding, and community support: From 7cc56bc6a06c35e961e607f8129fd603f096c2fa Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 4 Dec 2025 18:26:32 +1100 Subject: [PATCH 1154/1376] docs: fix tooltips in code Signed-off-by: JP-Ellis --- .../posts/2025/12-04 pact-python-v3-release.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/blog/posts/2025/12-04 pact-python-v3-release.md b/docs/blog/posts/2025/12-04 pact-python-v3-release.md index d649acfb4..c356de8eb 100644 --- a/docs/blog/posts/2025/12-04 pact-python-v3-release.md +++ b/docs/blog/posts/2025/12-04 pact-python-v3-release.md @@ -65,7 +65,7 @@ provider = Provider('my-backend-service') pact = consumer.has_pact_with(provider, pact_dir='/path/to/pacts') ( pact - .given('user exists') # 1 + .given('user exists') # (1) .upon_receiving('a request for user data') .with_request( 'GET', @@ -80,12 +80,12 @@ pact = consumer.has_pact_with(provider, pact_dir='/path/to/pacts') ) ) -pact.start_service() # 2 +pact.start_service() # (2) pact.setup() response = requests.get(pact.uri + '/users/123') assert response.json() == {'id': 123, 'name': 'Alice'} -pact.verify() # 3 -pact.stop_service() # 4 +pact.verify() # (3) +pact.stop_service() # (4) # Pact file is written as part of verify() or when the service stops ``` @@ -102,7 +102,7 @@ pact = Pact('my-web-front-end', 'my-backend-service') ( pact .upon_receiving('a request for user data') - .given('user exists', id=123, name='Alice') # 1 + .given('user exists', id=123, name='Alice') # (1) .with_request('GET', '/users/123') .with_header('Accept', 'application/json') .with_query_parameter('include', 'profile') @@ -111,10 +111,10 @@ pact = Pact('my-web-front-end', 'my-backend-service') .with_body({'id': 123, 'name': 'Alice'}, content_type='application/json') ) -with pact.serve() as srv: # 2 +with pact.serve() as srv: # (2) response = requests.get(f"{srv.url}/users/123") assert response.json() == {'id': 123, 'name': 'Alice'} -pact.write_file('/path/to/pacts') # 3 +pact.write_file('/path/to/pacts') # (3) ``` 1. In v3, provider states can be parameterized, making it easier to reuse and manage test data across different scenarios. From 8bd76577df65625c74cc7ba4f4492d97dc8425bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 08:52:25 +1100 Subject: [PATCH 1155/1376] chore(deps): update ruff to v0.14.8 (#1384) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 73731b223..a4a4820b0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.7 + rev: v0.14.8 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index f4da4aac3..5212afb19 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -51,7 +51,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.7", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.8", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.0"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index de606ee59..e23466ff7 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.14.7", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.8", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.0", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index ca86d024b..e57c8a4b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.14.7", + "ruff==0.14.8", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From 63d8ec6300bb8b89b831e7c50fa223dea33a3ca7 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 5 Dec 2025 09:01:06 +1100 Subject: [PATCH 1156/1376] docs: remove redundant header from blog post Signed-off-by: JP-Ellis --- docs/blog/posts/2025/12-04 pact-python-v3-release.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/blog/posts/2025/12-04 pact-python-v3-release.md b/docs/blog/posts/2025/12-04 pact-python-v3-release.md index c356de8eb..cd6d18e45 100644 --- a/docs/blog/posts/2025/12-04 pact-python-v3-release.md +++ b/docs/blog/posts/2025/12-04 pact-python-v3-release.md @@ -107,7 +107,6 @@ pact = Pact('my-web-front-end', 'my-backend-service') .with_header('Accept', 'application/json') .with_query_parameter('include', 'profile') .will_respond_with(200) - .with_header('Content-Type', 'application/json') .with_body({'id': 123, 'name': 'Alice'}, content_type='application/json') ) From cd7ae006ed2d3e1b36201d15c793504fc9724968 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:34:02 +1100 Subject: [PATCH 1157/1376] chore(deps): update peter-evans/create-pull-request action to v7.0.11 (#1386) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index c2052e91c..be8d9ddeb 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -201,7 +201,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 + uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 13691f8a4..c47cd8417 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -202,7 +202,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 + uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e4a089e30..7fd09c301 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -168,7 +168,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 + uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' From 681ecbc91d3cd150483f8e06b7aadaa80a60e895 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:42:27 +1100 Subject: [PATCH 1158/1376] chore(deps): update pre-commit hook davidanson/markdownlint-cli2 to v0.20.0 (#1387) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4a4820b0..00cb7ec2d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,7 +61,7 @@ repos: - id: committed - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.19.1 + rev: v0.20.0 hooks: - id: markdownlint-cli2 From c190ba1b5d783656b74d789d675eda5bf7fd4113 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:45:47 +1100 Subject: [PATCH 1159/1376] chore(deps): update astral-sh/setup-uv action to v7.1.5 (#1388) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index be8d9ddeb..f6a0d3c87 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index c47cd8417..a589ab6c4 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7fd09c301..da7ddb5a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 67de7426f..d18b30241 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66cc8230f..8b702d942 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 with: enable-cache: true @@ -154,7 +154,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 with: enable-cache: true @@ -197,7 +197,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 with: enable-cache: true @@ -229,7 +229,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 with: enable-cache: true @@ -262,7 +262,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 with: enable-cache: true @@ -302,7 +302,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 with: enable-cache: true cache-suffix: prek From 8c2a8fa8390ccd608cd2802b00f66044906c9909 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:46:21 +1100 Subject: [PATCH 1160/1376] chore(deps): update taiki-e/install-action action to v2.62.63 (#1389) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index f6a0d3c87..30b271143 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 + uses: taiki-e/install-action@50708e9ba8d7b6587a2cb575ddaa9a62e927bc06 # v2.62.63 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index a589ab6c4..f914d49e6 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 + uses: taiki-e/install-action@50708e9ba8d7b6587a2cb575ddaa9a62e927bc06 # v2.62.63 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da7ddb5a2..8d65a6de7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60 + uses: taiki-e/install-action@50708e9ba8d7b6587a2cb575ddaa9a62e927bc06 # v2.62.63 with: tool: git-cliff,typos From 8009ddba30207571fceb78c161d3e7196cd0f437 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 09:20:29 +1100 Subject: [PATCH 1161/1376] chore(deps): update python:3.14-slim docker digest to fd2aff3 (#1390) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index f6609f036..e54873378 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:b823ded4377ebb5ff1af5926702df2284e53cecbc6e3549e93a19d8632a1897e +FROM python:3.14-slim@sha256:fd2aff39e5a3ed23098108786a9966fb036fdeeedd487d5360e466bb2a84377b ARG USERNAME=vscode ARG USER_UID=1000 From f4f909a0ea9dc29eab551f68206e317e4e393176 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 10 Dec 2025 12:14:46 +1100 Subject: [PATCH 1162/1376] feat(cli): update to 2.5 Starting with 2.5, the Pact Standalone repo contains a number of Rust-based CLIs to replace the (now legacy) Ruby CLIs. Signed-off-by: JP-Ellis --- pact-python-cli/CHANGELOG.md | 2 +- pact-python-cli/README.md | 4 +-- pact-python-cli/cliff.toml | 2 +- pact-python-cli/pyproject.toml | 3 ++ pact-python-cli/src/pact_cli/__init__.py | 41 ++++++++++++++++++++++-- pact-python-cli/tests/test_init.py | 35 +++++++++++++------- 6 files changed, 69 insertions(+), 18 deletions(-) diff --git a/pact-python-cli/CHANGELOG.md b/pact-python-cli/CHANGELOG.md index b63bdebf2..68d3f3ec8 100644 --- a/pact-python-cli/CHANGELOG.md +++ b/pact-python-cli/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -Note that this _only_ includes changes to the Python re-packaging of the Pact CLI. For changes to the Pact CLI itself, see the [Pact CLI changelog](https://github.com/pact-foundation/pact-ruby-standalone/blob/master/CHANGELOG.md). +Note that this _only_ includes changes to the Python re-packaging of the Pact CLI. For changes to the Pact CLI itself, see the [Pact CLI changelog](https://github.com/pact-foundation/pact-standalone/blob/master/CHANGELOG.md). diff --git a/pact-python-cli/README.md b/pact-python-cli/README.md index 9db993466..4cf5e00e1 100644 --- a/pact-python-cli/README.md +++ b/pact-python-cli/README.md @@ -90,7 +90,7 @@ --- -This sub-package is part of the [Pact Python](https://github.com/pact-foundation/pact-python) project and exists solely to distribute the [Pact CLI](https://github.com/pact-foundation/pact-ruby-standalone) as a Python package. If you are looking for the main Pact Python library for contract testing, please see the [root package](https://github.com/pact-foundation/pact-python#pact-python). +This sub-package is part of the [Pact Python](https://github.com/pact-foundation/pact-python) project and exists solely to distribute the [Pact CLI](https://github.com/pact-foundation/pact-standalone) as a Python package. If you are looking for the main Pact Python library for contract testing, please see the [root package](https://github.com/pact-foundation/pact-python#pact-python). It is used by version 2 of Pact Python, and can be used to install the Pact CLI in Python environments. @@ -108,7 +108,7 @@ pip install pact-python-cli Contributions to this package are generally not required as it contains minimal Python functionality and generally only requires updating the version number. This is done by pushing a tag of the form `pact-python-cli/` which will automatically trigger a release build in the CI pipeline. -To contribute to the Pact CLI itself, please refer to the [Pact Ruby Standalone repository](https://github.com/pact-foundation/pact-ruby-standalone). +To contribute to the Pact CLI itself, please refer to the [Pact Ruby Standalone repository](https://github.com/pact-foundation/pact-standalone). For contributing to Pact Python, see the [main contributing guide](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md). diff --git a/pact-python-cli/cliff.toml b/pact-python-cli/cliff.toml index 8791e03d5..f8a223678 100644 --- a/pact-python-cli/cliff.toml +++ b/pact-python-cli/cliff.toml @@ -9,7 +9,7 @@ header = """ All notable changes to this project will be documented in this file. -Note that this _only_ includes changes to the Python re-packaging of the Pact CLI. For changes to the Pact CLI itself, see the [Pact CLI changelog](https://github.com/pact-foundation/pact-ruby-standalone/blob/master/CHANGELOG.md). +Note that this _only_ includes changes to the Python re-packaging of the Pact CLI. For changes to the Pact CLI itself, see the [Pact CLI changelog](https://github.com/pact-foundation/pact-standalone/blob/master/CHANGELOG.md). diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 5212afb19..85674b3f1 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -45,7 +45,10 @@ requires-python = ">=3.10" pact-mock-service = "pact_cli:_exec" pact-plugin-cli = "pact_cli:_exec" pact-provider-verifier = "pact_cli:_exec" + pact-stub-server = "pact_cli:_exec" pact-stub-service = "pact_cli:_exec" + pact_mock_server_cli = "pact_cli:_exec" + pact_verifier_cli = "pact_cli:_exec" pactflow = "pact_cli:_exec" [dependency-groups] diff --git a/pact-python-cli/src/pact_cli/__init__.py b/pact-python-cli/src/pact_cli/__init__.py index 34b577523..02c0fab1a 100644 --- a/pact-python-cli/src/pact_cli/__init__.py +++ b/pact-python-cli/src/pact_cli/__init__.py @@ -43,10 +43,16 @@ ) if TYPE_CHECKING: - from collections.abc import Mapping + from collections.abc import Container, Mapping _USE_SYSTEM_BINS = os.getenv("PACT_USE_SYSTEM_BINS", "").upper() in ("TRUE", "YES") _BIN_DIR = Path(__file__).parent.resolve() / "bin" +_LEGACY_BINS: Container[str] = frozenset(( + "pact-message", + "pact-mock-service", + "pact-provider-verifier", + "pact-stub-service", +)) def _telemetry_env() -> Mapping[str, str]: @@ -91,15 +97,26 @@ def _exec() -> None: "pact-broker", "pact-message", "pact-mock-service", - "pact-provider-verifier", "pact-plugin-cli", - "pact-publish", + "pact-provider-verifier", + "pact-stub-server", "pact-stub-service", + "pact_mock_server_cli", + "pact_verifier_cli", "pactflow", ): print("Unknown command:", command, file=sys.stderr) # noqa: T201 sys.exit(1) + if command in _LEGACY_BINS: + warnings.warn( + f"The '{command}' executable is deprecated and will be removed in " + "a future release. Please migrate to the new Pact CLI tools. " + "See: ", + DeprecationWarning, + stacklevel=2, + ) + if not _USE_SYSTEM_BINS: executable = _find_executable(command) else: @@ -173,6 +190,9 @@ def _find_executable(executable: str) -> str | None: BROKER_CLIENT_PATH = _find_executable("pact-broker") """ Path to the Pact Broker executable + +This value is identical to `BROKER_PATH` and is provided for backward +compatibility. """ MESSAGE_PATH = _find_executable("pact-message") """ @@ -190,10 +210,25 @@ def _find_executable(executable: str) -> str | None: """ Path to the Pact Provider Verifier executable """ +STUB_SERVER_PATH = _find_executable("pact-stub-server") +""" +Path to the Pact Stub Server executable +""" STUB_SERVICE_PATH = _find_executable("pact-stub-service") """ Path to the Pact Stub Service executable """ +MOCK_SERVER_PATH = _find_executable("pact_mock_server_cli") +""" +Path to the Pact Mock Server CLI executable +""" +VERIFIER_CLI_PATH = _find_executable("pact_verifier_cli") +""" +Path to the Pact Verifier CLI executable + +This is distinct to the `VERIFIER_PATH` which points to the older Ruby-based +CLI. +""" PACTFLOW_PATH = _find_executable("pactflow") """ Path to the PactFlow CLI executable diff --git a/pact-python-cli/tests/test_init.py b/pact-python-cli/tests/test_init.py index c22b40078..004ef8f35 100644 --- a/pact-python-cli/tests/test_init.py +++ b/pact-python-cli/tests/test_init.py @@ -56,17 +56,22 @@ def assert_in_sys_path(p: str | Path) -> None: @pytest.mark.parametrize( ("constant", "expected"), [ - pytest.param("PACT_PATH", "pact", id="pact"), - pytest.param("BROKER_PATH", "pact-broker", id="pact-broker"), pytest.param("BROKER_CLIENT_PATH", "pact-broker", id="pact-broker"), + pytest.param("BROKER_PATH", "pact-broker", id="pact-broker"), pytest.param("MESSAGE_PATH", "pact-message", id="pactmessage"), - pytest.param("MOCK_SERVICE_PATH", "pact-mock-service", id="pact-message"), + pytest.param( + "MOCK_SERVER_PATH", "pact_mock_server_cli", id="pact_mock_server_cli" + ), + pytest.param("MOCK_SERVICE_PATH", "pact-mock-service", id="pact-mock-service"), + pytest.param("PACTFLOW_PATH", "pactflow", id="pactflow"), + pytest.param("PACT_PATH", "pact", id="pact"), pytest.param("PLUGIN_CLI_PATH", "pact-plugin-cli", id="pact-plugin-cli"), + pytest.param("STUB_SERVER_PATH", "pact-stub-server", id="pact-stub-server"), + pytest.param("STUB_SERVICE_PATH", "pact-stub-service", id="pact-stub-service"), + pytest.param("VERIFIER_CLI_PATH", "pact_verifier_cli", id="pact_verifier_cli"), pytest.param( "VERIFIER_PATH", "pact-provider-verifier", id="pact-provider-verifier" ), - pytest.param("STUB_SERVICE_PATH", "pact-stub-service", id="pact-stub-service"), - pytest.param("PACTFLOW_PATH", "pactflow", id="pactflow"), ], ) def test_constants_are_valid_executable_paths(constant: str, expected: str) -> None: @@ -86,7 +91,10 @@ def test_constants_are_valid_executable_paths(constant: str, expected: str) -> N pytest.param("pact-message", id="pact-message"), pytest.param("pact-plugin-cli", id="pact-plugin-cli"), pytest.param("pact-provider-verifier", id="pact-provider-verifier"), + pytest.param("pact-stub-server", id="pact-stub-server"), pytest.param("pact-stub-service", id="pact-stub-service"), + pytest.param("pact_mock_server_cli", id="pact_mock_server_cli"), + pytest.param("pact_verifier_cli", id="pact_verifier_cli"), pytest.param("pactflow", id="pactflow"), ], ) @@ -154,7 +162,10 @@ def test_cli_exec_wrapper_for_mock_service() -> None: pytest.param("pact-mock-service", id="pact-mock-service"), pytest.param("pact-plugin-cli", id="pact-plugin-cli"), pytest.param("pact-provider-verifier", id="pact-provider-verifier"), + pytest.param("pact-stub-server", id="pact-stub-server"), pytest.param("pact-stub-service", id="pact-stub-service"), + pytest.param("pact_mock_server_cli", id="pact_mock_server_cli"), + pytest.param("pact_verifier_cli", id="pact_verifier_cli"), pytest.param("pactflow", id="pactflow"), ], ) @@ -167,21 +178,23 @@ def test_exec_directly(executable: str) -> None: with ( patch.object(sys, "argv", new=[executable, "--help"]), - patch("os.execv") as mock_execv, + patch("os.execve") as mock_execve, ): pact_cli._exec() # noqa: SLF001 - mock_execv.assert_called_once() - cmd, args = mock_execv.call_args[0] + mock_execve.assert_called_once() + cmd, args, env = mock_execve.call_args[0] assert (os.sep + executable) in cmd assert args == [cmd, "--help"] + assert env patch.object(sys, "argv", new=[executable]) with ( patch.object(sys, "argv", new=[executable]), - patch("os.execv") as mock_execv, + patch("os.execve") as mock_execve, ): pact_cli._exec() # noqa: SLF001 - mock_execv.assert_called_once() - cmd, args = mock_execv.call_args[0] + mock_execve.assert_called_once() + cmd, args, env = mock_execve.call_args[0] assert (os.sep + executable) in cmd assert args == [cmd] + assert env From fefddc181ee464ea8753d4d714101dc7af4cf36b Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 10 Dec 2025 16:14:24 +1100 Subject: [PATCH 1163/1376] fix(cli)!: remove 32-bit wheels BREAKING CHANGE: Wheels for 32-bit windows will no longer be provided. Signed-off-by: JP-Ellis --- pact-python-cli/hatch_build.py | 2 -- pact-python-cli/pyproject.toml | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pact-python-cli/hatch_build.py b/pact-python-cli/hatch_build.py index ffdb35931..da0e0f3c9 100644 --- a/pact-python-cli/hatch_build.py +++ b/pact-python-cli/hatch_build.py @@ -186,8 +186,6 @@ def _pact_bin_url(self, version: str) -> str: machine = "arm64" elif platform.endswith(("x86_64", "amd64")): machine = "x86_64" - elif platform.endswith(("i386", "i686", "x86", "win32")): - machine = "x86" else: raise UnsupportedPlatformError(platform) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 85674b3f1..81235b90b 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -189,6 +189,9 @@ exclude = '' # with false-positives missing libraries despite being bundled. repair-wheel-command = "" + [tool.cibuildwheel.windows] + archs = ["auto64"] + [[tool.cibuildwheel.overrides]] environment.MACOSX_DEPLOYMENT_TARGET = "10.13" select = "*-macosx_x86_64" From 3e23b2baa4e2c34cfa5bebe7b5ef712e056c75bc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 05:47:50 +0000 Subject: [PATCH 1164/1376] chore(deps): update codecov/codecov-action action to v5.5.2 (#1392) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b702d942..d00a4c9aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -113,7 +113,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests @@ -174,7 +174,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} flags: examples From 1bfacefecba9e09fa371200d2083c81e155216b3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 05:48:45 +0000 Subject: [PATCH 1165/1376] chore(deps): update peter-evans/create-pull-request action to v8 (#1393) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 30b271143..58a0b30aa 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -201,7 +201,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 + uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index f914d49e6..6f32d0426 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -202,7 +202,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 + uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8d65a6de7..b62e5821b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -168,7 +168,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 + uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' From 2f43c922a138576ebb598dcb284d6470f6196092 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 10 Dec 2025 14:26:37 +1100 Subject: [PATCH 1166/1376] chore: switch to versioningit I have been finding hatch-vcs (built on top of setuptools-scm) to be a bit too finicky, so I am switching to versioningit. Signed-off-by: JP-Ellis --- docs/releases.md | 4 +-- pact-python-cli/pyproject.toml | 51 +++++++++++++++---------------- pact-python-ffi/pyproject.toml | 53 ++++++++++++++++---------------- pyproject.toml | 55 +++++++++++++++++----------------- src/pact/__version__.pyi | 10 ------- 5 files changed, 83 insertions(+), 90 deletions(-) delete mode 100644 src/pact/__version__.pyi diff --git a/docs/releases.md b/docs/releases.md index ba8d34996..4bbc6f658 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -17,10 +17,10 @@ There are a couple of exceptions to the [semantic versioning](https://semver.org Any deviation from the the standard semantic versioning rules will be clearly documented in the release notes. -The version is stored in `pact/__version__.py`. This file is automatically generated by [`hatch-vcs`](https://pypi.org/project/hatch-vcs/) and generates a version based on the latest version tag and the number of commits since that tag. Specifically: +The version is stored in `pact/__version__.py`. This file is automatically generated by [`versioningit`](https://versioningit.readthedocs.io/en/stable/) and generates a version based on the latest version tag and the number of commits since that tag. Specifically: - If the latest tag is `v1.2.3` and there have been no commits since then and the repository is clean, the version will be `1.2.3`. -- Otherwise, the version will take the form of `1.2.3.dev{N}+g{hash}` (or `1.2.3.dev{N}+g{hash}.d{date}` if there's a dirty repository) where `N` is the number of commits since the latest tag, `hash` is the short hash of the latest commit. +- Otherwise, the version will take the form of a post-release based on the last version. ## Build Pipeline diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 81235b90b..55d9a7846 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -63,38 +63,19 @@ types = ["mypy==1.19.0"] ################################################################################ [build-system] build-backend = "hatchling.build" -requires = ["hatch-vcs", "hatchling", "packaging"] +requires = ["hatchling", "packaging", "versioningit"] [tool.hatch] [tool.hatch.version] - source = "vcs" - tag-pattern = '^pact-python-cli/(?P\d+(?:\.\d+)*(?:(?:a|b|rc|\.post)\d+)?)$' - - [tool.hatch.version.raw-options] - git_describe_command = [ - "git", - "describe", - "--tags", - "--dirty", - "--always", - "--long", - "--match", - "pact-python-cli/*", - ] - root = ".." - version_scheme = "no-guess-dev" + source = "versioningit" [tool.hatch.build] + artifacts = ["src/pact_cli/__version__.py"] + packages = ["src/pact_cli"] - [tool.hatch.build.hooks.vcs] - version-file = "src/pact_cli/__version__.py" - - [tool.hatch.build.targets.wheel] - packages = ["src/pact_cli"] - - [tool.hatch.build.targets.wheel.hooks.custom] - patch = "hatch_build.py" + [tool.hatch.build.targets.wheel.hooks.custom] + patch = "hatch_build.py" ######################################## ## Hatch Environment Configuration @@ -133,6 +114,26 @@ requires = ["hatch-vcs", "hatchling", "packaging"] [[tool.hatch.envs.test.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.14"] +################################################################################ +## Versioningit Configuration +################################################################################ +[tool.versioningit] + [tool.versioningit.vcs] + match = ["pact-python-cli/*"] + method = "git" + + [tool.versioningit.tag2version] + rmprefix = "pact-python-cli/" + + [tool.versioningit.write] + file = "src/pact_cli/__version__.py" + template = '''\ +# This file is auto-generated by versioningit. Do not edit. +__version__ = "{version}" +__version_tuple__ = {version_tuple} +__commit_id__ = "{revision}" +''' + ################################################################################ ## PyTest Configuration ################################################################################ diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index e23466ff7..e5c7ed2f4 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -50,38 +50,19 @@ types = ["mypy==1.19.0", "typing-extensions~=4.0"] ################################################################################ [build-system] build-backend = "hatchling.build" -requires = ["hatch-vcs", "hatchling", "packaging", "cffi"] +requires = ["hatchling", "packaging", "cffi", "setuptools", "versioningit"] [tool.hatch] [tool.hatch.version] - source = "vcs" - tag-pattern = '^pact-python-ffi/(?P\d+(?:\.\d+)*(?:(?:a|b|rc|\.post)\d+)?)$' - - [tool.hatch.version.raw-options] - git_describe_command = [ - "git", - "describe", - "--tags", - "--dirty", - "--always", - "--long", - "--match", - "pact-python-ffi/*", - ] - root = ".." - version_scheme = "no-guess-dev" + source = "versioningit" [tool.hatch.build] + artifacts = ["src/pact_ffi/__version__.py"] + packages = ["src/pact_ffi"] - [tool.hatch.build.hooks.vcs] - version-file = "src/pact_ffi/__version__.py" - - [tool.hatch.build.targets.wheel] - packages = ["src/pact_ffi"] - - [tool.hatch.build.targets.wheel.hooks.custom] - patch = "hatch_build.py" + [tool.hatch.build.targets.wheel.hooks.custom] + patch = "hatch_build.py" ######################################## ## Hatch Environment Configuration @@ -91,7 +72,7 @@ requires = ["hatch-vcs", "hatchling", "packaging", "cffi"] # Install dev dependencies in the default environment to simplify the developer # workflow. [tool.hatch.envs.default] - extra-dependencies = ["hatch-vcs", "hatchling", "packaging", "cffi"] + extra-dependencies = ["hatchling", "packaging", "cffi"] installer = "uv" path = ".venv" pre-install-commands = ["uv pip install --group dev -e ."] @@ -127,6 +108,26 @@ requires = ["hatch-vcs", "hatchling", "packaging", "cffi"] [[tool.hatch.envs.test.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.14"] +################################################################################ +## Versioningit Configuration +################################################################################ +[tool.versioningit] + [tool.versioningit.vcs] + match = ["pact-python-ffi/*"] + method = "git" + + [tool.versioningit.tag2version] + rmprefix = "pact-python-ffi/" + + [tool.versioningit.write] + file = "src/pact_ffi/__version__.py" + template = '''\ +# This file is auto-generated by versioningit. Do not edit. +__version__ = "{version}" +__version_tuple__ = {version_tuple} +__commit_id__ = "{revision}" +''' + ################################################################################ ## PyTest Configuration ################################################################################ diff --git a/pyproject.toml b/pyproject.toml index e57c8a4b9..1309b3200 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,40 +150,21 @@ test-v2 = [ "uvicorn[standard]~=0.0", ] -[build-system] -build-backend = "hatchling.build" -requires = ["hatch-vcs", "hatchling"] - ################################################################################ ## Hatch Configuration ################################################################################ +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling", "versioningit"] + [tool.hatch] [tool.hatch.version] - source = "vcs" - tag-pattern = '^pact-python/(?P\d+(?:\.\d+)*(?:(?:a|b|rc|\.post)\d+)?)$' - - [tool.hatch.version.raw-options] - git_describe_command = [ - "git", - "describe", - "--tags", - "--dirty", - "--always", - "--long", - "--match", - "pact-python/*", - ] - root = "." - version_scheme = "no-guess-dev" + source = "versioningit" [tool.hatch.build] - - [tool.hatch.build.hooks.vcs] - version-file = "src/pact/__version__.py" - - [tool.hatch.build.targets.wheel] - packages = ["/src/pact"] + artifacts = ["src/pact/__version__.py"] + packages = ["src/pact"] ######################################## ## Hatch Environment Configuration @@ -193,7 +174,7 @@ requires = ["hatch-vcs", "hatchling"] # Install dev dependencies in the default environment to simplify the developer # workflow. [tool.hatch.envs.default] - extra-dependencies = ["hatchling", "hatch-vcs"] + extra-dependencies = ["hatchling", "packaging"] installer = "uv" path = ".venv" # This is require to get around an incompatibility between hatch and uv @@ -272,6 +253,26 @@ requires = ["hatch-vcs", "hatchling"] [[tool.hatch.envs.v2-example.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.14"] +################################################################################ +## Versioningit Configuration +################################################################################ +[tool.versioningit] + [tool.versioningit.vcs] + match = ["pact-python/*"] + method = "git" + + [tool.versioningit.tag2version] + rmprefix = "pact-python/" + + [tool.versioningit.write] + file = "src/pact/__version__.py" + template = '''\ +# This file is auto-generated by versioningit. Do not edit. +__version__ = "{version}" +__version_tuple__ = {version_tuple} +__commit_id__ = "{revision}" +''' + ################################################################################ ## UV Workspace ################################################################################ diff --git a/src/pact/__version__.pyi b/src/pact/__version__.pyi deleted file mode 100644 index a019c2c56..000000000 --- a/src/pact/__version__.pyi +++ /dev/null @@ -1,10 +0,0 @@ -from typing import TypeAlias - -__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] - -VERSION_TUPLE: TypeAlias = tuple[int | str, ...] - -version: str -__version__: str -__version_tuple__: VERSION_TUPLE -version_tuple: VERSION_TUPLE From 1c478f3382c6ab1296411d49efdae50185b211cd Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 10 Dec 2025 14:04:42 +1100 Subject: [PATCH 1167/1376] fix(deps): update minimum cli version There was an update in the CLI package which Pact Python now depends on. Signed-off-by: JP-Ellis --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1309b3200..51eb073a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ dependencies = [ # Dependencies required for v2 only compat-v2 = [ # Pact dependencies - "pact-python-cli~=2.0", + "pact-python-cli~=2.5", # External dependencies "click~=8.0", "psutil~=7.0", From 793d5f64f81b592781cc623bc01a3e0c918fb5b6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 10 Dec 2025 14:03:05 +1100 Subject: [PATCH 1168/1376] chore(deps): add missing example dep Signed-off-by: JP-Ellis --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 51eb073a0..874af5282 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,6 +106,7 @@ example = [ "pytest-cov~=7.0", "pytest-rerunfailures~=16.0", "pytest~=9.0", + "python-multipart~=0.0", "testcontainers~=4.0", "uvicorn[standard]~=0.0", ] From f38176254585d23be299a9863d5cacacafb8a334 Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Wed, 10 Dec 2025 08:03:11 +0000 Subject: [PATCH 1169/1376] docs: update changelog for pact-python-cli/2.5.7.0 --- pact-python-cli/CHANGELOG.md | 44 ++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/pact-python-cli/CHANGELOG.md b/pact-python-cli/CHANGELOG.md index 68d3f3ec8..f21b80009 100644 --- a/pact-python-cli/CHANGELOG.md +++ b/pact-python-cli/CHANGELOG.md @@ -8,22 +8,52 @@ Note that this _only_ includes changes to the Python re-packaging of the Pact CL -## [pact-python-cli/2.4.26.2] _2025-07-23_ +## [pact-python-cli/2.5.7.0] _2025-12-10_ ### 🚀 Features -- Create pact-python-cli package -- _(cli)_ Build abi-agnostic wheels +- _(ffi)_ Add standalone ffi package +- _(v3)_ [**breaking**] Remove pact.v3.ffi module + > `pact.v3.ffi` is removed, and to be replaced by `pact_ffi`. That is, `pact.v3.ffi.$fn` should be replaced by `pact_ffi.$fn`. +- _(ffi)_ Upgrade lib to 0.4.28 + +### 🐛 Bug Fixes + +- Allow none in with_metadata +- _(ffi)_ Make version dynamic + +### 📚 Documentation + +- Update changelog for pact-python-ffi/0.4.22.0 +- _(ffi)_ Fix old references to pact.v3.ffi +- V3 review +- Update git cliff configuration +- Update changelog for pact-python-ffi/0.4.28.0 +- Update changelog for pact-python-ffi/0.4.28.1 +- Fix CI badge links +- Update changelogs ### ⚙️ Miscellaneous Tasks - Create cli and ffi packages -- _(ci)_ Add build cli pipeline -- Add git cliff configuration -- Properly extract tag version +- _(ffi)_ Cleanup build script +- Ignore extensions +- Split out dependencies and tests +- Support pre and post release tags +- Remove reference count checks +- Store hatch venv in .venv +- Fix sub-project git cliff config +- _(ffi)_ Clean up data directory +- [**breaking**] Drop python 3.9 add 3.14 + > Python 3.9 is no longer supported. +- Update non-compliant docstrings and types +- Upgrade pymdownx extensions +- Fix json schema url +- Remove ruff sub-configs +- Switch to versioningit ### Contributors - @JP-Ellis - + From b889925dd27452140c51f80be15d19b7b47594fd Mon Sep 17 00:00:00 2001 From: JP-Ellis <3196162+JP-Ellis@users.noreply.github.com> Date: Wed, 10 Dec 2025 08:13:10 +0000 Subject: [PATCH 1170/1376] docs: update changelog for pact-python/3.2.1 --- CHANGELOG.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a683cd9de..c2e2a140d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,26 @@ All notable changes to this project will be documented in this file. +## [pact-python/3.2.1] _2025-12-10_ + +### 📚 Documentation + +- Update changelog for pact-python/3.2.0 +- Fix internal references +- Add v3 blog post +- Fix partial url highlight +- Fix tooltips in code +- Remove redundant header from blog post + +### ⚙️ Miscellaneous Tasks + +- _(ci)_ Use strict docs building +- Switch to versioningit + +### Contributors + +- @JP-Ellis + ## [pact-python/3.2.0] _2025-12-02_ ### 🚀 Features @@ -1670,4 +1690,4 @@ All notable changes to this project will be documented in this file. - @matthewbalvanz-wf - @mefellows - + From 13043634f7a4d073d385d7c1e07a57c8730cc987 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:41:23 +1100 Subject: [PATCH 1171/1376] chore(deps): update ruff to v0.14.9 (#1402) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 00cb7ec2d..a5dc679e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.8 + rev: v0.14.9 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 55d9a7846..27786f330 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.8", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.9", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.0"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index e5c7ed2f4..49aae5cd8 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.14.8", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.9", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.0", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 874af5282..1c999867d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.14.8", + "ruff==0.14.9", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From e35a60c2624cd0f03a4bd6a2e22e3a1b97fbf77e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:44:44 +1100 Subject: [PATCH 1172/1376] chore(deps): update actions/cache action to v5 (#1403) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d00a4c9aa..ec66ee212 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -288,7 +288,7 @@ jobs: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Cache prek - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: |- ~/.cache/prek From 5358c8c60b5feb0c7eeadc342a3f05de0d3815a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:44:53 +1100 Subject: [PATCH 1173/1376] chore(deps): update python:3.14-slim docker digest to 2751cbe (#1404) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index e54873378..7bb862322 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:fd2aff39e5a3ed23098108786a9966fb036fdeeedd487d5360e466bb2a84377b +FROM python:3.14-slim@sha256:2751cbe93751f0147bc1584be957c6dd4c5f977c3d4e0396b56456a9fd4ed137 ARG USERNAME=vscode ARG USER_UID=1000 From 85630d566139b4d63c782f062226709f5ce2cdf1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:44:59 +1100 Subject: [PATCH 1174/1376] chore(deps): update github artifact actions (major) (#1405) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 6 +++--- .github/workflows/build-ffi.yml | 6 +++--- .github/workflows/build.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 58a0b30aa..48ce35903 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -70,7 +70,7 @@ jobs: run: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: wheels-sdist path: pact-python-cli/dist/*.tar* @@ -106,7 +106,7 @@ jobs: CIBW_BUILD: cp${{ env.STABLE_PYTHON_VERSION }}-* - name: Upload wheels - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: wheels-${{ matrix.os }} path: wheelhouse/*.whl @@ -179,7 +179,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - name: Download wheels and sdist - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: path: wheelhouse merge-multiple: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 6f32d0426..d33ac062a 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -70,7 +70,7 @@ jobs: run: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: wheels-sdist path: pact-python-ffi/dist/*.tar* @@ -107,7 +107,7 @@ jobs: HATCH_VERBOSE: '1' - name: Upload wheels - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: wheels-${{ matrix.os }} path: wheelhouse/*.whl @@ -180,7 +180,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - name: Download wheels and sdist - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: path: wheelhouse merge-multiple: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b62e5821b..654c4ac42 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: run: hatch build - name: Upload sdist - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: wheels-sdist path: ./dist/*.tar* @@ -76,7 +76,7 @@ jobs: compression-level: 0 - name: Upload wheel - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: wheels-whl path: ./dist/*.whl @@ -146,7 +146,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - name: Download wheels and sdist - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: path: wheelhouse merge-multiple: true From ddc31be812856bb3f8804c5aeacffa161b54d1b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:48:21 +1100 Subject: [PATCH 1175/1376] chore(deps): update astral-sh/setup-uv action to v7.1.6 (#1406) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 48ce35903..fb765f44a 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index d33ac062a..f78277266 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 654c4ac42..f1284112f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d18b30241..8b10145a0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec66ee212..eaceb5211 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 with: enable-cache: true @@ -154,7 +154,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 with: enable-cache: true @@ -197,7 +197,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 with: enable-cache: true @@ -229,7 +229,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 with: enable-cache: true @@ -262,7 +262,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 with: enable-cache: true @@ -302,7 +302,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5 + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 with: enable-cache: true cache-suffix: prek From 8a75c50d82a719c48b3dc78fee9d51059ac7424a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:47:44 +1100 Subject: [PATCH 1176/1376] chore(deps): update taiki-e/install-action action to v2.63.1 (#1407) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index fb765f44a..469089db7 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@50708e9ba8d7b6587a2cb575ddaa9a62e927bc06 # v2.62.63 + uses: taiki-e/install-action@61e5998d108b2b55a81b9b386c18bd46e4237e4f # v2.63.1 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index f78277266..d3ba6856a 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@50708e9ba8d7b6587a2cb575ddaa9a62e927bc06 # v2.62.63 + uses: taiki-e/install-action@61e5998d108b2b55a81b9b386c18bd46e4237e4f # v2.63.1 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1284112f..c2bb6f00b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@50708e9ba8d7b6587a2cb575ddaa9a62e927bc06 # v2.62.63 + uses: taiki-e/install-action@61e5998d108b2b55a81b9b386c18bd46e4237e4f # v2.63.1 with: tool: git-cliff,typos From 17d65c4b6d3ad82bfb38f42fc6b0978b4edb04a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:33:06 +1100 Subject: [PATCH 1177/1376] chore(deps): update dependency mypy to v1.19.1 (#1408) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 27786f330..46293325a 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -56,7 +56,7 @@ requires-python = ">=3.10" # developper consistency. All other dependencies are as above. dev = ["ruff==0.14.9", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] -types = ["mypy==1.19.0"] +types = ["mypy==1.19.1"] ################################################################################ ## Build System diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 49aae5cd8..b705c6018 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -43,7 +43,7 @@ dependencies = ["cffi~=2.0"] [dependency-groups] dev = ["ruff==0.14.9", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] -types = ["mypy==1.19.0", "typing-extensions~=4.0"] +types = ["mypy==1.19.1", "typing-extensions~=4.0"] ################################################################################ ## Build System diff --git a/pyproject.toml b/pyproject.toml index 1c999867d..c4bc61cb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ test = [ "testcontainers~=4.0", ] types = [ - "mypy==1.19.0", + "mypy==1.19.1", "types-grpcio~=1.0", "types-protobuf~=6.0", "types-requests~=2.0", From c5f9e5701a56ad81177e6c67f162b1af58ee3d29 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:33:20 +1100 Subject: [PATCH 1178/1376] chore(deps): update codecov/test-results-action action to v1.2.1 (#1409) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eaceb5211..a3bac18a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -120,7 +120,7 @@ jobs: - name: Upload test results if: ${{ !cancelled() }} - uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 + uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -181,7 +181,7 @@ jobs: - name: Upload test results if: ${{ !cancelled() }} - uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 + uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1 with: token: ${{ secrets.CODECOV_TOKEN }} From 02562fe0e1faef2f06bd388390158daa800db550 Mon Sep 17 00:00:00 2001 From: Nikhil Arora Date: Wed, 17 Dec 2025 08:54:17 +0530 Subject: [PATCH 1179/1376] feat: allow iteration over all interactions Add support to iterating over all interactions within a Pact. Co-authored-by: JP-Ellis --- pact-python-ffi/src/pact_ffi/__init__.py | 163 ++++++++++++++++++++++- pact-python-ffi/tests/test_init.py | 140 +++++++++++++++++++ src/pact/pact.py | 27 +++- tests/.ruff.toml | 1 + tests/test_pact.py | 151 +++++++++++++++++---- tests/test_util.py | 8 +- 6 files changed, 455 insertions(+), 35 deletions(-) diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index b9b7c3a5d..b0e36eb95 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -960,7 +960,48 @@ def port(self) -> int: return self._ref -class PactInteraction: ... +class PactInteraction: + """ + A Pact Interaction. + + This is a minimal implementation to support iteration over all interactions. + Full support requires additional upstream library development. + """ + + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: + """ + Initialise a new Pact Interaction. + + Args: + ptr: + CFFI data structure. + + owned: + Whether the interaction is owned by something else or not. This + determines whether the interaction should be freed when the + Python object is destroyed. + """ + self._ptr = ptr + self._owned = owned + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "PactInteraction" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"PactInteraction({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Interaction. + """ + if self._owned: + pass # pact_interaction_delete not implemented yet class PactInteractionIterator: @@ -1009,6 +1050,12 @@ def __del__(self) -> None: """ pact_interaction_iter_delete(self) + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + def __next__(self) -> PactInteraction: """ Get the next interaction from the iterator. @@ -1016,6 +1063,64 @@ def __next__(self) -> PactInteraction: return pact_interaction_iter_next(self) +class PactMessageIterator: + """ + Iterator over a Pact's interactions. + + Interactions encompasses all types of interactions, including HTTP + interactions and messages. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Pact Message Iterator. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct PactMessageIterator`. + """ + if ffi.typeof(ptr).cname != "struct PactMessageIterator *": + msg = ( + f"ptr must be a struct PactMessageIterator, got {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "PactMessageIterator" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"PactMessageIterator({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact Message Iterator. + """ + pact_message_iter_delete(self) + + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + + def __next__(self) -> PactInteraction: + """ + Get the next interaction from the iterator. + """ + return pact_message_iter_next(self) + + class PactSyncHttpIterator: """ Iterator over a Pact's synchronous HTTP interactions. @@ -4036,8 +4141,7 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction ptr = lib.pactffi_pact_interaction_iter_next(iter._ptr) if ptr == ffi.NULL: raise StopIteration - raise NotImplementedError - return PactInteraction(ptr) + return PactInteraction(ptr, owned=True) def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: @@ -4050,6 +4154,33 @@ def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: lib.pactffi_pact_interaction_iter_delete(iter._ptr) +def pact_message_iter_next(iter: PactMessageIterator) -> PactInteraction: + """ + Get the next interaction from the pact. + + [Rust + `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_message_iter_next) + + Raises: + StopIteration: + If the iterator has reached the end. + """ + ptr = lib.pactffi_pact_message_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return PactInteraction(ptr, owned=True) + + +def pact_message_iter_delete(iter: PactMessageIterator) -> None: + """ + Free the iterator when you're done using it. + + [Rust + `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_message_iter_delete) + """ + lib.pactffi_pact_message_iter_delete(iter._ptr) + + def matching_rule_to_json(rule: MatchingRule) -> str: """ Get the JSON form of the matching rule. @@ -6644,6 +6775,32 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: return PactSyncHttpIterator(lib.pactffi_pact_handle_get_sync_http_iter(pact._ref)) +def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: + r""" + Get an iterator over all the interactions of the Pact. + + The returned iterator needs to be freed with + `pactffi_pact_message_iter_delete`. + + [Rust + `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_handle_get_message_iter) + + # Safety + + The iterator contains a copy of the Pact, so it is always safe to use. + + # Error Handling + + On failure, this function will return a NULL pointer. + + This function may fail if any of the Rust strings contain embedded null + ('\0') bytes. + """ + return PactMessageIterator( + lib.pactffi_pact_handle_get_message_iter(pact._ref), + ) + + def pact_handle_write_file( pact: PactHandle, directory: Path | str | None, diff --git a/pact-python-ffi/tests/test_init.py b/pact-python-ffi/tests/test_init.py index 3faf3ff9f..13e6ea5b1 100644 --- a/pact-python-ffi/tests/test_init.py +++ b/pact-python-ffi/tests/test_init.py @@ -77,3 +77,143 @@ def test_owned_string() -> None: "-----END CERTIFICATE-----\r\n", ), ) + + +class TestInteractionIteration: + """ + Test interaction iteration functionality. + """ + + def test_pact_interaction(self) -> None: + """Test PactInteraction class.""" + pact = pact_ffi.new_pact("consumer", "provider") + pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + + # Create HTTP interaction + pact_ffi.new_sync_message_interaction(pact, "test") + + # Get interactions via iterator + sync_http_iter = pact_ffi.pact_handle_get_sync_http_iter(pact) + list(sync_http_iter) + # Test string representation works on iterator + assert "PactSyncHttpIterator" in str(sync_http_iter) or str(sync_http_iter) + + def test_pact_message_iterator(self) -> None: + """Test PactMessageIterator class.""" + pact = pact_ffi.new_pact("consumer", "provider") + pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + + # Create message interaction + pact_ffi.new_message_interaction(pact, "test message") + + # Get message iterator + iterator = pact_ffi.pact_handle_get_message_iter(pact) + + # Test string representation + assert "PactMessageIterator" in str(iterator) + assert "PactMessageIterator" in repr(iterator) + + # Iterate and count messages + message_count = sum(1 for _ in iterator) + + # Should have the message + assert message_count >= 1 + + def test_pact_interaction_owned(self) -> None: + """Test PactInteraction with owned parameter.""" + pact = pact_ffi.new_pact("consumer", "provider") + pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + pact_ffi.new_sync_message_interaction(pact, "test") + + # Get an interaction through the iterator + sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact) + for interaction in sync_iter: + # Interaction should be owned by the iterator + # Test destructor doesn't crash + del interaction + break + + def test_pact_message_iterator_empty(self) -> None: + """Test PactMessageIterator with no messages.""" + pact = pact_ffi.new_pact("consumer", "provider") + pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + + iterator = pact_ffi.pact_handle_get_message_iter(pact) + + # Should iterate zero times + message_count = sum(1 for _ in iterator) + assert message_count == 0 + + def test_pact_interaction_iterator_next(self) -> None: + """Test iterator next functions.""" + pact = pact_ffi.new_pact("consumer", "provider") + pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + + # Create multiple interactions + pact_ffi.new_interaction(pact, "http") + pact_ffi.new_message_interaction(pact, "async") + pact_ffi.new_sync_message_interaction(pact, "sync") + + # Test each iterator type + http_iter = pact_ffi.pact_handle_get_sync_http_iter(pact) + http_count = sum(1 for _ in http_iter) + assert http_count == 1 + + async_iter = pact_ffi.pact_handle_get_async_message_iter(pact) + async_count = sum(1 for _ in async_iter) + assert async_count == 1 + + sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact) + sync_count = sum(1 for _ in sync_iter) + assert sync_count == 1 + + def test_pact_message_iterator_repr(self) -> None: + """Test PactMessageIterator __repr__ method.""" + pact = pact_ffi.new_pact("consumer", "provider") + pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + + iterator = pact_ffi.pact_handle_get_message_iter(pact) + repr_str = repr(iterator) + + assert "PactMessageIterator" in repr_str + assert "0x" in repr_str or ">" in repr_str + + def test_pact_interaction_str_repr(self) -> None: + """Test PactInteraction __str__ and __repr__ methods.""" + pact = pact_ffi.new_pact("consumer", "provider") + pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + pact_ffi.new_sync_message_interaction(pact, "test") + + # Get an interaction from iterator + sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact) + for interaction in sync_iter: + str_result = str(interaction) + repr_result = repr(interaction) + + assert "SynchronousMessage" in str_result + assert "SynchronousMessage" in repr_result + break + + def test_multiple_iterator_types_simultaneously(self) -> None: + """Test using multiple iterator types at the same time.""" + pact = pact_ffi.new_pact("consumer", "provider") + pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + + # Create one of each type + pact_ffi.new_interaction(pact, "http") + pact_ffi.new_message_interaction(pact, "async") + pact_ffi.new_sync_message_interaction(pact, "sync") + + # Create all three iterators + http_iter = pact_ffi.pact_handle_get_sync_http_iter(pact) + async_iter = pact_ffi.pact_handle_get_async_message_iter(pact) + sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact) + + # Iterate through all of them + http_list = list(http_iter) + async_list = list(async_iter) + sync_list = list(sync_iter) + + assert len(http_list) == 1 + assert len(async_list) == 1 + assert len(sync_list) == 1 diff --git a/src/pact/pact.py b/src/pact/pact.py index 4fc922f37..b2af413aa 100644 --- a/src/pact/pact.py +++ b/src/pact/pact.py @@ -389,13 +389,32 @@ def interactions( kind: Literal["Async"], ) -> Generator[pact_ffi.AsynchronousMessage, None, None]: ... + @overload + def interactions( + self, + kind: Literal["All"], + ) -> Generator[ + pact_ffi.SynchronousHttp + | pact_ffi.SynchronousMessage + | pact_ffi.AsynchronousMessage, + None, + None, + ]: ... + def interactions( self, - kind: Literal["HTTP", "Sync", "Async"] = "HTTP", + kind: Literal["HTTP", "Sync", "Async", "All"] = "HTTP", ) -> ( Generator[pact_ffi.SynchronousHttp, None, None] | Generator[pact_ffi.SynchronousMessage, None, None] | Generator[pact_ffi.AsynchronousMessage, None, None] + | Generator[ + pact_ffi.SynchronousHttp + | pact_ffi.SynchronousMessage + | pact_ffi.AsynchronousMessage, + None, + None, + ] ): """ Return an iterator over the Pact's interactions. @@ -407,14 +426,16 @@ def interactions( ValueError: If the kind is unknown. """ - # TODO: Add an iterator for `All` interactions. - # https://github.com/pact-foundation/pact-python/issues/451 if kind == "HTTP": yield from pact_ffi.pact_handle_get_sync_http_iter(self._handle) elif kind == "Sync": yield from pact_ffi.pact_handle_get_sync_message_iter(self._handle) elif kind == "Async": yield from pact_ffi.pact_handle_get_async_message_iter(self._handle) + elif kind == "All": + yield from pact_ffi.pact_handle_get_sync_http_iter(self._handle) + yield from pact_ffi.pact_handle_get_sync_message_iter(self._handle) + yield from pact_ffi.pact_handle_get_async_message_iter(self._handle) else: msg = f"Unknown interaction type: {kind}" raise ValueError(msg) diff --git a/tests/.ruff.toml b/tests/.ruff.toml index 5bb2e803d..3558b7bba 100644 --- a/tests/.ruff.toml +++ b/tests/.ruff.toml @@ -5,6 +5,7 @@ extend = "../pyproject.toml" [lint] ignore = [ + "D102", # Require docstring in public methods "D103", # Require docstring in public function "D104", # Require docstring in public package "INP001", # Forbid implicit namespaces diff --git a/tests/test_pact.py b/tests/test_pact.py index 5fdb4e6e1..59c208382 100644 --- a/tests/test_pact.py +++ b/tests/test_pact.py @@ -4,6 +4,7 @@ from __future__ import annotations +import itertools import json from typing import TYPE_CHECKING, Literal @@ -72,31 +73,6 @@ def test_invalid_interaction(pact: Pact) -> None: pact.upon_receiving("a basic request", "Invalid") # type: ignore[call-overload] -@pytest.mark.parametrize( - "interaction_type", - [ - "HTTP", - "Sync", - "Async", - ], -) -def test_interactions_iter( - pact: Pact, - interaction_type: Literal[ - "HTTP", - "Sync", - "Async", - ], -) -> None: - interactions = pact.interactions(interaction_type) - assert interactions is not None - for _interaction in interactions: - # This should be an empty list and therefore the error should never be - # raised. - msg = "Should not be reached" - raise RuntimeError(msg) - - def test_write_file(pact: Pact, tmp_path: Path) -> None: pact.write_file(tmp_path) outfile = tmp_path / "consumer-provider.json" @@ -132,3 +108,128 @@ def test_specification(pact: Pact, version: str) -> None: def test_server_log(pact: Pact) -> None: with pact.serve() as srv: assert srv.logs is not None + + +class TestInteractionsIter: + """ + Collection of for `pact.interactions` iterator tests. + """ + + @staticmethod + def _interaction_count( + pact: Pact, + interaction_type: Literal["HTTP", "Sync", "Async", "All"], + ) -> int: + """ + Count the number of interactions for the requested type. + + Args: + pact: + Pact instance under test. + + interaction_type: + Interaction type to count (HTTP, Async, Sync, All). + + Returns: + Number of interactions that match the provided type. + """ + return sum(1 for _ in pact.interactions(interaction_type)) + + @pytest.mark.parametrize( + "interaction_type", + [ + "HTTP", + "Sync", + "Async", + "All", + ], + ) + def test_empty( + self, + pact: Pact, + interaction_type: Literal[ + "HTTP", + "Sync", + "Async", + "All", + ], + ) -> None: + interactions = pact.interactions(interaction_type) + assert interactions is not None + for _interaction in interactions: + # This should be an empty list and therefore the error should never be + # raised. + msg = "Should not be reached" + raise RuntimeError(msg) + + @classmethod + def _add_http_interaction(cls, pact: Pact, id_: int) -> None: + ( + pact.upon_receiving(f"HTTP request {id_}", "HTTP") + .with_request("GET", f"/{id_}") + .will_respond_with(200) + ) + + @classmethod + def _add_async_interaction(cls, pact: Pact, id_: int) -> None: + (pact.upon_receiving(f"Async message {id_}", "Async").with_body({"count": id_})) + + @classmethod + def _add_sync_interaction(cls, pact: Pact, id_: int) -> None: + ( + pact.upon_receiving(f"Sync message {id_}", "Sync") + .with_body(f"request {id_}") + .will_respond_with() + .with_body(f"response {id_}") + ) + + @classmethod + def _add_interactions(cls, pact: Pact, id_: int) -> None: + cls._add_http_interaction(pact, id_) + cls._add_async_interaction(pact, id_) + cls._add_sync_interaction(pact, id_) + + @pytest.mark.parametrize( + "version", + ["1", "1.1", "2", "3", "4"], + ) + def test_pact_versions(self, pact: Pact, version: str) -> None: + pact.with_specification(version) + self._add_interactions(pact, 1) + + assert self._interaction_count(pact, "HTTP") == 1 + assert self._interaction_count(pact, "Async") == 1 + assert self._interaction_count(pact, "Sync") == 1 + assert self._interaction_count(pact, "All") == 3 + + @pytest.mark.parametrize( + ("version", "http", "async_", "sync"), + itertools.product(["1", "1.1", "2", "3", "4"], range(3), range(3), range(3)), + ) + def test_mixed_interactions( + self, + pact: Pact, + version: str, + http: int, + async_: int, + sync: int, + ) -> None: + pact.with_specification(version) + for i in range(http): + self._add_http_interaction(pact, i) + for i in range(async_): + self._add_async_interaction(pact, i) + for i in range(sync): + self._add_sync_interaction(pact, i) + + # Verify the expected counts + assert self._interaction_count(pact, "HTTP") == http + assert self._interaction_count(pact, "Async") == async_ + assert self._interaction_count(pact, "Sync") == sync + assert self._interaction_count(pact, "All") == (http + async_ + sync) + + # Verify repeated iteration works + assert self._interaction_count(pact, "HTTP") == http + assert self._interaction_count(pact, "Async") == async_ + assert self._interaction_count(pact, "Sync") == sync + assert self._interaction_count(pact, "All") == (http + async_ + sync) diff --git a/tests/test_util.py b/tests/test_util.py index 612daa2ff..370e36cf5 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -147,7 +147,7 @@ class Foo: # noqa: D101 def __init__(self) -> None: # noqa: D107 pass - def __call__(self, a: int, b: str, c: float, d: bytes = b"d") -> Args: # noqa: D102 + def __call__(self, a: int, b: str, c: float, d: bytes = b"d") -> Args: return Args( args={"a": a, "b": b, "c": c, "d": d}, kwargs={}, @@ -155,7 +155,7 @@ def __call__(self, a: int, b: str, c: float, d: bytes = b"d") -> Args: # noqa: variadic_kwargs={}, ) - def method(self, a: int, b: str, c: float, d: bytes = b"d") -> Args: # noqa: D102 + def method(self, a: int, b: str, c: float, d: bytes = b"d") -> Args: return Args( args={"a": a, "b": b, "c": c, "d": d}, kwargs={}, @@ -164,7 +164,7 @@ def method(self, a: int, b: str, c: float, d: bytes = b"d") -> Args: # noqa: D1 ) @classmethod - def class_method(cls, a: int, b: str, c: float, d: bytes = b"d") -> Args: # noqa: D102 + def class_method(cls, a: int, b: str, c: float, d: bytes = b"d") -> Args: return Args( args={"a": a, "b": b, "c": c, "d": d}, kwargs={}, @@ -173,7 +173,7 @@ def class_method(cls, a: int, b: str, c: float, d: bytes = b"d") -> Args: # noq ) @staticmethod - def static_method(a: int, b: str, c: float, d: bytes = b"d") -> Args: # noqa: D102 + def static_method(a: int, b: str, c: float, d: bytes = b"d") -> Args: return Args( args={"a": a, "b": b, "c": c, "d": d}, kwargs={}, From b042ecafd51b54c34fd1d74850a83713fc0ac4e8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:21:59 +1100 Subject: [PATCH 1180/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.9 (#1410) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a5dc679e3..b7502c500 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.8 + rev: v2.3.9 hooks: - id: biome-check From e97d8dd12f623137adf1f7d259fb1193a6324151 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 19 Dec 2025 14:24:43 +1100 Subject: [PATCH 1181/1376] chore(ci): re-enable 3.14 tests These were temporarily disabled while waiting for Pydantic to catch up. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a3bac18a8..377322d05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -99,8 +99,6 @@ jobs: run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml - name: Run tests (v2) - # Temporary workaround until Pydantic 3.12 is released with Python 3.14 support - if: matrix.python-version != '3.14' run: hatch run v2-test.py${{ matrix.python-version }}:test --junit-xml=v2-junit.xml - name: Run tests (CLI) @@ -143,9 +141,7 @@ jobs: - '3.11' - '3.12' - '3.13' - # Temporarily excluded until Pydantic 3.12 is released with Python - # 3.14 support - # - '3.14' + - '3.14' steps: - name: Checkout code @@ -169,6 +165,7 @@ jobs: while IFS= read -r -d $'\0' file <&3; do cd "$(dirname "$file")" + echo "Running example in $(pwd)" uv run --python ${{ matrix.python-version }} --group test pytest --junit-xml=junit.xml done 3< <(find "$(pwd)/examples" -name pyproject.toml -print0) From 196a45ea42557baeae4a470e17c4cd974de0dee6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 04:12:32 +0000 Subject: [PATCH 1182/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.10 (#1412) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7502c500..037be3fd2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.9 + rev: v2.3.10 hooks: - id: biome-check From b588fd8b6bb65066be581fe9595950df7b611970 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 04:13:38 +0000 Subject: [PATCH 1183/1376] chore(deps): update dependency mkdocs-material to v9.7.1 (#1413) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c4bc61cb5..490722725 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,7 @@ docs = [ "mkdocs-github-admonitions-plugin==0.1.1", "mkdocs-literate-nav==0.6.2", "mkdocs-llmstxt==0.5.0", - "mkdocs-material[recommended,git,imaging]==9.7.0", + "mkdocs-material[recommended,git,imaging]==9.7.1", "mkdocs-section-index==0.3.10", "mkdocs==1.6.1", "mkdocstrings[python]==1.0.0", From 975414ffa1b7bc9f27eace7b59149762770f901b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 04:24:07 +0000 Subject: [PATCH 1184/1376] chore(deps): update ruff to v0.14.10 (#1414) Signed-off-by: JP-Ellis Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- docs/scripts/markdown.py | 3 +- docs/scripts/other.py | 3 +- docs/scripts/python.py | 3 +- .../multipart_matching_rules/test_consumer.py | 3 +- .../http/aiohttp_and_flask/test_consumer.py | 12 ++- .../requests_and_fastapi/test_consumer.py | 12 ++- examples/plugins/protobuf/test_consumer.py | 6 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- src/pact/__init__.py | 3 +- src/pact/interaction/_base.py | 3 +- src/pact/interaction/_http_interaction.py | 12 ++- src/pact/pact.py | 3 +- .../compatibility_suite/test_v3_generators.py | 47 ++++++----- .../compatibility_suite/test_v4_generators.py | 41 ++++++---- tests/interaction/test_http_interaction.py | 81 ++++++++++++------- .../test_sync_message_interaction.py | 12 ++- tests/test_error.py | 18 +++-- tests/test_match.py | 3 +- tests/test_pact.py | 6 +- tests/test_verifier.py | 3 +- 23 files changed, 184 insertions(+), 98 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 037be3fd2..fd4864076 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.9 + rev: v0.14.10 hooks: - id: ruff-check exclude: | diff --git a/docs/scripts/markdown.py b/docs/scripts/markdown.py index 6942592b9..a8af79d03 100644 --- a/docs/scripts/markdown.py +++ b/docs/scripts/markdown.py @@ -75,7 +75,8 @@ def process_markdown( ] files = sorted( Path(p) - for p in subprocess.check_output( # noqa: S603 + for p in subprocess # noqa: S603 + .check_output( ["git", "ls-files", src], # noqa: S607 ) .decode("utf-8") diff --git a/docs/scripts/other.py b/docs/scripts/other.py index eece99d5d..34bfde76e 100644 --- a/docs/scripts/other.py +++ b/docs/scripts/other.py @@ -36,7 +36,8 @@ ALL_FILES = sorted( map( Path, - subprocess.check_output(["git", "ls-files", SRC_ROOT]) # noqa: S603, S607 + subprocess # noqa: S603 + .check_output(["git", "ls-files", SRC_ROOT]) # noqa: S607 .decode("utf-8") .splitlines(), ), diff --git a/docs/scripts/python.py b/docs/scripts/python.py index 864e0174e..16954964d 100644 --- a/docs/scripts/python.py +++ b/docs/scripts/python.py @@ -165,7 +165,8 @@ def process_python( ignore_spec = PathSpec.from_lines("gitwildmatch", ignore or []) files = sorted( Path(p) - for p in subprocess.check_output( # noqa: S603 + for p in subprocess # noqa: S603 + .check_output( ["git", "ls-files", src], # noqa: S607 ) .decode("utf-8") diff --git a/examples/catalog/multipart_matching_rules/test_consumer.py b/examples/catalog/multipart_matching_rules/test_consumer.py index 40d9975a3..f95d678ae 100644 --- a/examples/catalog/multipart_matching_rules/test_consumer.py +++ b/examples/catalog/multipart_matching_rules/test_consumer.py @@ -133,7 +133,8 @@ def test_multipart_upload_with_matching_rules(pact: Pact) -> None: # Define the interaction with matching rules ( - pact.upon_receiving("a multipart upload with JSON metadata and image") + pact + .upon_receiving("a multipart upload with JSON metadata and image") .with_request("POST", "/upload") .with_header( "Content-Type", diff --git a/examples/http/aiohttp_and_flask/test_consumer.py b/examples/http/aiohttp_and_flask/test_consumer.py index aea42c0b5..e77e3f0fa 100644 --- a/examples/http/aiohttp_and_flask/test_consumer.py +++ b/examples/http/aiohttp_and_flask/test_consumer.py @@ -70,7 +70,8 @@ async def test_get_user(pact: Pact) -> None: "created_on": match.datetime(), } ( - pact.upon_receiving("A user request") + pact + .upon_receiving("A user request") .given("the user exists", id=123, name="Alice") .with_request("GET", "/users/123") .will_respond_with(200) @@ -94,7 +95,8 @@ async def test_get_unknown_user(pact: Pact) -> None: """ response = {"detail": "User not found"} ( - pact.upon_receiving("A request for an unknown user") + pact + .upon_receiving("A request for an unknown user") .given("the user doesn't exist", id=123) .with_request("GET", "/users/123") .will_respond_with(404) @@ -126,7 +128,8 @@ async def test_create_user(pact: Pact) -> None: } ( - pact.upon_receiving("A request to create a new user") + pact + .upon_receiving("A request to create a new user") .with_request("POST", "/users") .with_body(payload, content_type="application/json") .will_respond_with(201) @@ -150,7 +153,8 @@ async def test_delete_user(pact: Pact) -> None: correctly. """ ( - pact.upon_receiving("A user deletion request") + pact + .upon_receiving("A user deletion request") .given("the user exists", id=124, name="Bob") .with_request("DELETE", "/users/124") .will_respond_with(204) diff --git a/examples/http/requests_and_fastapi/test_consumer.py b/examples/http/requests_and_fastapi/test_consumer.py index dd0621843..6f69a8c64 100644 --- a/examples/http/requests_and_fastapi/test_consumer.py +++ b/examples/http/requests_and_fastapi/test_consumer.py @@ -69,7 +69,8 @@ def test_get_user(pact: Pact) -> None: "created_on": match.datetime(), } ( - pact.upon_receiving("A user request") + pact + .upon_receiving("A user request") .given("the user exists", id=123, name="Alice") .with_request("GET", "/users/123") .will_respond_with(200) @@ -94,7 +95,8 @@ def test_get_unknown_user(pact: Pact) -> None: """ response = {"detail": "User not found"} ( - pact.upon_receiving("A request for an unknown user") + pact + .upon_receiving("A request for an unknown user") .given("the user doesn't exist", id=123) .with_request("GET", "/users/123") .will_respond_with(404) @@ -127,7 +129,8 @@ def test_create_user(pact: Pact) -> None: } ( - pact.upon_receiving("A request to create a new user") + pact + .upon_receiving("A request to create a new user") .with_request("POST", "/users") .with_body(payload, content_type="application/json") .will_respond_with(201) @@ -149,7 +152,8 @@ def test_delete_user(pact: Pact) -> None: correctly. """ ( - pact.upon_receiving("A user deletion request") + pact + .upon_receiving("A user deletion request") .given("the user exists", id=124, name="Bob") .with_request("DELETE", "/users/124") .will_respond_with(204) diff --git a/examples/plugins/protobuf/test_consumer.py b/examples/plugins/protobuf/test_consumer.py index 29c1eabfc..2dbe4f84c 100644 --- a/examples/plugins/protobuf/test_consumer.py +++ b/examples/plugins/protobuf/test_consumer.py @@ -75,7 +75,8 @@ def test_get_person_by_id(pact: Pact) -> None: expected_protobuf_data = alice.SerializeToString() ( - pact.upon_receiving("a request to get person by ID") + pact + .upon_receiving("a request to get person by ID") .given("person with the given ID exists", user_id=1) .with_request("GET", "/person/1") .will_respond_with(200) @@ -118,7 +119,8 @@ def test_get_nonexistent_person(pact: Pact) -> None: code with an appropriate error message as a JSON response. """ ( - pact.upon_receiving("a request to get non-existent person") + pact + .upon_receiving("a request to get non-existent person") .given("person with the given ID does not exist", user_id=999) .with_request("GET", "/person/999") .will_respond_with(404) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 46293325a..ce8920454 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.9", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.10", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index b705c6018..68948a027 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.14.9", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.10", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 490722725..1457a63a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.14.9", + "ruff==0.14.10", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, diff --git a/src/pact/__init__.py b/src/pact/__init__.py index 7066585f2..fe11fd1b5 100644 --- a/src/pact/__init__.py +++ b/src/pact/__init__.py @@ -63,7 +63,8 @@ # Define expected interactions ( - pact.upon_receiving("get user") + pact + .upon_receiving("get user") .given("user exists") .with_request(method="GET", path="/users/123") .will_respond_with( diff --git a/src/pact/interaction/_base.py b/src/pact/interaction/_base.py index 4d478b9b4..825f7d485 100644 --- a/src/pact/interaction/_base.py +++ b/src/pact/interaction/_base.py @@ -153,7 +153,8 @@ def given( ```python ( - pact.upon_receiving("a request") + pact + .upon_receiving("a request") .given("a user exists", id=123, name="Alice") .given("a user exists", id=456, name="Bob") ) diff --git a/src/pact/interaction/_http_interaction.py b/src/pact/interaction/_http_interaction.py index 777abf9c3..b62273dce 100644 --- a/src/pact/interaction/_http_interaction.py +++ b/src/pact/interaction/_http_interaction.py @@ -39,7 +39,8 @@ class HttpInteraction(Interaction): ```python ( - pact.upon_receiving("a request") + pact + .upon_receiving("a request") .with_request("GET", "/") .with_header("X-Foo", "bar") .will_respond_with(200) @@ -49,7 +50,8 @@ class HttpInteraction(Interaction): ```python ( - pact.upon_receiving("a request") + pact + .upon_receiving("a request") .with_request("GET", "/") .will_respond_with(200) .with_header("X-Foo", "bar", part="Request") @@ -132,7 +134,8 @@ def with_header( ```python ( - pact.upon_receiving("a request") + pact + .upon_receiving("a request") .with_header("X-Foo", "bar") .with_header("X-Foo", "baz") ) @@ -320,7 +323,8 @@ def with_query_parameter( ```python ( - pact.upon_receiving("a request") + pact + .upon_receiving("a request") .with_query_parameter("name", "John") .with_query_parameter("name", "Mary") ) diff --git a/src/pact/pact.py b/src/pact/pact.py index b2af413aa..6d3adb95c 100644 --- a/src/pact/pact.py +++ b/src/pact/pact.py @@ -47,7 +47,8 @@ pact = Pact("consumer", "provider") ( - pact.upon_receiving("a basic request") + pact + .upon_receiving("a basic request") .given("user 123 exists") .with_request("GET", "/user/123") .will_respond_with(200) diff --git a/tests/compatibility_suite/test_v3_generators.py b/tests/compatibility_suite/test_v3_generators.py index d02aee036..7d4053c70 100644 --- a/tests/compatibility_suite/test_v3_generators.py +++ b/tests/compatibility_suite/test_v3_generators.py @@ -153,27 +153,38 @@ def the_request_is_prepared_for_use(pact: Pact) -> requests.Response: ################################################################################ GENERATOR_PATTERN: dict[str, Callable[[object], bool]] = { - "UUID": lambda v: isinstance(v, str) - and re.match(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", v) - is not None, + "UUID": lambda v: ( + isinstance(v, str) + and re.match( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", v + ) + is not None + ), "boolean": lambda v: isinstance(v, bool), - "date": lambda v: isinstance(v, str) - and re.match(r"^\d{4}-\d{2}-\d{2}$", v) is not None, - "time": lambda v: isinstance(v, str) - and re.match(r"^\d{2}:\d{2}:\d{2}(\.\d+)?$", v) is not None, - "date-time": lambda v: isinstance(v, str) - and re.match( - r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$", v - ) - is not None, - "decimal number": lambda v: isinstance(v, str) - and re.match(r"^-?\d+\.\d+$", v) is not None, - "hexadecimal number": lambda v: isinstance(v, str) - and re.match(r"^[0-9a-fA-F]+$", v) is not None, + "date": lambda v: ( + isinstance(v, str) and re.match(r"^\d{4}-\d{2}-\d{2}$", v) is not None + ), + "time": lambda v: ( + isinstance(v, str) and re.match(r"^\d{2}:\d{2}:\d{2}(\.\d+)?$", v) is not None + ), + "date-time": lambda v: ( + isinstance(v, str) + and re.match( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$", v + ) + is not None + ), + "decimal number": lambda v: ( + isinstance(v, str) and re.match(r"^-?\d+\.\d+$", v) is not None + ), + "hexadecimal number": lambda v: ( + isinstance(v, str) and re.match(r"^[0-9a-fA-F]+$", v) is not None + ), "integer": lambda v: isinstance(v, int) or (isinstance(v, str) and v.isdigit()), "random string": lambda v: isinstance(v, str) and len(v) > 0, - "string from the regex": lambda v: isinstance(v, str) - and re.match(r"^\d{1,8}$", v) is not None, + "string from the regex": lambda v: ( + isinstance(v, str) and re.match(r"^\d{1,8}$", v) is not None + ), } diff --git a/tests/compatibility_suite/test_v4_generators.py b/tests/compatibility_suite/test_v4_generators.py index 760098646..d0dcaf2cd 100644 --- a/tests/compatibility_suite/test_v4_generators.py +++ b/tests/compatibility_suite/test_v4_generators.py @@ -53,7 +53,8 @@ def test_provider_state_generator( ) -> None: """Test the provider state generator.""" ( - pact.upon_receiving("a generators request") + pact + .upon_receiving("a generators request") .given("a provider state exists", {"id": 1000}) .with_request("POST", "/") .with_body({"one": "a", "two": "b"}) @@ -245,19 +246,31 @@ def the_request_is_prepared_with_context( GENERATOR_PATTERN: dict[str, Callable[[object], bool]] = { - "upper-case-hyphenated UUID": lambda v: isinstance(v, str) - and re.match(r"^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", v) - is not None, - "lower-case-hyphenated UUID": lambda v: isinstance(v, str) - and re.match(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", v) - is not None, - "simple UUID": lambda v: isinstance(v, str) - and re.match(r"^[0-9a-fA-F]{32}$", v) is not None, - "URN UUID": lambda v: isinstance(v, str) - and re.match( - r"^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", v - ) - is not None, + "upper-case-hyphenated UUID": lambda v: ( + isinstance(v, str) + and re.match( + r"^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", v + ) + is not None + ), + "lower-case-hyphenated UUID": lambda v: ( + isinstance(v, str) + and re.match( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", v + ) + is not None + ), + "simple UUID": lambda v: ( + isinstance(v, str) and re.match(r"^[0-9a-fA-F]{32}$", v) is not None + ), + "URN UUID": lambda v: ( + isinstance(v, str) + and re.match( + r"^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + v, + ) + is not None + ), } diff --git a/tests/interaction/test_http_interaction.py b/tests/interaction/test_http_interaction.py index 695ab835a..6a29a67cc 100644 --- a/tests/interaction/test_http_interaction.py +++ b/tests/interaction/test_http_interaction.py @@ -60,7 +60,8 @@ def test_repr(pact: Pact) -> None: @pytest.mark.asyncio async def test_basic_request_method(pact: Pact, method: str) -> None: ( - pact.upon_receiving(f"a basic {method} request") + pact + .upon_receiving(f"a basic {method} request") .with_request(method, "/") .will_respond_with(200) ) @@ -82,7 +83,8 @@ async def test_basic_request_method(pact: Pact, method: str) -> None: @pytest.mark.asyncio async def test_basic_response_status(pact: Pact, status: int) -> None: ( - pact.upon_receiving(f"a basic request producing status {status}") + pact + .upon_receiving(f"a basic request producing status {status}") .with_request("GET", "/") .will_respond_with(status) ) @@ -106,7 +108,8 @@ async def test_with_header_request( headers: list[tuple[str, str]], ) -> None: ( - pact.upon_receiving("a basic request with a header") + pact + .upon_receiving("a basic request with a header") .with_request("GET", "/") .with_headers(headers) .will_respond_with(200) @@ -131,7 +134,8 @@ async def test_with_header_response( headers: list[tuple[str, str]], ) -> None: ( - pact.upon_receiving("a basic request with a header") + pact + .upon_receiving("a basic request with a header") .with_request("GET", "/") .will_respond_with(200) .with_headers(headers) @@ -148,7 +152,8 @@ async def test_with_header_response( @pytest.mark.asyncio async def test_with_header_dict(pact: Pact) -> None: ( - pact.upon_receiving("a basic request with a headers from a dict") + pact + .upon_receiving("a basic request with a headers from a dict") .with_request("GET", "/") .with_headers({"X-Test": "true", "X-Foo": "bar"}) .will_respond_with(200) @@ -176,7 +181,8 @@ async def test_set_header_request( headers: list[tuple[str, str]], ) -> None: ( - pact.upon_receiving("a basic request with a header") + pact + .upon_receiving("a basic request with a header") .with_request("GET", "/") .set_headers(headers) .will_respond_with(200) @@ -193,7 +199,8 @@ async def test_set_header_request_repeat( ) -> None: headers = [("X-Test", "1"), ("X-Test", "2")] ( - pact.upon_receiving("a basic request with a header") + pact + .upon_receiving("a basic request with a header") .with_request("GET", "/") # As set_headers makes no additional processing, the last header will be # the one that is used. @@ -228,7 +235,8 @@ async def test_set_header_response( headers: list[tuple[str, str]], ) -> None: ( - pact.upon_receiving("a basic request with a header") + pact + .upon_receiving("a basic request with a header") .with_request("GET", "/") .will_respond_with(200) .set_headers(headers) @@ -248,7 +256,8 @@ async def test_set_header_response_repeat( ) -> None: headers = [("X-Test", "1"), ("X-Test", "2")] ( - pact.upon_receiving("a basic request with a header") + pact + .upon_receiving("a basic request with a header") .with_request("GET", "/") .will_respond_with(200) # As set_headers makes no additional processing, the last header will be @@ -267,7 +276,8 @@ async def test_set_header_response_repeat( @pytest.mark.asyncio async def test_set_header_dict(pact: Pact) -> None: ( - pact.upon_receiving("a basic request with a headers from a dict") + pact + .upon_receiving("a basic request with a headers from a dict") .with_request("GET", "/") .set_headers({"X-Test": "true", "X-Foo": "bar"}) .will_respond_with(200) @@ -296,7 +306,8 @@ async def test_with_query_parameter_request( query: list[tuple[str, str]], ) -> None: ( - pact.upon_receiving("a basic request with a query parameter") + pact + .upon_receiving("a basic request with a query parameter") .with_request("GET", "/") .with_query_parameters(query) .will_respond_with(200) @@ -313,7 +324,8 @@ async def test_with_query_parameter_with_matcher( pact: Pact, ) -> None: ( - pact.upon_receiving("a basic request with a query parameter") + pact + .upon_receiving("a basic request with a query parameter") .with_request("GET", "/") .with_query_parameter("test", match.string("true")) .will_respond_with(200) @@ -328,7 +340,8 @@ async def test_with_query_parameter_with_matcher( @pytest.mark.asyncio async def test_with_query_parameter_dict(pact: Pact) -> None: ( - pact.upon_receiving("a basic request with a query parameter from a dict") + pact + .upon_receiving("a basic request with a query parameter from a dict") .with_request("GET", "/") .with_query_parameters({"test": "true", "foo": "bar"}) .will_respond_with(200) @@ -343,7 +356,8 @@ async def test_with_query_parameter_dict(pact: Pact) -> None: @pytest.mark.asyncio async def test_with_query_parameter_tuple_list(pact: Pact) -> None: ( - pact.upon_receiving("a basic request with a query parameter from a dict") + pact + .upon_receiving("a basic request with a query parameter from a dict") .with_request("GET", "/") .with_query_parameters([("test", "true"), ("foo", "bar")]) .will_respond_with(200) @@ -362,7 +376,8 @@ async def test_with_query_parameter_tuple_list(pact: Pact) -> None: @pytest.mark.asyncio async def test_with_body_request(pact: Pact, method: str) -> None: ( - pact.upon_receiving(f"a basic {method} request with a body") + pact + .upon_receiving(f"a basic {method} request with a body") .with_request(method, "/") .with_body(json.dumps({"test": True}), "application/json") .will_respond_with(200) @@ -384,7 +399,8 @@ async def test_with_body_request(pact: Pact, method: str) -> None: @pytest.mark.asyncio async def test_with_body_response(pact: Pact, method: str) -> None: ( - pact.upon_receiving( + pact + .upon_receiving( f"a basic {method} request expecting a response with a body", ) .with_request(method, "/") @@ -405,7 +421,8 @@ async def test_with_body_response(pact: Pact, method: str) -> None: @pytest.mark.asyncio async def test_with_body_explicit(pact: Pact) -> None: ( - pact.upon_receiving("") + pact + .upon_receiving("") .with_request("GET", "/") .will_respond_with(200) .with_body(json.dumps({"request": True}), "application/json", "Request") @@ -425,7 +442,8 @@ async def test_with_body_explicit(pact: Pact) -> None: def test_with_body_invalid(pact: Pact) -> None: with pytest.raises(ValueError, match="Invalid part: Invalid"): ( - pact.upon_receiving("") + pact + .upon_receiving("") .with_request("GET", "/") .will_respond_with(200) .with_body( @@ -439,20 +457,23 @@ def test_with_body_invalid(pact: Pact) -> None: @pytest.mark.asyncio async def test_given(pact: Pact) -> None: ( - pact.upon_receiving("a basic request given state 1") + pact + .upon_receiving("a basic request given state 1") .given("state 1") .with_request("GET", "/state") .will_respond_with(200) ) ( - pact.upon_receiving("a basic request given a user exists (1)") + pact + .upon_receiving("a basic request given a user exists (1)") .given("a user exists", id=123) .given("a user exists", name="John") .with_request("GET", "/user1") .will_respond_with(201) ) ( - pact.upon_receiving("a basic request given a user exists (2)") + pact + .upon_receiving("a basic request given a user exists (2)") .given("a user exists", {"id": "123", "name": "John"}) .with_request("GET", "/user2") .will_respond_with(202) @@ -472,7 +493,8 @@ async def test_given(pact: Pact) -> None: async def test_binary_file_request(pact: Pact) -> None: payload = bytes(range(8)) ( - pact.upon_receiving("a basic request with a binary file") + pact + .upon_receiving("a basic request with a binary file") .with_request("POST", "/") .with_binary_body(payload, "application/octet-stream") .will_respond_with(200) @@ -492,7 +514,8 @@ async def test_binary_file_request(pact: Pact) -> None: async def test_binary_file_response(pact: Pact) -> None: payload = bytes(range(5)) ( - pact.upon_receiving("a basic request with a binary file response") + pact + .upon_receiving("a basic request with a binary file response") .with_request("GET", "/") .will_respond_with(200) .with_binary_body(payload, "application/bytes") @@ -512,7 +535,8 @@ async def test_multipart_file_request(pact: Pact, temp_assets: Path) -> None: fpy = temp_assets / "test.py" fpng = temp_assets / "test.png" ( - pact.upon_receiving("a basic request with a multipart file") + pact + .upon_receiving("a basic request with a multipart file") .with_request("POST", "/") .with_multipart_file( fpy.name, @@ -554,7 +578,8 @@ async def test_multipart_file_request(pact: Pact, temp_assets: Path) -> None: @pytest.mark.asyncio async def test_name(pact: Pact) -> None: ( - pact.upon_receiving("a basic request with a test name") + pact + .upon_receiving("a basic request with a test name") .test_name("a test name") .will_respond_with(200) ) @@ -568,7 +593,8 @@ async def test_name(pact: Pact) -> None: @pytest.mark.asyncio async def test_with_plugin(pact: Pact) -> None: ( - pact.upon_receiving("a basic request with a plugin") + pact + .upon_receiving("a basic request with a plugin") .with_plugin_contents("{}", "application/json") .will_respond_with(200) ) @@ -585,7 +611,8 @@ async def test_pact_server_verbose( caplog: pytest.LogCaptureFixture, ) -> None: ( - pact.upon_receiving("a basic request with a plugin") + pact + .upon_receiving("a basic request with a plugin") .with_request("GET", "/foo") .will_respond_with(200) ) diff --git a/tests/interaction/test_sync_message_interaction.py b/tests/interaction/test_sync_message_interaction.py index 8cf296e2e..98164de66 100644 --- a/tests/interaction/test_sync_message_interaction.py +++ b/tests/interaction/test_sync_message_interaction.py @@ -38,7 +38,8 @@ def test_repr(pact: Pact) -> None: def test_with_metadata_with_positional_dict(pact: Pact) -> None: ( - pact.upon_receiving("with_metadatadict", "Sync") + pact + .upon_receiving("with_metadatadict", "Sync") .with_body("request", content_type="text/plain") .with_metadata({"foo": "bar"}) .will_respond_with() @@ -54,7 +55,8 @@ def test_with_metadata_with_positional_dict(pact: Pact) -> None: def test_with_metadata_with_keyword_args(pact: Pact) -> None: ( - pact.upon_receiving("with_metadata_kwargs", "Sync") + pact + .upon_receiving("with_metadata_kwargs", "Sync") .with_body("request", content_type="text/plain") .with_metadata(foo="bar") .will_respond_with() @@ -70,7 +72,8 @@ def test_with_metadata_with_keyword_args(pact: Pact) -> None: def test_with_metadata_with_mixed_args(pact: Pact) -> None: ( - pact.upon_receiving("with_metadata_mixed", "Sync") + pact + .upon_receiving("with_metadata_mixed", "Sync") .with_body("request", content_type="text/plain") .with_metadata({"foo": {"bar": 1.23}}, metadata=123) .will_respond_with() @@ -88,7 +91,8 @@ def test_with_metadata_with_mixed_args(pact: Pact) -> None: def test_with_metadata_with_part(pact: Pact) -> None: ( - pact.upon_receiving("with_metadata_part", "Sync") + pact + .upon_receiving("with_metadata_part", "Sync") .with_body("request", content_type="text/plain") .will_respond_with() .with_body("response", content_type="text/plain") diff --git a/tests/test_error.py b/tests/test_error.py index b945a7b26..7b57f3b7d 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -33,7 +33,8 @@ def pact() -> Pact: @pytest.mark.asyncio async def test_missing_request(pact: Pact) -> None: ( - pact.upon_receiving("a missing request") + pact + .upon_receiving("a missing request") .with_request("GET", "/") .will_respond_with(200) ) @@ -67,7 +68,8 @@ async def test_missing_request(pact: Pact) -> None: @pytest.mark.asyncio async def test_query_mismatch_value(pact: Pact) -> None: ( - pact.upon_receiving("a query mismatch") + pact + .upon_receiving("a query mismatch") .with_request("GET", "/resource") .with_query_parameter("param", "expected") .will_respond_with(200) @@ -107,7 +109,8 @@ async def test_query_mismatch_value(pact: Pact) -> None: @pytest.mark.asyncio async def test_query_mismatch_different_keys(pact: Pact) -> None: ( - pact.upon_receiving("a query mismatch with different keys") + pact + .upon_receiving("a query mismatch with different keys") .with_request("GET", "/resource") .with_query_parameter("key", "value") .will_respond_with(200) @@ -148,7 +151,8 @@ async def test_query_mismatch_different_keys(pact: Pact) -> None: @pytest.mark.asyncio async def test_header_mismatch(pact: Pact) -> None: ( - pact.upon_receiving("a header mismatch") + pact + .upon_receiving("a header mismatch") .with_request("GET", "/") .with_header("X-Foo", "expected") .will_respond_with(200) @@ -183,7 +187,8 @@ async def test_header_mismatch(pact: Pact) -> None: @pytest.mark.asyncio async def test_body_type_mismatch(pact: Pact) -> None: ( - pact.upon_receiving("a body type mismatch") + pact + .upon_receiving("a body type mismatch") .with_request("POST", "/") .with_body("{}", "application/json") .will_respond_with(200) @@ -228,7 +233,8 @@ async def test_body_type_mismatch(pact: Pact) -> None: @pytest.mark.asyncio async def test_body_mismatch(pact: Pact) -> None: ( - pact.upon_receiving("a body mismatch") + pact + .upon_receiving("a body mismatch") .with_request("POST", "/") .with_body("expected") .will_respond_with(200) diff --git a/tests/test_match.py b/tests/test_match.py index e078cc42b..0ed9de7a9 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -123,7 +123,8 @@ def test_matchers() -> None: pact_dir = Path(Path(__file__).parent.parent.parent / "pacts") pact = Pact("consumer", "provider").with_specification("V4") ( - pact.upon_receiving("a request") + pact + .upon_receiving("a request") .given("a state", {"providerStateArgument": "providerStateValue"}) .with_request("GET", match.regex("/path/to/100", regex=r"/path/to/\d{1,4}")) .with_query_parameter( diff --git a/tests/test_pact.py b/tests/test_pact.py index 59c208382..ab8c2ec13 100644 --- a/tests/test_pact.py +++ b/tests/test_pact.py @@ -165,7 +165,8 @@ def test_empty( @classmethod def _add_http_interaction(cls, pact: Pact, id_: int) -> None: ( - pact.upon_receiving(f"HTTP request {id_}", "HTTP") + pact + .upon_receiving(f"HTTP request {id_}", "HTTP") .with_request("GET", f"/{id_}") .will_respond_with(200) ) @@ -177,7 +178,8 @@ def _add_async_interaction(cls, pact: Pact, id_: int) -> None: @classmethod def _add_sync_interaction(cls, pact: Pact, id_: int) -> None: ( - pact.upon_receiving(f"Sync message {id_}", "Sync") + pact + .upon_receiving(f"Sync message {id_}", "Sync") .with_body(f"request {id_}") .will_respond_with() .with_body(f"response {id_}") diff --git a/tests/test_verifier.py b/tests/test_verifier.py index b9aa4fc3b..895b8b2e5 100644 --- a/tests/test_verifier.py +++ b/tests/test_verifier.py @@ -148,7 +148,8 @@ def test_broker_source(verifier: Verifier) -> None: def test_broker_source_selector(verifier: Verifier) -> None: ( - verifier.broker_source("http://localhost:8080", selector=True) + verifier + .broker_source("http://localhost:8080", selector=True) .consumer_tags("main", "test") .provider_tags("main", "test") .consumer_versions('{"latest": true}') From b4a98675be211fcb4585f2d3483afca1a23a5e34 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 22 Dec 2025 13:16:00 +1100 Subject: [PATCH 1185/1376] feat: implement the Pact class This class is analogous to the `PactHandle` class, but is instead based on a pointer to the struct. Signed-off-by: JP-Ellis --- pact-python-ffi/src/pact_ffi/__init__.py | 45 ++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index b0e36eb95..60971e95a 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -814,7 +814,48 @@ class Mismatches: ... class MismatchesIterator: ... -class Pact: ... +class Pact: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = True) -> None: + """ + Wrapper for a Pact model pointer. + + Args: + ptr: + CFFI pointer to `struct Pact *`. + + owned: + Whether the pact is owned by something else or not. This + determines whether the pact should be freed when the Python + object is destroyed. + + Raises: + TypeError: + If `ptr` is not a `struct Pact *`. + """ + if ffi.typeof(ptr).cname != "struct Pact *": + msg = f"ptr must be a struct Pact, got {ffi.typeof(ptr).cname}" + raise TypeError(msg) + self._ptr = ptr + self._owned = owned + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "Pact" + + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"Pact({self._ptr!r})" + + def __del__(self) -> None: + """ + Destructor for the Pact. + """ + if not self._owned: + pact_model_delete(self) class PactAsyncMessageIterator: @@ -2446,7 +2487,7 @@ def pact_model_delete(pact: Pact) -> None: [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_model_delete) """ - raise NotImplementedError + lib.pactffi_pact_model_delete(pact._ptr) def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: From c2b6b538d6f4f43fe949eb77c4a23d9e8c88714a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 22 Dec 2025 13:17:17 +1100 Subject: [PATCH 1186/1376] feat: add handle to pointer conversion The Pact behind the PactHandle can be copied to a struct, whose pointer can subsequently be used. Signed-off-by: JP-Ellis --- pact-python-ffi/src/pact_ffi/__init__.py | 37 ++++++++++++++++++++---- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index 60971e95a..067edff75 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -951,6 +951,19 @@ def __repr__(self) -> str: """ return f"PactHandle({self._ref!r})" + def pointer(self) -> Pact: + """ + Unwrap the handle to access the underlying Pact model. + + This function clones the underlying structure, therefore any + modification to the original handle will not be reflected in the + returned Pact model, and vice versa. + + Returns: + The underlying Pact model. + """ + return pact_handle_to_pointer(self) + class PactServerHandle: """ @@ -5454,15 +5467,27 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle: def pact_handle_to_pointer(pact: PactHandle) -> Pact: """ - Unwraps a Pact handle to the underlying Pact. + Copy a Pact handle to a raw Pact. - The Pact model which has been cloned from the Pact handle's inner Pact - model. + The underlying data is cloned, therefore, any changes made to the original + Pact handle will not be reflected in the Pact model, and vice versa. - The returned Pact model must be freed with the `pactffi_pact_model_delete` - function when no longer needed. + Args: + pact: + The Pact handle to unwrap. + + Returns: + The underlying Pact model pointer. + + Raises: + RuntimeError: + If the unwrap operation fails. """ - raise NotImplementedError + ptr = lib.pactffi_pact_handle_to_pointer(pact._ref) + if ptr == ffi.NULL: + msg = f"Failed to unwrap pact handle: {pact}" + raise RuntimeError(msg) + return Pact(ptr, owned=False) def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: From 0cbe00c74b2667a589881259c9a327961f0a2e1e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 22 Dec 2025 13:19:14 +1100 Subject: [PATCH 1187/1376] feat: add casting interaction to subtypes Signed-off-by: JP-Ellis --- pact-python-ffi/src/pact_ffi/__init__.py | 117 ++++++++++++++++------- 1 file changed, 81 insertions(+), 36 deletions(-) diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index 067edff75..2b524c0fc 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -1057,6 +1057,36 @@ def __del__(self) -> None: if self._owned: pass # pact_interaction_delete not implemented yet + def as_synchronous_http(self) -> SynchronousHttp: + """ + Cast this interaction as a synchronous HTTP interaction. + + Raises: + TypeError: + If the interaction is not a synchronous HTTP interaction. + """ + return pact_interaction_as_synchronous_http(self) + + def as_asynchronous_message(self) -> AsynchronousMessage: + """ + Cast this interaction as an asynchronous message interaction. + + Raises: + TypeError: + If the interaction is not an asynchronous message interaction. + """ + return pact_interaction_as_asynchronous_message(self) + + def as_synchronous_message(self) -> SynchronousMessage: + """ + Cast this interaction as a synchronous message interaction. + + Raises: + TypeError: + If the interaction is not a synchronous message interaction. + """ + return pact_interaction_as_synchronous_message(self) + class PactInteractionIterator: """ @@ -4034,70 +4064,85 @@ def sync_http_get_provider_state_iter( def pact_interaction_as_synchronous_http( interaction: PactInteraction, ) -> SynchronousHttp: - r""" - Casts this interaction to a `SynchronousHttp` interaction. + """ + Cast this interaction to a `SynchronousHttp` interaction. - Returns a NULL pointer if the interaction can not be casted to a - `SynchronousHttp` interaction (for instance, it is a message interaction). - The returned pointer must be freed with `pactffi_sync_http_delete` when no - longer required. + [Rust `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) - [Rust - `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) + Args: + interaction: + The interaction to cast. - # Safety This function is safe as long as the interaction pointer is a valid - pointer. + Returns: + The interaction cast as a `SynchronousHttp`. - # Errors On any error, this function will return a NULL pointer. + Raises: + TypeError: + If the interaction cannot be cast to a `SynchronousHttp` + interaction. """ - raise NotImplementedError + ptr = lib.pactffi_pact_interaction_as_synchronous_http(interaction._ptr) + if ptr == ffi.NULL: + msg = "Interaction is not a SynchronousHttp interaction" + raise TypeError(msg) + return SynchronousHttp(ptr, owned=False) def pact_interaction_as_asynchronous_message( interaction: PactInteraction, ) -> AsynchronousMessage: """ - Casts this interaction to a `AsynchronousMessage` interaction. - - Returns a NULL pointer if the interaction can not be casted to a - `AsynchronousMessage` interaction (for instance, it is a http interaction). - The returned pointer must be freed with `pactffi_async_message_delete` when - no longer required. - - [Rust - `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) + Cast this interaction to an `AsynchronousMessage` interaction. Note that if the interaction is a V3 `Message`, it will be converted to a V4 `AsynchronousMessage` before being returned. - # Safety This function is safe as long as the interaction pointer is a valid - pointer. + [Rust `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) - # Errors On any error, this function will return a NULL pointer. + Args: + interaction: + The interaction to cast. + + Returns: + The interaction cast as an `AsynchronousMessage`. + + Raises: + TypeError: + If the interaction cannot be cast to an `AsynchronousMessage` + interaction. """ - raise NotImplementedError + ptr = lib.pactffi_pact_interaction_as_asynchronous_message(interaction._ptr) + if ptr == ffi.NULL: + msg = "Interaction is not an AsynchronousMessage interaction" + raise TypeError(msg) + return AsynchronousMessage(ptr, owned=False) def pact_interaction_as_synchronous_message( interaction: PactInteraction, ) -> SynchronousMessage: """ - Casts this interaction to a `SynchronousMessage` interaction. + Cast this interaction to a `SynchronousMessage` interaction. - Returns a NULL pointer if the interaction can not be casted to a - `SynchronousMessage` interaction (for instance, it is a http interaction). - The returned pointer must be freed with `pactffi_sync_message_delete` when - no longer required. + [Rust `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) - [Rust - `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) + Args: + interaction: + The interaction to cast. - # Safety This function is safe as long as the interaction pointer is a valid - pointer. + Returns: + The interaction cast as a `SynchronousMessage`. - # Errors On any error, this function will return a NULL pointer. + Raises: + TypeError: + If the interaction cannot be cast to a `SynchronousMessage` + interaction. """ - raise NotImplementedError + ptr = lib.pactffi_pact_interaction_as_synchronous_message(interaction._ptr) + if ptr == ffi.NULL: + msg = "Interaction is not a SynchronousMessage interaction" + raise TypeError(msg) + return SynchronousMessage(ptr, owned=False) def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> AsynchronousMessage: From 8e8442d898569f60c722365446dfa16594c6bb0f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 22 Dec 2025 13:21:44 +1100 Subject: [PATCH 1188/1376] feat: add iterator over all interactions Signed-off-by: JP-Ellis --- pact-python-ffi/src/pact_ffi/__init__.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index 2b524c0fc..387c2f57f 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -2540,17 +2540,20 @@ def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: [Rust `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_model_interaction_iterator) - The iterator will have to be deleted using the - `pactffi_pact_interaction_iter_delete` function. The iterator will contain a - copy of the interactions, so it will not be affected but mutations to the - Pact model and will still function if the Pact model is deleted. + The iterator will contain a copy of the interactions, so it will not be + affected but mutations to the Pact model and will still function if the Pact + model is deleted. - # Safety This function is safe as long as the Pact pointer is a valid - pointer. + Args: + pact: + The Pact model. - # Errors On any error, this function will return a NULL pointer. + Returns: + An iterator over all interactions in the Pact. """ - raise NotImplementedError + return PactInteractionIterator( + lib.pactffi_pact_model_interaction_iterator(pact._ptr) + ) def pact_spec_version(pact: Pact) -> PactSpecification: From b43efb8e51de3cd9ac419455006b223591d23afe Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 22 Dec 2025 13:25:11 +1100 Subject: [PATCH 1189/1376] fix: incorrect sync http deletion Signed-off-by: JP-Ellis --- pact-python-ffi/src/pact_ffi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index 387c2f57f..a80293906 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -3708,7 +3708,7 @@ def sync_http_delete(interaction: SynchronousHttp) -> None: [Rust `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_delete) """ - lib.pactffi_sync_http_delete(interaction) + lib.pactffi_sync_http_delete(interaction._ptr) def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: From 7733985268fd210260d21a1703eacc085852acd8 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 22 Dec 2025 13:25:54 +1100 Subject: [PATCH 1190/1376] chore: ensure pact interactions get deleted Signed-off-by: JP-Ellis --- pact-python-ffi/src/pact_ffi/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index a80293906..ca0141605 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -1054,8 +1054,8 @@ def __del__(self) -> None: """ Destructor for the Pact Interaction. """ - if self._owned: - pass # pact_interaction_delete not implemented yet + if not self._owned: + pact_interaction_delete(self) def as_synchronous_http(self) -> SynchronousHttp: """ @@ -2571,7 +2571,7 @@ def pact_interaction_delete(interaction: PactInteraction) -> None: [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_delete) """ - raise NotImplementedError + lib.pactffi_pact_interaction_delete(interaction._ptr) def async_message_new() -> AsynchronousMessage: From 2c0e61f50bd14885ee7fc58576a26679bea121f2 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 22 Dec 2025 13:26:26 +1100 Subject: [PATCH 1191/1376] feat: use common PactInteraction type The PactInteraction is generic and can be cast down to the relevant subtype as need be. Signed-off-by: JP-Ellis --- src/pact/pact.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/pact/pact.py b/src/pact/pact.py index 6d3adb95c..7e5afcdd6 100644 --- a/src/pact/pact.py +++ b/src/pact/pact.py @@ -395,9 +395,7 @@ def interactions( self, kind: Literal["All"], ) -> Generator[ - pact_ffi.SynchronousHttp - | pact_ffi.SynchronousMessage - | pact_ffi.AsynchronousMessage, + pact_ffi.PactInteraction, None, None, ]: ... @@ -410,9 +408,7 @@ def interactions( | Generator[pact_ffi.SynchronousMessage, None, None] | Generator[pact_ffi.AsynchronousMessage, None, None] | Generator[ - pact_ffi.SynchronousHttp - | pact_ffi.SynchronousMessage - | pact_ffi.AsynchronousMessage, + pact_ffi.PactInteraction, None, None, ] @@ -427,20 +423,21 @@ def interactions( ValueError: If the kind is unknown. """ + if kind == "All": + yield from pact_ffi.pact_model_interaction_iterator(self._handle.pointer()) + return if kind == "HTTP": yield from pact_ffi.pact_handle_get_sync_http_iter(self._handle) - elif kind == "Sync": + return + if kind == "Sync": yield from pact_ffi.pact_handle_get_sync_message_iter(self._handle) - elif kind == "Async": + return + if kind == "Async": yield from pact_ffi.pact_handle_get_async_message_iter(self._handle) - elif kind == "All": - yield from pact_ffi.pact_handle_get_sync_http_iter(self._handle) - yield from pact_ffi.pact_handle_get_sync_message_iter(self._handle) - yield from pact_ffi.pact_handle_get_async_message_iter(self._handle) - else: - msg = f"Unknown interaction type: {kind}" - raise ValueError(msg) - return # Ensures that the parent object outlives the generator + return + + msg = f"Unknown interaction kind: {kind}" + raise ValueError(msg) @overload def verify( From 4e5aae871c723afb85734d48b1c4fb0273766d7f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 22 Dec 2025 14:27:28 +1100 Subject: [PATCH 1192/1376] chore: add ruff ignores for tests Signed-off-by: JP-Ellis --- pact-python-ffi/tests/.ruff.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pact-python-ffi/tests/.ruff.toml diff --git a/pact-python-ffi/tests/.ruff.toml b/pact-python-ffi/tests/.ruff.toml new file mode 100644 index 000000000..3558b7bba --- /dev/null +++ b/pact-python-ffi/tests/.ruff.toml @@ -0,0 +1,16 @@ +#:schema https://www.schemastore.org/ruff.json +extend = "../pyproject.toml" + +# We have a number of helper files which contain assertions/magic values, etc. + +[lint] +ignore = [ + "D102", # Require docstring in public methods + "D103", # Require docstring in public function + "D104", # Require docstring in public package + "INP001", # Forbid implicit namespaces + "PLR2004", # Forbid magic values + "RUF018", # Forbid assignment in assertions + "S101", # Forbid assert statements + "TID252", # Require absolute imports +] From e28be216b1ea207b4a53c4aac4d8e8ce2dc9f197 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 22 Dec 2025 15:13:42 +1100 Subject: [PATCH 1193/1376] chore: refactor ffi tests This simplifies/reorganises tests to make them more logically organised, and more intentful (even if not as exhaustive). Signed-off-by: JP-Ellis --- pact-python-ffi/tests/test_init.py | 224 +++++++++++++++++------------ 1 file changed, 133 insertions(+), 91 deletions(-) diff --git a/pact-python-ffi/tests/test_init.py b/pact-python-ffi/tests/test_init.py index 13e6ea5b1..efd6863fb 100644 --- a/pact-python-ffi/tests/test_init.py +++ b/pact-python-ffi/tests/test_init.py @@ -84,136 +84,178 @@ class TestInteractionIteration: Test interaction iteration functionality. """ - def test_pact_interaction(self) -> None: - """Test PactInteraction class.""" + @pytest.fixture + def pact(self) -> pact_ffi.PactHandle: + """Create a V4 pact for testing.""" pact = pact_ffi.new_pact("consumer", "provider") pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + return pact - # Create HTTP interaction - pact_ffi.new_sync_message_interaction(pact, "test") + def test_interaction_iterator_repr(self, pact: pact_ffi.PactHandle) -> None: + iterator = pact_ffi.pact_model_interaction_iterator(pact.pointer()) + assert str(iterator) == "PactInteractionIterator" + assert repr(iterator).startswith("PactInteractionIterator(") - # Get interactions via iterator - sync_http_iter = pact_ffi.pact_handle_get_sync_http_iter(pact) - list(sync_http_iter) - # Test string representation works on iterator - assert "PactSyncHttpIterator" in str(sync_http_iter) or str(sync_http_iter) + def test_http_iterator_repr(self, pact: pact_ffi.PactHandle) -> None: + iterator = pact_ffi.pact_handle_get_sync_http_iter(pact) + assert str(iterator) == "PactSyncHttpIterator" + assert repr(iterator).startswith("PactSyncHttpIterator(") - def test_pact_message_iterator(self) -> None: - """Test PactMessageIterator class.""" - pact = pact_ffi.new_pact("consumer", "provider") - pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + def test_async_message_iterator_repr(self, pact: pact_ffi.PactHandle) -> None: + iterator = pact_ffi.pact_handle_get_async_message_iter(pact) + assert str(iterator) == "PactAsyncMessageIterator" + assert repr(iterator).startswith("PactAsyncMessageIterator(") - # Create message interaction - pact_ffi.new_message_interaction(pact, "test message") + def test_sync_message_iterator_repr(self, pact: pact_ffi.PactHandle) -> None: + iterator = pact_ffi.pact_handle_get_sync_message_iter(pact) + assert str(iterator) == "PactSyncMessageIterator" + assert repr(iterator).startswith("PactSyncMessageIterator(") - # Get message iterator - iterator = pact_ffi.pact_handle_get_message_iter(pact) + def test_empty_iterators(self, pact: pact_ffi.PactHandle) -> None: + inter_iter = pact_ffi.pact_model_interaction_iterator(pact.pointer()) + assert sum(1 for _ in inter_iter) == 0 - # Test string representation - assert "PactMessageIterator" in str(iterator) - assert "PactMessageIterator" in repr(iterator) + http_iterator = pact_ffi.pact_handle_get_sync_http_iter(pact) + assert sum(1 for _ in http_iterator) == 0 - # Iterate and count messages - message_count = sum(1 for _ in iterator) + async_iterator = pact_ffi.pact_handle_get_async_message_iter(pact) + assert sum(1 for _ in async_iterator) == 0 - # Should have the message - assert message_count >= 1 + sync_iterator = pact_ffi.pact_handle_get_sync_message_iter(pact) + assert sum(1 for _ in sync_iterator) == 0 - def test_pact_interaction_owned(self) -> None: - """Test PactInteraction with owned parameter.""" - pact = pact_ffi.new_pact("consumer", "provider") - pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) - pact_ffi.new_sync_message_interaction(pact, "test") + def test_iterators(self, pact: pact_ffi.PactHandle) -> None: + pact_ffi.new_interaction(pact, "http") + pact_ffi.new_message_interaction(pact, "async") + pact_ffi.new_sync_message_interaction(pact, "sync") - # Get an interaction through the iterator - sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact) - for interaction in sync_iter: - # Interaction should be owned by the iterator - # Test destructor doesn't crash - del interaction - break - - def test_pact_message_iterator_empty(self) -> None: - """Test PactMessageIterator with no messages.""" - pact = pact_ffi.new_pact("consumer", "provider") - pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + # Test each iterator type + interaction_iter = pact_ffi.pact_model_interaction_iterator(pact.pointer()) + assert sum(1 for _ in interaction_iter) == 3 + assert sum(1 for _ in interaction_iter) == 0 # exhausted - iterator = pact_ffi.pact_handle_get_message_iter(pact) + http_iter = pact_ffi.pact_handle_get_sync_http_iter(pact) + assert sum(1 for _ in http_iter) == 1 + assert sum(1 for _ in http_iter) == 0 # exhausted - # Should iterate zero times - message_count = sum(1 for _ in iterator) - assert message_count == 0 + async_iter = pact_ffi.pact_handle_get_async_message_iter(pact) + assert sum(1 for _ in async_iter) == 1 + assert sum(1 for _ in async_iter) == 0 # exhausted - def test_pact_interaction_iterator_next(self) -> None: - """Test iterator next functions.""" - pact = pact_ffi.new_pact("consumer", "provider") - pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact) + assert sum(1 for _ in sync_iter) == 1 + assert sum(1 for _ in sync_iter) == 0 # exhausted - # Create multiple interactions + def test_iterator_types(self, pact: pact_ffi.PactHandle) -> None: pact_ffi.new_interaction(pact, "http") pact_ffi.new_message_interaction(pact, "async") pact_ffi.new_sync_message_interaction(pact, "sync") - # Test each iterator type + interaction_iter = pact_ffi.pact_model_interaction_iterator(pact.pointer()) + assert all( + isinstance(interaction, pact_ffi.PactInteraction) + for interaction in interaction_iter + ) + http_iter = pact_ffi.pact_handle_get_sync_http_iter(pact) - http_count = sum(1 for _ in http_iter) - assert http_count == 1 + assert all( + isinstance(interaction, pact_ffi.SynchronousHttp) + for interaction in http_iter + ) async_iter = pact_ffi.pact_handle_get_async_message_iter(pact) - async_count = sum(1 for _ in async_iter) - assert async_count == 1 + assert all( + isinstance(interaction, pact_ffi.AsynchronousMessage) + for interaction in async_iter + ) sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact) - sync_count = sum(1 for _ in sync_iter) - assert sync_count == 1 + assert all( + isinstance(interaction, pact_ffi.SynchronousMessage) + for interaction in sync_iter + ) - def test_pact_message_iterator_repr(self) -> None: - """Test PactMessageIterator __repr__ method.""" + +class TestPactModelHandle: + """ + Test basic Pact model pointer handling. + """ + + @pytest.fixture + def pact(self) -> pact_ffi.PactHandle: + """Create a V4 pact for testing.""" pact = pact_ffi.new_pact("consumer", "provider") pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + return pact - iterator = pact_ffi.pact_handle_get_message_iter(pact) - repr_str = repr(iterator) + def test_pact_handle_repr(self, pact: pact_ffi.PactHandle) -> None: + assert str(pact).startswith("PactHandle(") + assert repr(pact).startswith("PactHandle(") - assert "PactMessageIterator" in repr_str - assert "0x" in repr_str or ">" in repr_str + def test_pact_repr(self, pact: pact_ffi.PactHandle) -> None: + pact_model = pact_ffi.pact_handle_to_pointer(pact) - def test_pact_interaction_str_repr(self) -> None: - """Test PactInteraction __str__ and __repr__ methods.""" - pact = pact_ffi.new_pact("consumer", "provider") - pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) - pact_ffi.new_sync_message_interaction(pact, "test") + assert isinstance(pact_model, pact_ffi.Pact) + assert str(pact_model) == "Pact" + assert repr(pact_model).startswith("Pact(") - # Get an interaction from iterator - sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact) - for interaction in sync_iter: - str_result = str(interaction) - repr_result = repr(interaction) - assert "SynchronousMessage" in str_result - assert "SynchronousMessage" in repr_result - break +class TestPactInteractionCasting: + """ + Test casting interactions to specific subtypes via iterators. + """ - def test_multiple_iterator_types_simultaneously(self) -> None: - """Test using multiple iterator types at the same time.""" + @pytest.fixture + def pact(self) -> pact_ffi.PactHandle: + """Create a V4 pact for testing.""" pact = pact_ffi.new_pact("consumer", "provider") pact_ffi.with_specification(pact, pact_ffi.PactSpecification.V4) + return pact - # Create one of each type + def test_synchronous_http_casting(self, pact: pact_ffi.PactHandle) -> None: + """Test SynchronousHttp interaction casting and representation.""" pact_ffi.new_interaction(pact, "http") + + # Test HTTP iterator yields SynchronousHttp + interaction_iter = pact_ffi.pact_model_interaction_iterator(pact.pointer()) + interaction = next(interaction_iter) + assert isinstance(interaction, pact_ffi.PactInteraction) + http = interaction.as_synchronous_http() + assert isinstance(http, pact_ffi.SynchronousHttp) + + with pytest.raises(TypeError): + interaction.as_asynchronous_message() + with pytest.raises(TypeError): + interaction.as_synchronous_message() + + def test_asynchronous_message_casting(self, pact: pact_ffi.PactHandle) -> None: + """Test AsynchronousMessage interaction casting and representation.""" pact_ffi.new_message_interaction(pact, "async") - pact_ffi.new_sync_message_interaction(pact, "sync") - # Create all three iterators - http_iter = pact_ffi.pact_handle_get_sync_http_iter(pact) - async_iter = pact_ffi.pact_handle_get_async_message_iter(pact) - sync_iter = pact_ffi.pact_handle_get_sync_message_iter(pact) + # Test async message iterator yields AsynchronousMessage + interaction_iter = pact_ffi.pact_model_interaction_iterator(pact.pointer()) + interaction = next(interaction_iter) + assert isinstance(interaction, pact_ffi.PactInteraction) + async_msg = interaction.as_asynchronous_message() + assert isinstance(async_msg, pact_ffi.AsynchronousMessage) - # Iterate through all of them - http_list = list(http_iter) - async_list = list(async_iter) - sync_list = list(sync_iter) + with pytest.raises(TypeError): + interaction.as_synchronous_http() + with pytest.raises(TypeError): + interaction.as_synchronous_message() + + def test_synchronous_message_casting(self, pact: pact_ffi.PactHandle) -> None: + """Test SynchronousMessage interaction casting and representation.""" + pact_ffi.new_sync_message_interaction(pact, "sync") - assert len(http_list) == 1 - assert len(async_list) == 1 - assert len(sync_list) == 1 + # Test sync message iterator yields SynchronousMessage + interaction_iter = pact_ffi.pact_model_interaction_iterator(pact.pointer()) + interaction = next(interaction_iter) + assert isinstance(interaction, pact_ffi.PactInteraction) + sync_msg = interaction.as_synchronous_message() + assert isinstance(sync_msg, pact_ffi.SynchronousMessage) + + with pytest.raises(TypeError): + interaction.as_synchronous_http() + with pytest.raises(TypeError): + interaction.as_asynchronous_message() From 91a8574fa62e689f64df24f8da44d5627790d9d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:02:54 +1100 Subject: [PATCH 1194/1376] chore(deps): update taiki-e/install-action action to v2.65.1 (#1417) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 469089db7..7a4075c73 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@61e5998d108b2b55a81b9b386c18bd46e4237e4f # v2.63.1 + uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2.65.1 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index d3ba6856a..a6dee3b18 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@61e5998d108b2b55a81b9b386c18bd46e4237e4f # v2.63.1 + uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2.65.1 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c2bb6f00b..9e013d31e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@61e5998d108b2b55a81b9b386c18bd46e4237e4f # v2.63.1 + uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2.65.1 with: tool: git-cliff,typos From 8996bad4c966b1d1f8556fc83b41496bb8d76be5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:37:28 +1100 Subject: [PATCH 1195/1376] chore(deps): update pre-commit hook bmares/check-json5 to v1.0.1 (#1419) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fd4864076..9ac569036 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - id: yamlfmt - repo: https://gitlab.com/bmares/check-json5 - rev: v1.0.0 + rev: v1.0.1 hooks: # As above, this only checks for valid JSON files. This implementation # allows for comments within JSON files. From 6411a56aac8c578ca5cac7dc51caecf079c6c476 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:38:27 +1100 Subject: [PATCH 1196/1376] chore(deps): update taiki-e/install-action action to v2.65.7 (#1420) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 7a4075c73..99a408831 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2.65.1 + uses: taiki-e/install-action@4c6723ec9c638cccae824b8957c5085b695c8085 # v2.65.7 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index a6dee3b18..a69b0d76d 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2.65.1 + uses: taiki-e/install-action@4c6723ec9c638cccae824b8957c5085b695c8085 # v2.65.7 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e013d31e..c892dbf73 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2.65.1 + uses: taiki-e/install-action@4c6723ec9c638cccae824b8957c5085b695c8085 # v2.65.7 with: tool: git-cliff,typos From aa677219472577c802970730f66f317eb0b3f2e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:38:40 +1100 Subject: [PATCH 1197/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.40.1 (#1421) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9ac569036..ef33fe2f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.40.0 + rev: v1.40.1 hooks: - id: typos exclude: | From b7a7ec21a5e22b246d45ca25765a66e13b0a2f2f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 08:58:47 +1100 Subject: [PATCH 1198/1376] chore(deps): update python:3.14-slim docker digest to f7864aa (#1422) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 7bb862322..92886416b 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:2751cbe93751f0147bc1584be957c6dd4c5f977c3d4e0396b56456a9fd4ed137 +FROM python:3.14-slim@sha256:f7864aa85847985ba72d2dcbcbafd7475354c848e1abbdf84f523a100800ae0b ARG USERNAME=vscode ARG USER_UID=1000 From ac5bf1aab682a6532ea0d5ed6f37164683a7c380 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 08:59:29 +1100 Subject: [PATCH 1199/1376] chore(deps): update pre-commit hook crate-ci/committed to v1.1.9 (#1423) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef33fe2f4..c162978d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,7 +56,7 @@ repos: )$ - repo: https://github.com/crate-ci/committed - rev: v1.1.8 + rev: v1.1.9 hooks: - id: committed From d72f90dbdb298da26afeaec217f0d3d688a6127f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 09:00:39 +1100 Subject: [PATCH 1200/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.41.0 (#1424) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c162978d8..080df99fc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.40.1 + rev: v1.41.0 hooks: - id: typos exclude: | From 9a6d06575101e31d7cdcbd8a5563d6f02573f1b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:00:19 +1100 Subject: [PATCH 1201/1376] chore(deps): update python:3.14-slim docker digest to 3955a7d (#1425) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 92886416b..c41711158 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:f7864aa85847985ba72d2dcbcbafd7475354c848e1abbdf84f523a100800ae0b +FROM python:3.14-slim@sha256:3955a7dd66ccf92b68d0232f7f86d892eaf75255511dc7e98961bdc990dc6c9b ARG USERNAME=vscode ARG USER_UID=1000 From 876f59031f94c5b04512ad4505a92c455949a50e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:01:32 +1100 Subject: [PATCH 1202/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.11 (#1426) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 080df99fc..fa83d67d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.10 + rev: v2.3.11 hooks: - id: biome-check From c3577a021243062559fbf1f10c0690b4e200c279 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 08:22:00 +1100 Subject: [PATCH 1203/1376] chore(deps): update pypa/cibuildwheel action to v3.3.1 (#1428) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 99a408831..f3d30b655 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@63fd63b352a9a8bdcc24791c9dbee952ee9a8abc # v3.3.0 + uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 with: package-dir: pact-python-cli env: diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index a69b0d76d..6233e94ce 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@63fd63b352a9a8bdcc24791c9dbee952ee9a8abc # v3.3.0 + uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 with: package-dir: pact-python-ffi env: From a3571013f87a3c663b660504ae96dad12860dd4c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 08:22:08 +1100 Subject: [PATCH 1204/1376] chore(deps): update pre-commit hook google/yamlfmt to v0.21.0 (#1429) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa83d67d8..30e1aaebf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/google/yamlfmt - rev: v0.20.0 + rev: v0.21.0 hooks: - id: yamlfmt From 4e90d5b392600e4779e14834773bb0cfae716303 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:57:48 +1100 Subject: [PATCH 1205/1376] chore(deps): update taiki-e/install-action action to v2.65.13 (#1427) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index f3d30b655..b2d8ef08c 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@4c6723ec9c638cccae824b8957c5085b695c8085 # v2.65.7 + uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 6233e94ce..f0cb8def3 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@4c6723ec9c638cccae824b8957c5085b695c8085 # v2.65.7 + uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c892dbf73..fdb95fdf2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@4c6723ec9c638cccae824b8957c5085b695c8085 # v2.65.7 + uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13 with: tool: git-cliff,typos From 51879481d43815d35eb4dd9d580060adf17c5364 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:11:52 +1100 Subject: [PATCH 1206/1376] chore(deps): update astral-sh/setup-uv action to v7.2.0 (#1431) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index b2d8ef08c..63064984f 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index f0cb8def3..2e31af650 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fdb95fdf2..950185178 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8b10145a0..c57ee59c7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 377322d05..c94507d1b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 with: enable-cache: true @@ -150,7 +150,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 with: enable-cache: true @@ -194,7 +194,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 with: enable-cache: true @@ -226,7 +226,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 with: enable-cache: true @@ -259,7 +259,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 with: enable-cache: true @@ -299,7 +299,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 with: enable-cache: true cache-suffix: prek From dc319469c26c9700ec565b5a0a547242a87fc168 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:13:52 +1100 Subject: [PATCH 1207/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.42.0 (#1432) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 30e1aaebf..90e0adf26 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.41.0 + rev: v1.42.0 hooks: - id: typos exclude: | From 7499ee789bbc0244c830190f4621735f85a3895c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:16:44 +1100 Subject: [PATCH 1208/1376] chore(deps): update ruff to v0.14.11 (#1433) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 90e0adf26..e14875188 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.10 + rev: v0.14.11 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index ce8920454..e03270578 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.10", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.11", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 68948a027..d8c0fae52 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.14.10", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.11", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 1457a63a2..c2ed13ec5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.14.10", + "ruff==0.14.11", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From 450a4865009a377a16c31ffb17a36feff4db354c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:19:31 +1100 Subject: [PATCH 1209/1376] chore(deps): update dependency pathspec to v1 (#1430) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c2ed13ec5..77d7719ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,7 @@ docs = [ "mkdocs-section-index==0.3.10", "mkdocs==1.6.1", "mkdocstrings[python]==1.0.0", - "pathspec==0.12.1", + "pathspec==1.0.3", ] example = [ "fastapi~=0.0", From dc4a38302de58986f818c029d9046abb34121082 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:21:18 +1100 Subject: [PATCH 1210/1376] chore(deps): update pre-commit hook crate-ci/committed to v1.1.10 (#1434) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e14875188..700399f2a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,7 +56,7 @@ repos: )$ - repo: https://github.com/crate-ci/committed - rev: v1.1.9 + rev: v1.1.10 hooks: - id: committed From 8c4bfd6cb2540c0c0872cb9d9683525a527170f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 02:32:08 +0000 Subject: [PATCH 1211/1376] chore(deps): update dependency griffe-pydantic to v1.2.0 (#1437) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 77d7719ca..bfb695d7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ dev = [ docs = [ "griffe-generics==1.0.13", "griffe-inherited-method-crossrefs==0.0.1.4", - "griffe-pydantic==1.1.8", + "griffe-pydantic==1.2.0", "griffe-warnings-deprecated==1.1.0", "mkdocs-gen-files==0.6.0", "mkdocs-github-admonitions-plugin==0.1.1", From 9998ace988de9ce8afaa6df40c051c9420be27a0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:32:41 +1100 Subject: [PATCH 1212/1376] chore(deps): update python:3.14-slim docker digest to 1f741ae (#1436) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index c41711158..7425112fd 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:3955a7dd66ccf92b68d0232f7f86d892eaf75255511dc7e98961bdc990dc6c9b +FROM python:3.14-slim@sha256:1f741aef81d09464251f4c52c83a02f93ece0a636db125d411bd827bf381a763 ARG USERNAME=vscode ARG USER_UID=1000 From ffb3d759ba7c86fa515ede26cc9e3f4e605b34d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:35:24 +1100 Subject: [PATCH 1213/1376] chore(deps): update taiki-e/install-action action to v2.66.5 (#1435) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 63064984f..7f29890cf 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13 + uses: taiki-e/install-action@2e9d707ef49c9b094d45955b60c7e5c0dfedeb14 # v2.66.5 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 2e31af650..c10f542fd 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13 + uses: taiki-e/install-action@2e9d707ef49c9b094d45955b60c7e5c0dfedeb14 # v2.66.5 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 950185178..3446a01f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13 + uses: taiki-e/install-action@2e9d707ef49c9b094d45955b60c7e5c0dfedeb14 # v2.66.5 with: tool: git-cliff,typos From 69d49277a571ca7a65716cc7079565d40eadc65b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:41:35 +1100 Subject: [PATCH 1214/1376] chore(deps): update ruff to v0.14.13 (#1438) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 700399f2a..8a8b8d41b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.11 + rev: v0.14.13 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index e03270578..f4212990d 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.11", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.13", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index d8c0fae52..fa10ca7d1 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.14.11", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.13", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index bfb695d7d..4f4ad170a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.14.11", + "ruff==0.14.13", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From 463c46238daf8c597204f61131b9ea75d1d1dc2d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:44:55 +1100 Subject: [PATCH 1215/1376] chore(deps): update actions/cache action to v5.0.2 (#1439) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c94507d1b..15c5e720c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -285,7 +285,7 @@ jobs: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Cache prek - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: |- ~/.cache/prek From 7bab8e983cf928a5c49f64a04a3266ec45b96b46 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:45:41 +1100 Subject: [PATCH 1216/1376] chore(deps): update python:3.14-slim docker digest to 9b81fe9 (#1440) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 7425112fd..4d1084c63 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:1f741aef81d09464251f4c52c83a02f93ece0a636db125d411bd827bf381a763 +FROM python:3.14-slim@sha256:9b81fe9acff79e61affb44aaf3b6ff234392e8ca477cb86c9f7fd11732ce9b6a ARG USERNAME=vscode ARG USER_UID=1000 From 641a14f5bae108ebd1c1348bd4815093eda3b5f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:47:36 +1100 Subject: [PATCH 1217/1376] chore(deps): update dependency mkdocstrings to v1.0.1 (#1442) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4f4ad170a..3f96ce4c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ docs = [ "mkdocs-material[recommended,git,imaging]==9.7.1", "mkdocs-section-index==0.3.10", "mkdocs==1.6.1", - "mkdocstrings[python]==1.0.0", + "mkdocstrings[python]==1.0.1", "pathspec==1.0.3", ] example = [ From 456ec37d51e9e3bd76a7c518420dae5b236d1817 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:48:22 +1100 Subject: [PATCH 1218/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.42.1 (#1443) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a8b8d41b..1e451098d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.42.0 + rev: v1.42.1 hooks: - id: typos exclude: | From d5ea61662a26cc837d79a8cbb2a9b717988e3ac5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:52:39 +1100 Subject: [PATCH 1219/1376] chore(deps): update taiki-e/install-action action to v2.67.1 (#1441) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 7f29890cf..e119a649b 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@2e9d707ef49c9b094d45955b60c7e5c0dfedeb14 # v2.66.5 + uses: taiki-e/install-action@30791125b88ed3200de59e9b5edbf78d1949e41f # v2.67.1 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index c10f542fd..ba6fd3036 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@2e9d707ef49c9b094d45955b60c7e5c0dfedeb14 # v2.66.5 + uses: taiki-e/install-action@30791125b88ed3200de59e9b5edbf78d1949e41f # v2.67.1 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3446a01f2..2ebe27cdb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@2e9d707ef49c9b094d45955b60c7e5c0dfedeb14 # v2.66.5 + uses: taiki-e/install-action@30791125b88ed3200de59e9b5edbf78d1949e41f # v2.67.1 with: tool: git-cliff,typos From 0a0a99b06d9416d964fbf558eec59c734f4865ff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:38:05 +1100 Subject: [PATCH 1220/1376] chore(deps): update taiki-e/install-action action to v2.67.10 (#1449) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index e119a649b..d91ba0ce8 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@30791125b88ed3200de59e9b5edbf78d1949e41f # v2.67.1 + uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index ba6fd3036..1cf3faad1 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@30791125b88ed3200de59e9b5edbf78d1949e41f # v2.67.1 + uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ebe27cdb..2c2c638c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@30791125b88ed3200de59e9b5edbf78d1949e41f # v2.67.1 + uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 with: tool: git-cliff,typos From c15b3685c3fc84cfa091182453429662f276b904 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:38:19 +1100 Subject: [PATCH 1221/1376] chore(deps): update dependency mkdocstrings to v1.0.2 (#1448) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3f96ce4c5..e8d06ff70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ docs = [ "mkdocs-material[recommended,git,imaging]==9.7.1", "mkdocs-section-index==0.3.10", "mkdocs==1.6.1", - "mkdocstrings[python]==1.0.1", + "mkdocstrings[python]==1.0.2", "pathspec==1.0.3", ] example = [ From c698f11c902361e490f569bafa9945ff4389da55 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:38:33 +1100 Subject: [PATCH 1222/1376] chore(deps): update peter-evans/create-pull-request action to v8.1.0 (#1444) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index d91ba0ce8..7e3c7df4b 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -201,7 +201,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 1cf3faad1..3fa38f1ab 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -202,7 +202,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c2c638c8..94cf8e0b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -168,7 +168,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' From 49490db4189a05b9f3daa91f83dc07c3ea1cf8ec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:38:49 +1100 Subject: [PATCH 1223/1376] chore(deps): update actions/checkout action to v6.0.2 (#1445) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 6 +++--- .github/workflows/build-ffi.yml | 6 +++--- .github/workflows/build.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 7e3c7df4b..f37c1cbe5 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 @@ -94,7 +94,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 @@ -135,7 +135,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 3fa38f1ab..5c4bd62dd 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 @@ -94,7 +94,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 @@ -136,7 +136,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94cf8e0b5..8f4c33bf1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 @@ -104,7 +104,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c57ee59c7..7c5ef4533 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 2c1a40f73..92d88b083 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Synchronize labels uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15c5e720c..c8ccd3a8d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 submodules: true @@ -145,7 +145,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 @@ -189,7 +189,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 @@ -221,7 +221,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 @@ -254,7 +254,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 @@ -282,7 +282,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Cache prek uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 From aee3b5d09e85734922b22901cd4467b937afee0f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:39:06 +1100 Subject: [PATCH 1224/1376] chore(deps): update ruff to v0.14.14 (#1446) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e451098d..f102f0c1a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.13 + rev: v0.14.14 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index f4212990d..cb7d483cd 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.13", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.14", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index fa10ca7d1..eed206b83 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.14.13", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.14.14", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index e8d06ff70..b4f338f0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.14.13", + "ruff==0.14.14", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From a073496c177d66a2e92bf9803e20d1e3e5c1e89e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:39:22 +1100 Subject: [PATCH 1225/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.12 (#1447) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f102f0c1a..0c89b8dd6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.11 + rev: v2.3.12 hooks: - id: biome-check From 11187fa753a1ccc228e12bb59a4e9a2ce7521626 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 08:30:46 +1100 Subject: [PATCH 1226/1376] chore(deps): update dependency pathspec to v1.0.4 (#1451) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b4f338f0d..f8f8c3a42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,7 @@ docs = [ "mkdocs-section-index==0.3.10", "mkdocs==1.6.1", "mkdocstrings[python]==1.0.2", - "pathspec==1.0.3", + "pathspec==1.0.4", ] example = [ "fastapi~=0.0", From 71ee5eace62fd94ad5ee2920d5d642a7ae7dc3e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 08:30:54 +1100 Subject: [PATCH 1227/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.13 (#1452) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c89b8dd6..e70bc4cf3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.12 + rev: v2.3.13 hooks: - id: biome-check From 0e731c986904af041d044f01c258c41857f73028 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 08:57:10 +1100 Subject: [PATCH 1228/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.42.3 (#1450) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e70bc4cf3..acba1d058 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.42.1 + rev: v1.42.3 hooks: - id: typos exclude: | From bd7a82e56ff79fa8a84bafb9d5dd989508ca66db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:42:46 +1100 Subject: [PATCH 1229/1376] chore(deps): update actions/cache action to v5.0.3 (#1453) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c8ccd3a8d..2eb388d0e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -285,7 +285,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Cache prek - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: |- ~/.cache/prek From ce3683d72eb9b8c68e839bb0250fedad77f735d3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:46:47 +1100 Subject: [PATCH 1230/1376] chore(deps): update astral-sh/setup-uv action to v7.2.1 (#1454) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index f37c1cbe5..562f320ae 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 5c4bd62dd..59a5cb935 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f4c33bf1..47e04a5c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7c5ef4533..aae86120d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2eb388d0e..185c6b8d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 with: enable-cache: true @@ -150,7 +150,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 with: enable-cache: true @@ -194,7 +194,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 with: enable-cache: true @@ -226,7 +226,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 with: enable-cache: true @@ -259,7 +259,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 with: enable-cache: true @@ -299,7 +299,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 with: enable-cache: true cache-suffix: prek From c394d60b267232abe5bb54e4c1f3a3d7ce1131be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:59:09 +1100 Subject: [PATCH 1231/1376] chore(deps): update taiki-e/install-action action to v2.67.18 (#1455) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 562f320ae..1ab83371f 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 + uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 59a5cb935..ba2c1aa9b 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 + uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 47e04a5c8..fbb10a47f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 + uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 with: tool: git-cliff,typos From d50e1a46a5481911525415cbc0cb1a0f8f7e89a0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:29:44 +1100 Subject: [PATCH 1232/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.43.0 (#1457) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index acba1d058..0dadfad8f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.42.3 + rev: v1.43.0 hooks: - id: typos exclude: | From 71b5d03f751ed7fc086487183ffa5c1a4774aa51 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:46:01 +1100 Subject: [PATCH 1233/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.43.1 (#1459) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0dadfad8f..ea90419eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.43.0 + rev: v1.43.1 hooks: - id: typos exclude: | From a1c44740986056dbb47c45e9aa3ce622b36d3a49 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:46:19 +1100 Subject: [PATCH 1234/1376] chore(deps): update python:3.14-slim docker digest to 1a3c6db (#1458) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 4d1084c63..1d949c6d1 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:9b81fe9acff79e61affb44aaf3b6ff234392e8ca477cb86c9f7fd11732ce9b6a +FROM python:3.14-slim@sha256:1a3c6dbfd2173971abba880c3cc2ec4643690901f6ad6742d0827bae6cefc925 ARG USERNAME=vscode ARG USER_UID=1000 From de600506fe9ff4b67f925cccdb96d5e0b8cad9fd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:18:00 +1100 Subject: [PATCH 1235/1376] chore(deps): update python:3.14-slim docker digest to fa0acdc (#1462) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 1d949c6d1..e086eb082 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:1a3c6dbfd2173971abba880c3cc2ec4643690901f6ad6742d0827bae6cefc925 +FROM python:3.14-slim@sha256:fa0acdcd760f0bf265bc2c1ee6120776c4d92a9c3a37289e17b9642ad2e5b83b ARG USERNAME=vscode ARG USER_UID=1000 From 4163d63f1eeff257916465a0d2e5466efdf1fd9a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:18:13 +1100 Subject: [PATCH 1236/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.14 (#1461) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ea90419eb..5ca71c0a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.13 + rev: v2.3.14 hooks: - id: biome-check From e8089cd9a8fe663bb9031d379c76b874f6df6a71 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:29:42 +1100 Subject: [PATCH 1237/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.43.2 (#1463) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ca71c0a3..00c70e6e9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.43.1 + rev: v1.43.2 hooks: - id: typos exclude: | From f98fc0a035b3777f7f112a68ea994a7159f25f37 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 16:44:09 +1100 Subject: [PATCH 1238/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.43.3 (#1464) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 00c70e6e9..d172790d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.43.2 + rev: v1.43.3 hooks: - id: typos exclude: | From cf3e49e3afe55f3406338761d360b3781a78bfc5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 16:44:21 +1100 Subject: [PATCH 1239/1376] chore(deps): update astral-sh/setup-uv action to v7.3.0 (#1465) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 1ab83371f..8fac1dfb1 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index ba2c1aa9b..0772bc717 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fbb10a47f..7b08c2270 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index aae86120d..a942b8afe 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 185c6b8d4..05cfd5644 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: enable-cache: true @@ -150,7 +150,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: enable-cache: true @@ -194,7 +194,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: enable-cache: true @@ -226,7 +226,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: enable-cache: true @@ -259,7 +259,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: enable-cache: true @@ -299,7 +299,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1 + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 with: enable-cache: true cache-suffix: prek From 28a7012f5f7ec87022bc7c8b5326652501a122ec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:53:51 +1100 Subject: [PATCH 1240/1376] chore(deps): update python:3.14-slim docker digest to 486b809 (#1466) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index e086eb082..51a933379 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:fa0acdcd760f0bf265bc2c1ee6120776c4d92a9c3a37289e17b9642ad2e5b83b +FROM python:3.14-slim@sha256:486b8092bfb12997e10d4920897213a06563449c951c5506c2a2cfaf591c599f ARG USERNAME=vscode ARG USER_UID=1000 From bbfae161d393d2d8dfad78e1ccb4926a2dd29bdb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:55:33 +1100 Subject: [PATCH 1241/1376] chore(deps): update dependency mkdocstrings to v1.0.3 (#1467) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f8f8c3a42..161f974b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ docs = [ "mkdocs-material[recommended,git,imaging]==9.7.1", "mkdocs-section-index==0.3.10", "mkdocs==1.6.1", - "mkdocstrings[python]==1.0.2", + "mkdocstrings[python]==1.0.3", "pathspec==1.0.4", ] example = [ From 90d25f3d3228262937af420c3ffa55a7d0dbe69d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 21:01:42 +1100 Subject: [PATCH 1242/1376] chore(deps): update taiki-e/install-action action to v2.67.26 (#1468) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 8fac1dfb1..712c2774f 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 + uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 0772bc717..8f01ee9b4 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 + uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b08c2270..5c1627cd7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 + uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 with: tool: git-cliff,typos From 2b3c9c43368d7e14410e03b553a63132b59e2a62 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:21:31 +1100 Subject: [PATCH 1243/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.3.15 (#1471) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d172790d9..2be4b2de5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.14 + rev: v2.3.15 hooks: - id: biome-check From e391faf3ff7fad12a34d1ae0748990737f8730c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:44:23 +1100 Subject: [PATCH 1244/1376] chore(deps): update dependency griffe-pydantic to v1.3.0 (#1470) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 161f974b4..7d7a6b884 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ dev = [ docs = [ "griffe-generics==1.0.13", "griffe-inherited-method-crossrefs==0.0.1.4", - "griffe-pydantic==1.2.0", + "griffe-pydantic==1.3.0", "griffe-warnings-deprecated==1.1.0", "mkdocs-gen-files==0.6.0", "mkdocs-github-admonitions-plugin==0.1.1", From 2849c0389e0888d2162e8efe3f26cc243c65658d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:45:34 +1100 Subject: [PATCH 1245/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.43.4 (#1469) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2be4b2de5..a36ed265e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.43.3 + rev: v1.43.4 hooks: - id: typos exclude: | From 4a4255a6e6d62558162521fee3e4e4b0ad980f42 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 01:25:16 +0000 Subject: [PATCH 1246/1376] chore(deps): update taiki-e/install-action action to v2.67.30 (#1472) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 712c2774f..611f57d8e 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 + uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 8f01ee9b4..981ef6249 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 + uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c1627cd7..725637ea8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@509565405a8a987e73cf742e26b26dcc72c4b01a # v2.67.26 + uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 with: tool: git-cliff,typos From 1ddca62f12a0f83e321c84e6bca7e1dac8ab6254 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 01:27:56 +0000 Subject: [PATCH 1247/1376] chore(deps): update pre-commit hook davidanson/markdownlint-cli2 to v0.21.0 (#1473) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a36ed265e..bb403c1e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,7 +61,7 @@ repos: - id: committed - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.20.0 + rev: v0.21.0 hooks: - id: markdownlint-cli2 From 4538055b97cf582b32d4a2b8f1d7dac2881046b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:41:09 +1100 Subject: [PATCH 1248/1376] chore(deps): update ruff to v0.15.1 (#1460) Co-authored-by: JP-Ellis --- .pre-commit-config.yaml | 2 +- docs/scripts/other.py | 2 +- pact-python-cli/hatch_build.py | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/hatch_build.py | 2 +- pact-python-ffi/pyproject.toml | 2 +- pact-python-ffi/src/pact_ffi/__init__.py | 4 ++-- pyproject.toml | 2 +- src/pact/_util.py | 2 +- tests/compatibility_suite/util/consumer.py | 4 ++-- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb403c1e0..cf0442ec3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.14 + rev: v0.15.1 hooks: - id: ruff-check exclude: | diff --git a/docs/scripts/other.py b/docs/scripts/other.py index 34bfde76e..31d60dbf2 100644 --- a/docs/scripts/other.py +++ b/docs/scripts/other.py @@ -72,7 +72,7 @@ def is_binary(buffer: bytes) -> bool: for source_path in ALL_FILES: if not source_path.is_file(): continue - if source_path.parts[0] in ["docs"]: + if source_path.parts[0] == "docs": continue dest_path = Path(DOCS_DEST, source_path) diff --git a/pact-python-cli/hatch_build.py b/pact-python-cli/hatch_build.py index da0e0f3c9..650b356c0 100644 --- a/pact-python-cli/hatch_build.py +++ b/pact-python-cli/hatch_build.py @@ -238,7 +238,7 @@ def _download(self, url: str) -> Path: Returns: The path to the downloaded artefact. """ - filename = url.split("/")[-1] + filename = url.rsplit("/", maxsplit=1)[-1] artefact = PKG_DIR / "data" / filename artefact.parent.mkdir(parents=True, exist_ok=True) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index cb7d483cd..528ad5e52 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.14.14", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.1", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1"] diff --git a/pact-python-ffi/hatch_build.py b/pact-python-ffi/hatch_build.py index f730b2192..80d0e7bc6 100644 --- a/pact-python-ffi/hatch_build.py +++ b/pact-python-ffi/hatch_build.py @@ -383,7 +383,7 @@ def _download(self, url: str) -> Path: Returns: The path to the downloaded artefact. """ - filename = url.split("/")[-1] + filename = url.rsplit("/", maxsplit=1)[-1] artefact = PKG_DIR / "data" / filename artefact.parent.mkdir(parents=True, exist_ok=True) diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index eed206b83..983b42777 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.14.14", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.1", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1", "typing-extensions~=4.0"] diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index ca0141605..04ae931b5 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -6452,7 +6452,7 @@ def with_binary_body( interaction._ref, part.value, content_type.encode("utf-8") if content_type else ffi.NULL, - body if body else ffi.NULL, + body or ffi.NULL, len(body) if body else 0, ) if not success: @@ -6511,7 +6511,7 @@ def with_binary_file( interaction._ref, part.value, content_type.encode("utf-8") if content_type else ffi.NULL, - body if body else ffi.NULL, + body or ffi.NULL, len(body) if body else 0, ) if not success: diff --git a/pyproject.toml b/pyproject.toml index 7d7a6b884..9edd53f27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.14.14", + "ruff==0.15.1", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, diff --git a/src/pact/_util.py b/src/pact/_util.py index 6fe8cc2e4..ba7dcff73 100644 --- a/src/pact/_util.py +++ b/src/pact/_util.py @@ -151,7 +151,7 @@ def format_code_to_java_format(code: str) -> str: raise ValueError(msg) # The following codes simply do not have a direct equivalent in Java. - if code in ["w"]: + if code == "w": msg = f"Python format code `%{code}` is not supported in Java" raise ValueError(msg) diff --git a/tests/compatibility_suite/util/consumer.py b/tests/compatibility_suite/util/consumer.py index 237a9b804..673ebbca9 100644 --- a/tests/compatibility_suite/util/consumer.py +++ b/tests/compatibility_suite/util/consumer.py @@ -239,7 +239,7 @@ def _( if definition.query else None ), - headers=definition.headers if definition.headers else None, # type: ignore[arg-type] + headers=definition.headers or None, # type: ignore[arg-type] data=definition.body.bytes if definition.body else None, timeout=5, ) @@ -293,7 +293,7 @@ def _( if definition.query else None ), - headers=definition.headers if definition.headers else None, # type: ignore[arg-type] + headers=definition.headers or None, # type: ignore[arg-type] data=definition.body.bytes if definition.body else None, timeout=5, ) From b34014b6464012e66eb757b26b63038d4ef30c25 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 06:49:46 +0000 Subject: [PATCH 1249/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.4.0 (#1474) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf0442ec3..e6310d442 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.3.15 + rev: v2.4.0 hooks: - id: biome-check From 34c252237956721d545ae0b0e130971cee6a2f39 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 07:17:18 +1100 Subject: [PATCH 1250/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.43.5 (#1475) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e6310d442..b74eea5ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.43.4 + rev: v1.43.5 hooks: - id: typos exclude: | From a59ededae14d380f97f3ddfefdd8d7c077543598 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:41:04 +1100 Subject: [PATCH 1251/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.4.2 (#1476) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b74eea5ef..0ce407f92 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.4.0 + rev: v2.4.2 hooks: - id: biome-check From b8f607982a95a8f5109518c64a0619f8ec69e4d3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:22:40 +1100 Subject: [PATCH 1252/1376] chore(deps): update dependency mkdocs-material to v9.7.2 (#1477) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9edd53f27..42db56eef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,7 @@ docs = [ "mkdocs-github-admonitions-plugin==0.1.1", "mkdocs-literate-nav==0.6.2", "mkdocs-llmstxt==0.5.0", - "mkdocs-material[recommended,git,imaging]==9.7.1", + "mkdocs-material[recommended,git,imaging]==9.7.2", "mkdocs-section-index==0.3.10", "mkdocs==1.6.1", "mkdocstrings[python]==1.0.3", From 3cdab2924f293bfbeb8fc51904b7478646cd28ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 10:19:37 +1100 Subject: [PATCH 1253/1376] chore(deps): update ruff to v0.15.2 (#1478) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0ce407f92..357f7a923 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.1 + rev: v0.15.2 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 528ad5e52..221cb5d3f 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.15.1", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.2", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 983b42777..6e1991151 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.15.1", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.2", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 42db56eef..508328ba4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.15.1", + "ruff==0.15.2", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From 54715f791f6b4217e3545e4830893b11c047a38f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 10:22:40 +1100 Subject: [PATCH 1254/1376] chore(deps): update dependency griffe-pydantic to v1.3.1 (#1480) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 508328ba4..9c89a3a7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ dev = [ docs = [ "griffe-generics==1.0.13", "griffe-inherited-method-crossrefs==0.0.1.4", - "griffe-pydantic==1.3.0", + "griffe-pydantic==1.3.1", "griffe-warnings-deprecated==1.1.0", "mkdocs-gen-files==0.6.0", "mkdocs-github-admonitions-plugin==0.1.1", From 3117a6d0084b05253eca472b0f6e02bab8cd0950 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 10:27:33 +1100 Subject: [PATCH 1255/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.4.4 (#1479) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 357f7a923..f0f86363f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.4.2 + rev: v2.4.4 hooks: - id: biome-check From ffb85afc2501ec00e50fc933ef0e29f43df1c459 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 01:41:16 +0000 Subject: [PATCH 1256/1376] chore(deps): update dependency griffe-warnings-deprecated to v1.1.1 (#1481) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9c89a3a7f..5a293a781 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ docs = [ "griffe-generics==1.0.13", "griffe-inherited-method-crossrefs==0.0.1.4", "griffe-pydantic==1.3.1", - "griffe-warnings-deprecated==1.1.0", + "griffe-warnings-deprecated==1.1.1", "mkdocs-gen-files==0.6.0", "mkdocs-github-admonitions-plugin==0.1.1", "mkdocs-literate-nav==0.6.2", From ed8fed0ff7999d7796f755d94b2956b8292d6ae3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:37:58 +1100 Subject: [PATCH 1257/1376] chore(deps): update taiki-e/install-action action to v2.68.7 (#1482) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 611f57d8e..8b39a58f8 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 + uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 981ef6249..ce0243e9e 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 + uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 725637ea8..7a3b63828 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@288875dd3d64326724fa6d9593062d9f8ba0b131 # v2.67.30 + uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7 with: tool: git-cliff,typos From 2e6ada2827cfe28aac4c63613b9f552e7e8ae589 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:04:36 +1100 Subject: [PATCH 1258/1376] chore(deps): update dependency mkdocs-material to v9.7.3 (#1483) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5a293a781..87121b6c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,7 @@ docs = [ "mkdocs-github-admonitions-plugin==0.1.1", "mkdocs-literate-nav==0.6.2", "mkdocs-llmstxt==0.5.0", - "mkdocs-material[recommended,git,imaging]==9.7.2", + "mkdocs-material[recommended,git,imaging]==9.7.3", "mkdocs-section-index==0.3.10", "mkdocs==1.6.1", "mkdocstrings[python]==1.0.3", From 54d95f128ddcf6a0aa3815a5dbac18c48e6a508b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:04:42 +1100 Subject: [PATCH 1259/1376] chore(deps): update pre-commit hook crate-ci/committed to v1.1.11 (#1484) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f0f86363f..2e8d7b8cf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,7 +56,7 @@ repos: )$ - repo: https://github.com/crate-ci/committed - rev: v1.1.10 + rev: v1.1.11 hooks: - id: committed From 65f246988a53497d97aa90da3a615df98c9c7055 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:11:43 +1100 Subject: [PATCH 1260/1376] chore(deps): update python:3.14-slim docker digest to 9006fc6 (#1485) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 51a933379..81508a2f1 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:486b8092bfb12997e10d4920897213a06563449c951c5506c2a2cfaf591c599f +FROM python:3.14-slim@sha256:9006fc63e3eaedc00ebc81193c99528575a2f9b9e3fb36d95e94814c23f31f47 ARG USERNAME=vscode ARG USER_UID=1000 From d3d2d840fa3580615608f3dbd48c30de2e8b3d1e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 17 Mar 2026 21:12:45 +1100 Subject: [PATCH 1261/1376] chore: upgrade stable python version Signed-off-by: JP-Ellis --- .github/workflows/build.yml | 2 +- examples/container-compose.yml | 2 -- tests/compatibility_suite/util/pact-broker.yml | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a3b63828..c5462b7ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: - STABLE_PYTHON_VERSION: '39' + STABLE_PYTHON_VERSION: '310' HATCH_VERBOSE: '1' FORCE_COLOR: '1' CIBW_BUILD_FRONTEND: build diff --git a/examples/container-compose.yml b/examples/container-compose.yml index 15aad7288..d5796e003 100644 --- a/examples/container-compose.yml +++ b/examples/container-compose.yml @@ -1,6 +1,4 @@ --- -version: '3.9' - services: broker: image: pactfoundation/pact-broker:latest-multi diff --git a/tests/compatibility_suite/util/pact-broker.yml b/tests/compatibility_suite/util/pact-broker.yml index 259375468..d42d063d7 100644 --- a/tests/compatibility_suite/util/pact-broker.yml +++ b/tests/compatibility_suite/util/pact-broker.yml @@ -1,6 +1,4 @@ --- -version: '3.9' - services: postgres: image: postgres From 636bd51b3e5b8de92ed9f8da113e0f58890069d1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:11:04 +1100 Subject: [PATCH 1262/1376] chore(deps): update softprops/action-gh-release action to v2.6.1 (#1497) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 8b39a58f8..d7e2bdc31 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -186,7 +186,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index ce0243e9e..8d2d65682 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -187,7 +187,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5462b7ea..6d73ed40e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,7 +153,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md From c699c57631cd62a939a826f98a81236c5d0dd699 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:11:06 +1100 Subject: [PATCH 1263/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.44.0 (#1491) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2e8d7b8cf..236ff811b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.43.5 + rev: v1.44.0 hooks: - id: typos exclude: | From ce4636221cd77a93224804f35af70f86c8afe48c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:11:09 +1100 Subject: [PATCH 1264/1376] chore(deps): update ruff to v0.15.6 (#1486) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 236ff811b..14710cf3f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.2 + rev: v0.15.6 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 221cb5d3f..485016b84 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.15.2", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.6", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 6e1991151..bfd14df1b 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.15.2", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.6", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 87121b6c0..a5049f4a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.15.2", + "ruff==0.15.6", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From e750606cd850d965377aee24347f1959d5b803fd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:11:11 +1100 Subject: [PATCH 1265/1376] chore(deps): update dependency mkdocs-material to v9.7.5 (#1495) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a5049f4a5..60d24020a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,7 @@ docs = [ "mkdocs-github-admonitions-plugin==0.1.1", "mkdocs-literate-nav==0.6.2", "mkdocs-llmstxt==0.5.0", - "mkdocs-material[recommended,git,imaging]==9.7.3", + "mkdocs-material[recommended,git,imaging]==9.7.5", "mkdocs-section-index==0.3.10", "mkdocs==1.6.1", "mkdocstrings[python]==1.0.3", From ba9f85613e616848e25fc5a5d256ab2ceb4bf576 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:11:15 +1100 Subject: [PATCH 1266/1376] chore(deps): update python:3.14-slim docker digest to 584e89d (#1490) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 81508a2f1..db63bf3d8 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:9006fc63e3eaedc00ebc81193c99528575a2f9b9e3fb36d95e94814c23f31f47 +FROM python:3.14-slim@sha256:584e89d31009a79ae4d9e3ab2fba078524a6c0921cb2711d05e8bb5f628fc9b9 ARG USERNAME=vscode ARG USER_UID=1000 From 0cb7e0d0622f2914f80caec85636bd371dad2100 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:11:17 +1100 Subject: [PATCH 1267/1376] chore(deps): update dependency mkdocs-gen-files to v0.6.1 (#1498) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 60d24020a..c5df94c61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ docs = [ "griffe-inherited-method-crossrefs==0.0.1.4", "griffe-pydantic==1.3.1", "griffe-warnings-deprecated==1.1.1", - "mkdocs-gen-files==0.6.0", + "mkdocs-gen-files==0.6.1", "mkdocs-github-admonitions-plugin==0.1.1", "mkdocs-literate-nav==0.6.2", "mkdocs-llmstxt==0.5.0", From 7f83448ecc7cb7c87600b4f0eba5fa1440086ed9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:11:19 +1100 Subject: [PATCH 1268/1376] chore(deps): update github artifact actions (major) (#1487) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 6 +++--- .github/workflows/build-ffi.yml | 6 +++--- .github/workflows/build.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index d7e2bdc31..e15a4d41b 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -70,7 +70,7 @@ jobs: run: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-sdist path: pact-python-cli/dist/*.tar* @@ -106,7 +106,7 @@ jobs: CIBW_BUILD: cp${{ env.STABLE_PYTHON_VERSION }}-* - name: Upload wheels - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-${{ matrix.os }} path: wheelhouse/*.whl @@ -179,7 +179,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - name: Download wheels and sdist - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: wheelhouse merge-multiple: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 8d2d65682..db14e5427 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -70,7 +70,7 @@ jobs: run: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-sdist path: pact-python-ffi/dist/*.tar* @@ -107,7 +107,7 @@ jobs: HATCH_VERBOSE: '1' - name: Upload wheels - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-${{ matrix.os }} path: wheelhouse/*.whl @@ -180,7 +180,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - name: Download wheels and sdist - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: wheelhouse merge-multiple: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d73ed40e..b771a31f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: run: hatch build - name: Upload sdist - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-sdist path: ./dist/*.tar* @@ -76,7 +76,7 @@ jobs: compression-level: 0 - name: Upload wheel - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: wheels-whl path: ./dist/*.whl @@ -146,7 +146,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - name: Download wheels and sdist - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: wheelhouse merge-multiple: true From c3cb053f88cf81d2e0c1ff0e0ba1a435eb942020 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:11:34 +1100 Subject: [PATCH 1269/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.4.7 (#1494) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14710cf3f..7217b34da 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.4.4 + rev: v2.4.7 hooks: - id: biome-check From 5b21b1ad2bc4ee6e226ac7d4983a7bc8cf5fa4b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:11:36 +1100 Subject: [PATCH 1270/1376] chore(deps): update taiki-e/install-action action to v2.68.35 (#1493) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index e15a4d41b..02c34b75f 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7 + uses: taiki-e/install-action@94a7388bec5d4c8dd93e3ebf09e0ff448f3f6f4d # v2.68.35 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index db14e5427..9ab3c092c 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7 + uses: taiki-e/install-action@94a7388bec5d4c8dd93e3ebf09e0ff448f3f6f4d # v2.68.35 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b771a31f5..1e1f43046 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7 + uses: taiki-e/install-action@94a7388bec5d4c8dd93e3ebf09e0ff448f3f6f4d # v2.68.35 with: tool: git-cliff,typos From 50ad26572b056aaf0ad3ef71104f09ba0ac24a7d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:11:42 +1100 Subject: [PATCH 1271/1376] chore(deps): update dependency mkdocs-literate-nav to v0.6.3 (#1499) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c5df94c61..9d2aab782 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ docs = [ "griffe-warnings-deprecated==1.1.1", "mkdocs-gen-files==0.6.1", "mkdocs-github-admonitions-plugin==0.1.1", - "mkdocs-literate-nav==0.6.2", + "mkdocs-literate-nav==0.6.3", "mkdocs-llmstxt==0.5.0", "mkdocs-material[recommended,git,imaging]==9.7.5", "mkdocs-section-index==0.3.10", From 9fd1c8f4bcead19a6b4ee019ec1a495e57588a76 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:11:49 +1100 Subject: [PATCH 1272/1376] chore(deps): update pypa/cibuildwheel action to v3.4.0 (#1496) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 02c34b75f..bbea6c50b 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 + uses: pypa/cibuildwheel@ee02a1537ce3071a004a6b08c41e72f0fdc42d9a # v3.4.0 with: package-dir: pact-python-cli env: diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 9ab3c092c..81fa3b133 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 + uses: pypa/cibuildwheel@ee02a1537ce3071a004a6b08c41e72f0fdc42d9a # v3.4.0 with: package-dir: pact-python-ffi env: From d0f8b90f6d1888f650b9ded63d1034c11e88436e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:12:06 +1100 Subject: [PATCH 1273/1376] chore(deps): update astral-sh/setup-uv action to v7.6.0 (#1492) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index bbea6c50b..41cdcdf24 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 81fa3b133..bd0991ecb 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e1f43046..0d791ab06 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a942b8afe..cf66363fb 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 05cfd5644..3691ece03 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true @@ -150,7 +150,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true @@ -194,7 +194,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true @@ -226,7 +226,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true @@ -259,7 +259,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true @@ -299,7 +299,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: enable-cache: true cache-suffix: prek From e6247cb7bf8d9f92672f5bbeedcd39c30a95d0ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:12:14 +1100 Subject: [PATCH 1274/1376] chore(deps): update dependency protobuf to v7 (#1488) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9d2aab782..b2f565bce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,7 +101,7 @@ example = [ "fastapi~=0.0", "flask[async]~=3.0", "grpcio~=1.0", - "protobuf~=6.0", + "protobuf~=7.34", "pydantic~=2.0", "pytest-cov~=7.0", "pytest-rerunfailures~=16.0", From 7ee216665c30018ab3480612a56eb01624dbae34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:37:29 +0000 Subject: [PATCH 1275/1376] chore(deps): update dependency mkdocs-section-index to v0.3.11 (#1500) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b2f565bce..20a4adeb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ docs = [ "mkdocs-literate-nav==0.6.3", "mkdocs-llmstxt==0.5.0", "mkdocs-material[recommended,git,imaging]==9.7.5", - "mkdocs-section-index==0.3.10", + "mkdocs-section-index==0.3.11", "mkdocs==1.6.1", "mkdocstrings[python]==1.0.3", "pathspec==1.0.4", From 19ea532c31e22203bb3b5ee8233983d8a8576357 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 09:00:04 +1100 Subject: [PATCH 1276/1376] chore(deps): update actions/cache action to v5.0.4 (#1502) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3691ece03..606e11ca2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -285,7 +285,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Cache prek - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: |- ~/.cache/prek From eceab7824b8e81ef162a3d2d71afe2e2e4dfffa1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 09:01:02 +1100 Subject: [PATCH 1277/1376] chore(deps): update codecov/codecov-action action to v5.5.3 (#1503) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 606e11ca2..84abcb341 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -111,7 +111,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests @@ -171,7 +171,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 with: token: ${{ secrets.CODECOV_TOKEN }} flags: examples From 74298102894704d64cf07febbf1925dc75755879 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:47:59 +1100 Subject: [PATCH 1278/1376] chore(deps): update ruff to v0.15.7 (#1506) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7217b34da..355642b7e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.6 + rev: v0.15.7 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 485016b84..13ef37b75 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.15.6", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.7", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index bfd14df1b..9d4ab7891 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.15.6", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.7", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 20a4adeb0..dd4232ae5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.15.6", + "ruff==0.15.7", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From a2b428bb8cc1e733f883f5e86c329a5a609e87ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:48:17 +1100 Subject: [PATCH 1279/1376] chore(deps): update dependency mkdocs-material to v9.7.6 (#1505) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dd4232ae5..d0c7355fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,7 @@ docs = [ "mkdocs-github-admonitions-plugin==0.1.1", "mkdocs-literate-nav==0.6.3", "mkdocs-llmstxt==0.5.0", - "mkdocs-material[recommended,git,imaging]==9.7.5", + "mkdocs-material[recommended,git,imaging]==9.7.6", "mkdocs-section-index==0.3.11", "mkdocs==1.6.1", "mkdocstrings[python]==1.0.3", From a69ddc4337fc0a7fab0e57008b76851e83bcec0a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:49:34 +1100 Subject: [PATCH 1280/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.4.8 (#1504) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 355642b7e..3c38de0a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.4.7 + rev: v2.4.8 hooks: - id: biome-check From b5968e8ceaddf3f21837b11319ed9684f8892dda Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:37:47 +1100 Subject: [PATCH 1281/1376] chore(deps): update pre-commit hook davidanson/markdownlint-cli2 to v0.22.0 (#1509) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3c38de0a7..860a6506a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,7 +61,7 @@ repos: - id: committed - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.21.0 + rev: v0.22.0 hooks: - id: markdownlint-cli2 From b4383863e6030731ffe55b818002bfc9ba0472b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:38:01 +1100 Subject: [PATCH 1282/1376] chore(deps): update taiki-e/install-action action to v2.69.6 (#1510) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 41cdcdf24..70e82ac4e 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@94a7388bec5d4c8dd93e3ebf09e0ff448f3f6f4d # v2.68.35 + uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2.69.6 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index bd0991ecb..95771164d 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@94a7388bec5d4c8dd93e3ebf09e0ff448f3f6f4d # v2.68.35 + uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2.69.6 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d791ab06..e16e247f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@94a7388bec5d4c8dd93e3ebf09e0ff448f3f6f4d # v2.68.35 + uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2.69.6 with: tool: git-cliff,typos From 2275f7152fcab1f95f0119a5596cf55d06856544 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 04:42:12 +0000 Subject: [PATCH 1283/1376] chore(deps): update python:3.14-slim docker digest to fb83750 (#1508) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index db63bf3d8..9c34fe7ed 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:584e89d31009a79ae4d9e3ab2fba078524a6c0921cb2711d05e8bb5f628fc9b9 +FROM python:3.14-slim@sha256:fb83750094b46fd6b8adaa80f66e2302ecbe45d513f6cece637a841e1025b4ca ARG USERNAME=vscode ARG USER_UID=1000 From 9c9c945d664da32138c65ef5545ff9a1c03b5f65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 19:41:27 +1100 Subject: [PATCH 1284/1376] chore(deps): update actions/deploy-pages action to v5 (#1511) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cf66363fb..eae390912 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -67,4 +67,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 From b77899f52be3044b41bcbe7989ae6e2016d23aa3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 19:41:43 +1100 Subject: [PATCH 1285/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.4.9 (#1512) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 860a6506a..5f8bcdadb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.4.8 + rev: v2.4.9 hooks: - id: biome-check From ce001202489e03b57408f11d1ae32c9edb45ec09 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 19:44:54 +1100 Subject: [PATCH 1286/1376] chore(deps): update codecov/codecov-action action to v6 (#1515) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84abcb341..b9d3e1d97 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -111,7 +111,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests @@ -171,7 +171,7 @@ jobs: - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} flags: examples From dec4be8c500cc81d7000babd39d39bb70be02b96 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 19:45:34 +1100 Subject: [PATCH 1287/1376] chore(deps): update ruff to v0.15.8 (#1516) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5f8bcdadb..a73b3f85c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.7 + rev: v0.15.8 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 13ef37b75..0f29d03bc 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.15.7", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.8", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 9d4ab7891..3b325c25d 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.15.7", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.8", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.19.1", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index d0c7355fd..f747331a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.15.7", + "ruff==0.15.8", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From ccd5d63ff1d992efaf9aa421e607d04a16479e41 Mon Sep 17 00:00:00 2001 From: Benjamin Aduo <56738123+benaduo@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:52:12 +0100 Subject: [PATCH 1288/1376] docs(examples): add http+xml example Co-authored-by: JP-Ellis Co-authored-by: benaduo --- examples/http/README.md | 1 + examples/http/aiohttp_and_flask/conftest.py | 5 +- .../http/requests_and_fastapi/conftest.py | 5 +- examples/http/xml_example/__init__.py | 1 + examples/http/xml_example/conftest.py | 38 +++++ examples/http/xml_example/consumer.py | 110 +++++++++++++ examples/http/xml_example/provider.py | 103 +++++++++++++ examples/http/xml_example/pyproject.toml | 27 ++++ examples/http/xml_example/test_consumer.py | 93 +++++++++++ examples/http/xml_example/test_provider.py | 144 ++++++++++++++++++ 10 files changed, 525 insertions(+), 2 deletions(-) create mode 100644 examples/http/xml_example/__init__.py create mode 100644 examples/http/xml_example/conftest.py create mode 100644 examples/http/xml_example/consumer.py create mode 100644 examples/http/xml_example/provider.py create mode 100644 examples/http/xml_example/pyproject.toml create mode 100644 examples/http/xml_example/test_consumer.py create mode 100644 examples/http/xml_example/test_provider.py diff --git a/examples/http/README.md b/examples/http/README.md index 4f97723ca..1579c6312 100644 --- a/examples/http/README.md +++ b/examples/http/README.md @@ -6,3 +6,4 @@ This directory contains examples of HTTP-based contract testing with Pact. - [`aiohttp_and_flask/`](aiohttp_and_flask/) - Async aiohttp consumer with Flask provider - [`requests_and_fastapi/`](requests_and_fastapi/) - requests consumer with FastAPI provider +- [`xml_example/`](xml_example/) - requests consumer with FastAPI provider using XML bodies diff --git a/examples/http/aiohttp_and_flask/conftest.py b/examples/http/aiohttp_and_flask/conftest.py index 3ced5de4d..d8c4a164c 100644 --- a/examples/http/aiohttp_and_flask/conftest.py +++ b/examples/http/aiohttp_and_flask/conftest.py @@ -12,6 +12,7 @@ from __future__ import annotations +import contextlib from pathlib import Path import pytest @@ -32,4 +33,6 @@ def _setup_pact_logging() -> None: """ Set up logging for the pact package. """ - pact_ffi.log_to_stderr("INFO") + # If the logger is already configured, this will raise a RuntimeError. + with contextlib.suppress(RuntimeError): + pact_ffi.log_to_stderr("INFO") diff --git a/examples/http/requests_and_fastapi/conftest.py b/examples/http/requests_and_fastapi/conftest.py index 3ced5de4d..d8c4a164c 100644 --- a/examples/http/requests_and_fastapi/conftest.py +++ b/examples/http/requests_and_fastapi/conftest.py @@ -12,6 +12,7 @@ from __future__ import annotations +import contextlib from pathlib import Path import pytest @@ -32,4 +33,6 @@ def _setup_pact_logging() -> None: """ Set up logging for the pact package. """ - pact_ffi.log_to_stderr("INFO") + # If the logger is already configured, this will raise a RuntimeError. + with contextlib.suppress(RuntimeError): + pact_ffi.log_to_stderr("INFO") diff --git a/examples/http/xml_example/__init__.py b/examples/http/xml_example/__init__.py new file mode 100644 index 000000000..6e031999e --- /dev/null +++ b/examples/http/xml_example/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/examples/http/xml_example/conftest.py b/examples/http/xml_example/conftest.py new file mode 100644 index 000000000..d8c4a164c --- /dev/null +++ b/examples/http/xml_example/conftest.py @@ -0,0 +1,38 @@ +""" +Shared PyTest configuration. + +In order to run the examples, we need to run the Pact broker. In order to avoid +having to run the Pact broker manually, or repeating the same code in each +example, we define a PyTest fixture to run the Pact broker. + +We also define a `pact_dir` fixture to define the directory where the generated +Pact files will be stored. You are encouraged to have a look at these files +after the examples have been run. +""" + +from __future__ import annotations + +import contextlib +from pathlib import Path + +import pytest + +import pact_ffi + +EXAMPLE_DIR = Path(__file__).parent.resolve() + + +@pytest.fixture(scope="session") +def pacts_path() -> Path: + """Fixture for the Pact directory.""" + return EXAMPLE_DIR / "pacts" + + +@pytest.fixture(scope="session", autouse=True) +def _setup_pact_logging() -> None: + """ + Set up logging for the pact package. + """ + # If the logger is already configured, this will raise a RuntimeError. + with contextlib.suppress(RuntimeError): + pact_ffi.log_to_stderr("INFO") diff --git a/examples/http/xml_example/consumer.py b/examples/http/xml_example/consumer.py new file mode 100644 index 000000000..798c6c003 --- /dev/null +++ b/examples/http/xml_example/consumer.py @@ -0,0 +1,110 @@ +""" +Requests XML consumer example. + +This module defines a simple +[consumer](https://docs.pact.io/getting_started/terminology#service-consumer) +using the synchronous [`requests`][requests] library which will be tested with +Pact in the [consumer test][examples.http.xml_example.test_consumer]. + +The consumer sends requests expecting XML responses and parses them using the +standard library [`xml.etree.ElementTree`][xml.etree.ElementTree] module. + +Note that the code in this module is agnostic of Pact (i.e., this would be your +production code). The `pact-python` dependency only appears in the tests. +""" + +from __future__ import annotations + +import logging +import xml.etree.ElementTree as ET +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import requests + +if TYPE_CHECKING: + from types import TracebackType + + from typing_extensions import Self + +logger = logging.getLogger(__name__) + + +@dataclass() +class User: + """ + Represents a user as seen by the consumer. + """ + + id: int + name: str + + +class UserClient: + """ + HTTP client for interacting with a user provider service via XML. + """ + + def __init__(self, hostname: str) -> None: + """ + Initialise the user client. + + Args: + hostname: + The base URL of the provider (must include scheme, e.g., + `http://`). + + Raises: + ValueError: + If the hostname does not start with 'http://' or `https://`. + """ + if not hostname.startswith(("http://", "https://")): + msg = "Invalid base URI" + raise ValueError(msg) + self._hostname = hostname + self._session = requests.Session() + + def __enter__(self) -> Self: + """ + Begin the context for the client. + """ + self._session.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """ + Exit the context for the client. + """ + self._session.__exit__(exc_type, exc_val, exc_tb) + + def get_user(self, user_id: int) -> User: + """ + Fetch a user by ID from the provider, expecting an XML response. + + Args: + user_id: + The ID of the user to fetch. + + Returns: + A `User` instance parsed from the XML response. + + Raises: + requests.HTTPError: + If the server returns a non-2xx response. + """ + logger.debug("Fetching user %s", user_id) + response = self._session.get( + f"{self._hostname}/users/{user_id}", + headers={"Accept": "application/xml"}, + ) + response.raise_for_status() + root = ET.fromstring(response.text) # noqa: S314 + return User( + id=int(root.findtext("id") or 0), + name=root.findtext("name") or "", + ) diff --git a/examples/http/xml_example/provider.py b/examples/http/xml_example/provider.py new file mode 100644 index 000000000..1a051874f --- /dev/null +++ b/examples/http/xml_example/provider.py @@ -0,0 +1,103 @@ +""" +FastAPI XML provider example. + +This module defines a simple +[provider](https://docs.pact.io/getting_started/terminology#service-provider) +implemented with [`fastapi`](https://fastapi.tiangolo.com/) which will be tested +with Pact in the [provider test][examples.http.xml_example.test_provider]. + +The provider receives requests from the consumer and returns XML responses built +using the standard library [`xml.etree.ElementTree`][xml.etree.ElementTree] +module. + +Note that the code in this module is agnostic of Pact (i.e., this would be your +production code). The `pact-python` dependency only appears in the tests. +""" + +from __future__ import annotations + +import logging +import xml.etree.ElementTree as ET +from dataclasses import dataclass +from typing import ClassVar + +from fastapi import FastAPI, HTTPException, status +from fastapi.responses import Response + +logger = logging.getLogger(__name__) + + +@dataclass() +class User: + """ + Represents a user in the provider system. + """ + + id: int + name: str + + +class UserDb: + """ + A simple in-memory user database abstraction for demonstration purposes. + """ + + _db: ClassVar[dict[int, User]] = {} + + @classmethod + def create(cls, user: User) -> None: + """ + Add a new user to the database. + """ + cls._db[user.id] = user + + @classmethod + def delete(cls, user_id: int) -> None: + """ + Delete a user from the database by their ID. + + Raises: + KeyError: If the user does not exist. + """ + if user_id not in cls._db: + msg = f"User {user_id} does not exist." + raise KeyError(msg) + del cls._db[user_id] + + @classmethod + def get(cls, user_id: int) -> User | None: + """ + Retrieve a user by their ID. + """ + return cls._db.get(user_id) + + +app = FastAPI() + + +@app.get("/users/{uid}") +async def get_user_by_id(uid: int) -> Response: + """ + Retrieve a user by their ID, returning an XML response. + + Args: + uid: + The user ID to retrieve. + + Raises: + HTTPException: If the user is not found, a 404 error is raised. + """ + logger.debug("GET /users/%s", uid) + user = UserDb.get(uid) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found", + ) + root = ET.Element("user") + ET.SubElement(root, "id").text = str(user.id) + ET.SubElement(root, "name").text = user.name + return Response( + content=ET.tostring(root, encoding="unicode"), + media_type="application/xml", + ) diff --git a/examples/http/xml_example/pyproject.toml b/examples/http/xml_example/pyproject.toml new file mode 100644 index 000000000..3275a020e --- /dev/null +++ b/examples/http/xml_example/pyproject.toml @@ -0,0 +1,27 @@ +#:schema https://www.schemastore.org/pyproject.json +[project] +name = "example-xml" + +description = "Example of XML contract testing with Pact Python" + +dependencies = ["requests~=2.0", "fastapi~=0.0", "typing-extensions~=4.0"] +requires-python = ">=3.10" +version = "1.0.0" + +[dependency-groups] +test = ["pact-python", "pytest~=9.0", "uvicorn~=0.29"] + +[tool.uv.sources] +pact-python = { path = "../../../" } + +[tool.ruff] +extend = "../../../pyproject.toml" + +[tool.pytest] +addopts = ["--import-mode=importlib"] + +asyncio_default_fixture_loop_scope = "session" + +log_date_format = "%H:%M:%S" +log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" +log_level = "NOTSET" diff --git a/examples/http/xml_example/test_consumer.py b/examples/http/xml_example/test_consumer.py new file mode 100644 index 000000000..a7f758046 --- /dev/null +++ b/examples/http/xml_example/test_consumer.py @@ -0,0 +1,93 @@ +""" +Consumer contract tests using Pact (XML). + +This module demonstrates how to test a consumer (see +[`consumer.py`][examples.http.xml_example.consumer]) against a mock provider +using Pact. The key difference from JSON-based examples is that the response +body is specified as a plain XML string — no matchers are used, as XML matchers +do not exist in pact-python. The `Accept` header is set via a separate +`.with_header()` call after `.with_request()`. +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +import pytest +import requests + +from examples.http.xml_example.consumer import UserClient +from pact import Pact + +if TYPE_CHECKING: + from collections.abc import Generator + from pathlib import Path + +logger = logging.getLogger(__name__) + + +@pytest.fixture +def pact(pacts_path: Path) -> Generator[Pact, None, None]: + """ + Set up a Pact mock provider for consumer tests. + + Args: + pacts_path: + The path where the generated pact file will be written. + + Yields: + A Pact object for use in tests. + """ + pact = Pact("xml-consumer", "xml-provider").with_specification("V4") + yield pact + pact.write_file(pacts_path) + + +def test_get_user(pact: Pact) -> None: + """ + Test the GET request for a user, expecting an XML response. + + The response body is a plain XML string. Note that `.with_header()` is + called as a separate chain step — `with_request()` does not accept a + headers argument. + """ + response = "123Alice" + ( + pact + .upon_receiving("A request for a user as XML") + .given("the user exists", id=123, name="Alice") + .with_request("GET", "/users/123") + .with_header("Accept", "application/xml") + .will_respond_with(200) + .with_body(response, content_type="application/xml") + ) + + with ( + pact.serve() as srv, + UserClient(str(srv.url)) as client, + ): + user = client.get_user(123) + assert user.id == 123 + assert user.name == "Alice" + + +def test_get_unknown_user(pact: Pact) -> None: + """ + Test the GET request for an unknown user, expecting a 404 response. + """ + ( + pact + .upon_receiving("A request for an unknown user as XML") + .given("the user doesn't exist", id=123) + .with_request("GET", "/users/123") + .with_header("Accept", "application/xml") + .will_respond_with(404) + ) + + with ( + pact.serve() as srv, + UserClient(str(srv.url)) as client, + pytest.raises(requests.HTTPError), + ): + client.get_user(123) diff --git a/examples/http/xml_example/test_provider.py b/examples/http/xml_example/test_provider.py new file mode 100644 index 000000000..1e988acad --- /dev/null +++ b/examples/http/xml_example/test_provider.py @@ -0,0 +1,144 @@ +""" +Provider contract tests using Pact (XML). + +This module demonstrates how to test a FastAPI provider (see +[`provider.py`][examples.http.xml_example.provider]) against a mock consumer +using Pact. The mock consumer replays the requests defined by the consumer +contract, and Pact validates that the provider responds as expected. + +Provider state handlers set up the in-memory database before each interaction +is verified, ensuring repeatable and isolated contract verification. +""" + +from __future__ import annotations + +import contextlib +import logging +import socket +import time +from threading import Thread +from typing import TYPE_CHECKING, Any, Literal + +import pytest +import uvicorn + +import pact._util +from examples.http.xml_example.provider import User, UserDb, app +from pact import Verifier + +if TYPE_CHECKING: + from pathlib import Path + +logger = logging.getLogger(__name__) + + +@pytest.fixture(scope="session") +def app_server() -> str: + """ + Run the FastAPI server for provider verification. + + Returns: + The base URL of the running FastAPI server. + """ + hostname = "localhost" + port = pact._util.find_free_port() # noqa: SLF001 + Thread( + target=uvicorn.run, + args=(app,), + kwargs={"host": hostname, "port": port}, + daemon=True, + ).start() + for _ in range(50): + with ( + contextlib.suppress(ConnectionRefusedError, OSError), + socket.create_connection((hostname, port), timeout=0.1), + ): + break + time.sleep(0.1) + return f"http://{hostname}:{port}" + + +def test_provider(app_server: str, pacts_path: Path) -> None: + """ + Test the provider against the consumer contract. + + Runs the Pact verifier against the FastAPI provider using the contract + generated by the consumer tests. State handlers ensure the database is + in the correct state for each interaction. + """ + verifier = ( + Verifier("xml-provider") + .add_source(pacts_path) + .add_transport(url=app_server) + .state_handler( + { + "the user exists": mock_user_exists, + "the user doesn't exist": mock_user_does_not_exist, + }, + teardown=True, + ) + ) + + verifier.verify() + + +def mock_user_exists( + action: Literal["setup", "teardown"], + parameters: dict[str, Any], +) -> None: + """ + Mock the provider state where a user exists. + + Args: + action: + Either "setup" or "teardown". + parameters: + Must contain "id" and optionally "name". + """ + user = User( + id=int(parameters.get("id", 1)), + name=str(parameters.get("name", "Alice")), + ) + + if action == "setup": + UserDb.create(user) + return + + if action == "teardown": + with contextlib.suppress(KeyError): + UserDb.delete(user.id) + return + + msg = f"Unknown action: {action}" + raise ValueError(msg) + + +def mock_user_does_not_exist( + action: Literal["setup", "teardown"], + parameters: dict[str, Any], +) -> None: + """ + Mock the provider state where a user does not exist. + + Args: + action: + Either "setup" or "teardown". + parameters: + Must contain "id". + """ + if "id" not in parameters: + msg = "State must contain an 'id' field to mock user non-existence" + raise ValueError(msg) + + uid = int(parameters["id"]) + + if action == "setup": + if user := UserDb.get(uid): + UserDb.delete(user.id) + return + + if action == "teardown": + return + + msg = f"Unknown action: {action}" + raise ValueError(msg) From cad437100fb3790edaf738bb9f42cfbba31820fc Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 24 Mar 2026 17:39:07 +1100 Subject: [PATCH 1289/1376] feat: add xml matching While the Pact FFI does support matchers within XML bodies, their construction cannot easily be achieved with a standard `dict`. In addition, the way XML data is matched (e.g., within attributes, or the concept of children) needs to be made more explicit. The new `xml` module of Pact Python adds support for building an XML tree, allowing for matchers to be embedded. This is than handled by Pact Python and serialised for consumption by the FFI. Signed-off-by: JP-Ellis --- src/pact/__init__.py | 13 +- src/pact/xml.py | 312 +++++++++++++++++++++++++++++++++++++++++++ tests/test_xml.py | 222 ++++++++++++++++++++++++++++++ 3 files changed, 546 insertions(+), 1 deletion(-) create mode 100644 src/pact/xml.py create mode 100644 tests/test_xml.py diff --git a/src/pact/__init__.py b/src/pact/__init__.py index fe11fd1b5..a84f9ab38 100644 --- a/src/pact/__init__.py +++ b/src/pact/__init__.py @@ -26,7 +26,7 @@ For flexible contract definitions, use the matching and generation modules: ```python -from pact import match, generate +from pact import match, generate, xml # Import modules, not individual functions # Use functions via module namespace to avoid shadowing built-ins @@ -37,6 +37,15 @@ # Generators work similarly response_id = generate.uuid() score = generate.float(precision=2) + +# XML bodies use the xml module +response = xml.body( + xml.element( + "user", + xml.element("id", match.int(123)), + xml.element("name", match.str("Alice")), + ) +) ``` The functions within these modules are designed to align with a number of @@ -102,6 +111,7 @@ from __future__ import annotations +from pact import xml as xml from pact.__version__ import __version__, __version_tuple__ from pact.pact import Pact from pact.verifier import Verifier @@ -115,4 +125,5 @@ "Verifier", "__version__", "__version_tuple__", + "xml", ] diff --git a/src/pact/xml.py b/src/pact/xml.py new file mode 100644 index 000000000..27e114ba6 --- /dev/null +++ b/src/pact/xml.py @@ -0,0 +1,312 @@ +""" +XML body builder for Pact contract testing. + +This module provides builder functions for constructing XML request and response +bodies with embedded [Pact matchers][match]. It abstracts the FFI's +internal XML description format, so test authors only need to describe the XML +structure and attach matchers where desired. + +```python +from pact import match, xml + +response = xml.body( + xml.element( + "user", + xml.element("id", match.int(123)), + xml.element("name", match.str("Alice")), + ) +) +pact.with_body(response, content_type="application/xml") +``` + +The returned dict is passed to +[`with_body()`][interaction._base.Interaction.with_body] with +`content_type="application/xml"`. The Pact FFI detects the JSON-formatted body +and extracts matching rules automatically. + +Individual functions can also be imported directly: + +```python +from pact.xml import body, element + +response = body(element("user", element("id", match.int(123)))) +``` +""" + +from __future__ import annotations + +from typing import Any, TypeAlias, Union + +from pact.match.matcher import AbstractMatcher + +XmlContent: TypeAlias = Union[ + "XmlElement", "AbstractMatcher[Any]", str, int, float, bool +] +""" +Valid types for element children. + +These can be child elements, matchers (text with rules), or literal primitive +values (text without rules). +""" + + +class XmlElement: + """ + Represents an XML element for use in Pact body descriptions. + + Do not instantiate directly, use [`element()`][element] instead. + """ + + def __init__( + self, + tag: str, + *children: XmlContent, + attrs: dict[str, Any] | None = None, + ) -> None: + """ + Initialise an XML element. + + Args: + tag: + Element tag name, optionally including a namespace prefix + (e.g. `"ns1:project"`). + + *children: + Child elements or text content. Each positional argument is one + of: + + - [`XmlElement`][XmlElement]: a nested child element. + - [`AbstractMatcher`][match.matcher.AbstractMatcher]: text + content with a matching rule. The matcher's example value is + used as the generated text. + - `str`, `int`, `float`, or `bool`: literal text content with + no matching rule. + + attrs: + Element attributes as a mapping of attribute name to value. + Values may be plain primitives or + [`AbstractMatcher`][match.matcher.AbstractMatcher] + instances. Use this parameter for namespace declarations (e.g. + `{"xmlns:ns1": "http://..."}`) and any attribute whose name is + not a valid Python identifier. + """ + self._tag = tag + self._children = children + self._attrs: dict[str, Any] = dict(attrs) if attrs is not None else {} + self._repeated = False + self._min: int | None = None + self._max: int | None = None + self._examples: int = 1 + + def each( + self, + *, + min: int = 1, # noqa: A002 + max: int | None = None, # noqa: A002 + examples: int | None = None, + ) -> XmlElement: + """ + Mark this element as a type-matched repeating element. + + Causes a `type` matching rule to be registered for this element in the + generated pact file, meaning the provider must return at least `min` + instances of this element structure. Use `max` to also enforce an upper + bound. + + This method is typically used when the element appears as a child + inside another element's children list, but it can also be applied to + the root element passed to [`body()`][body]. + + Args: + min: + Minimum number of matching elements required in the provider + response. Defaults to `1`. + + max: + Maximum number of matching elements (optional). + + examples: + Number of example copies of this element generated in the pact + body. Defaults to `min`. + + Returns: + `self`, to allow chaining directly after the constructor call. + + Example: + ```python + xml.element( + "item", + xml.element("id", match.int(1)), + ).each(min=1, examples=3) + ``` + """ + if min < 1: + msg = f"min must be at least 1, got {min!r}." + raise ValueError(msg) + + if max is not None and max < min: + msg = ( + "max must be greater than or equal to min; " + f"got min={min!r}, max={max!r}." + ) + raise ValueError(msg) + + effective_examples = examples if examples is not None else min + + if effective_examples < min: + msg = ( + "examples must be greater than or equal to min; " + f"got min={min!r}, examples={effective_examples!r}." + ) + raise ValueError(msg) + + if max is not None and effective_examples > max: + msg = ( + "examples must be less than or equal to max when max is set; " + f"got max={max!r}, examples={effective_examples!r}." + ) + raise ValueError(msg) + + self._repeated = True + self._min = min + self._max = max + self._examples = effective_examples + return self + + def to_serializable_dict(self) -> dict[str, Any]: + """ + Convert this element to a serialisable dict. + + The returned dict is suitable for + [`IntegrationJSONEncoder`][match.matcher.IntegrationJSONEncoder]. + [`AbstractMatcher`][match.matcher.AbstractMatcher] objects within + it are left as Python objects so that `IntegrationJSONEncoder` + serialises them to their integration-JSON form when + [`with_body()`][interaction._base.Interaction.with_body] + calls `json.dumps`. + + If [`.each()`][XmlElement.each] was called on this element, + the returned dict is wrapped in the FFI type-matcher envelope + (`{"pact:matcher:type": "type", "value": ..., "examples": N}`). + + Returns: + A dict representing this element in the FFI XML description format. + """ + elem_dict = self._to_element_dict() + if self._repeated: + wrapper: dict[str, Any] = { + "pact:matcher:type": "type", + "value": elem_dict, + "examples": self._examples, + } + if self._min is not None: + wrapper["min"] = self._min + if self._max is not None: + wrapper["max"] = self._max + return wrapper + return elem_dict + + def _to_element_dict(self) -> dict[str, Any]: + """Return the raw element dict without any repetition wrapper.""" + children_list: list[dict[str, Any]] = [] + for child in self._children: + if isinstance(child, XmlElement): + children_list.append(child.to_serializable_dict()) + elif isinstance(child, AbstractMatcher): + entry: dict[str, Any] = {"matcher": child} + if child.has_value(): + entry["content"] = child.value # type: ignore[attr-defined] + children_list.append(entry) + else: + # Literal primitive, plain text content, no matching rule. + children_list.append({"content": child}) + + return { + "name": self._tag, + "children": children_list, + "attributes": self._attrs, + } + + +def element( + tag: str, + *children: XmlContent, + attrs: dict[str, Any] | None = None, +) -> XmlElement: + """ + Create an XML element for use in a Pact body description. + + Args: + tag: + Element tag name, optionally including a namespace prefix + (e.g. `"ns1:project"`). + + *children: + Child elements or text content. Each positional argument is one + of: + + - [`XmlElement`][XmlElement]: a nested child element. + - [`AbstractMatcher`][match.matcher.AbstractMatcher]: text + content with a matching rule. The example value is taken from the + matcher. + - `str`, `int`, `float`, or `bool`: literal text content with no + matching rule. + + attrs: + Element attributes as a mapping of attribute name to value. Values + may be [`AbstractMatcher`][match.matcher.AbstractMatcher] + instances or plain primitives. Required for namespace declarations + (e.g. `{"xmlns:ns1": "http://..."}`) and attribute names that are + not valid Python identifiers. + + Returns: + An [`XmlElement`][XmlElement] instance. + + Example: + ```python + from pact import match, xml + + xml.element( + "user", + xml.element("id", match.int(123)), + xml.element("name", match.str("Alice")), + attrs={"xmlns:ns1": "http://some.namespace/"}, + ) + ``` + """ + return XmlElement(tag, *children, attrs=attrs) + + +def body(root: XmlElement) -> dict[str, Any]: + """ + Wrap a root [`XmlElement`][XmlElement] as an XML body description. + + The returned dict is suitable for passing to + [`with_body()`][interaction._base.Interaction.with_body] + with `content_type="application/xml"`. The Pact FFI detects the + JSON-formatted body, generates the example XML, and extracts matching rules + into the contract file. + + Args: + root: + The root element of the XML body. + + Returns: + A dict in the FFI XML description format, ready for + `with_body(result, content_type="application/xml")`. + + Example: + ```python + from pact import match, xml + + response = xml.body( + xml.element( + "user", + xml.element("id", match.int(123)), + xml.element("name", match.str("Alice")), + ) + ) + pact.with_body(response, content_type="application/xml") + ``` + """ + return {"root": root.to_serializable_dict()} diff --git a/tests/test_xml.py b/tests/test_xml.py new file mode 100644 index 000000000..e16f1d293 --- /dev/null +++ b/tests/test_xml.py @@ -0,0 +1,222 @@ +""" +Unit tests for the :mod:`pact.xml` XML body builder. +""" + +from __future__ import annotations + +import json + +import pytest + +from pact import match, xml +from pact.match.matcher import AbstractMatcher, IntegrationJSONEncoder +from pact.xml import XmlElement, body, element + + +class TestElement: + """Tests for :func:`pact.xml.element`.""" + + def test_returns_xml_element(self) -> None: + result = element("id", 1) + assert isinstance(result, XmlElement) + + def test_literal_text_content(self) -> None: + result = body(element("id", 123)) + assert result == { + "root": { + "name": "id", + "children": [{"content": 123}], + "attributes": {}, + } + } + + def test_literal_string_content(self) -> None: + result = body(element("name", "Alice")) + assert result == { + "root": { + "name": "name", + "children": [{"content": "Alice"}], + "attributes": {}, + } + } + + def test_matcher_text_content_includes_content_and_matcher(self) -> None: + m = match.int(123) + result = body(element("id", m)) + children = result["root"]["children"] + assert len(children) == 1 + assert children[0]["content"] == 123 + assert children[0]["matcher"] is m + + def test_matcher_text_content_is_abstract_matcher(self) -> None: + result = body(element("id", match.int(123))) + matcher = result["root"]["children"][0]["matcher"] + assert isinstance(matcher, AbstractMatcher) + + def test_matcher_without_value_omits_content(self) -> None: + # match.int() with no value uses a generator + m = match.int() + result = body(element("id", m)) + children = result["root"]["children"] + assert "content" not in children[0] + assert children[0]["matcher"] is m + + def test_container_element_with_children(self) -> None: + result = body( + element( + "user", + element("id", 123), + element("name", "Alice"), + ) + ) + root = result["root"] + assert root["name"] == "user" + assert root["children"][0]["name"] == "id" + assert root["children"][1]["name"] == "name" + assert root["attributes"] == {} + + def test_empty_element(self) -> None: + result = body(element("empty")) + assert result == { + "root": { + "name": "empty", + "children": [], + "attributes": {}, + } + } + + def test_attrs_plain_values(self) -> None: + result = body(element("user", attrs={"id": "1", "version": "2"})) + assert result["root"]["attributes"] == {"id": "1", "version": "2"} + + def test_attrs_with_matcher(self) -> None: + m = match.int(1) + result = body(element("user", attrs={"id": m})) + assert result["root"]["attributes"]["id"] is m + + def test_attrs_none_gives_empty_dict(self) -> None: + result = body(element("tag")) + assert result["root"]["attributes"] == {} + + def test_namespace_declaration_in_attrs(self) -> None: + result = body( + element("ns1:projects", attrs={"xmlns:ns1": "http://example.com/"}) + ) + assert result["root"]["attributes"]["xmlns:ns1"] == "http://example.com/" + + +class TestEach: + """Tests for :meth:`XmlElement.each`.""" + + def test_each_returns_self(self) -> None: + elem = element("item", 1) + result = elem.each(min=1) + assert result is elem + + def test_each_wraps_element_in_type_matcher(self) -> None: + elem = element("item", element("id", 1)).each(min=1) + result = body(elem) + root = result["root"] + assert root["pact:matcher:type"] == "type" + assert root["min"] == 1 + assert root["value"]["name"] == "item" + + def test_each_default_examples_equals_min(self) -> None: + elem = element("item").each(min=3) + result = body(elem) + assert result["root"]["examples"] == 3 + + def test_each_explicit_examples(self) -> None: + elem = element("item").each(min=2, examples=5) + result = body(elem) + assert result["root"]["examples"] == 5 + assert result["root"]["min"] == 2 + + def test_each_with_max(self) -> None: + elem = element("item").each(min=1, max=10) + result = body(elem) + assert result["root"]["min"] == 1 + assert result["root"]["max"] == 10 + + def test_each_without_max_omits_max_key(self) -> None: + elem = element("item").each(min=1) + result = body(elem) + assert "max" not in result["root"] + + def test_each_raises_when_min_less_than_1(self) -> None: + with pytest.raises(ValueError, match="min must be at least 1"): + element("item").each(min=0) + + def test_each_raises_when_max_less_than_min(self) -> None: + with pytest.raises( + ValueError, match="max must be greater than or equal to min" + ): + element("item").each(min=3, max=2) + + def test_each_raises_when_examples_less_than_min(self) -> None: + with pytest.raises( + ValueError, match="examples must be greater than or equal to min" + ): + element("item").each(min=3, examples=2) + + def test_each_raises_when_examples_exceed_max(self) -> None: + with pytest.raises( + ValueError, match="examples must be less than or equal to max" + ): + element("item").each(min=1, max=3, examples=5) + + def test_each_as_child_wraps_in_type_matcher(self) -> None: + result = body( + element( + "items", + element("item", element("id", 1)).each(min=2), + ) + ) + child = result["root"]["children"][0] + assert child["pact:matcher:type"] == "type" + assert child["min"] == 2 + assert child["value"]["name"] == "item" + + +class TestBody: + """Tests for :func:`pact.xml.body`.""" + + def test_wraps_in_root_key(self) -> None: + result = body(element("foo")) + assert "root" in result + assert result["root"]["name"] == "foo" + + def test_returns_dict(self) -> None: + result = body(element("foo")) + assert isinstance(result, dict) + + +class TestIntegration: + """Integration: body() output survives json.dumps with IntegrationJSONEncoder.""" + + def test_json_serialises_matchers(self) -> None: + result = body( + element( + "user", + element("id", match.int(123)), + element("name", match.str("Alice")), + ) + ) + serialised = json.dumps(result, cls=IntegrationJSONEncoder) + parsed = json.loads(serialised) + + id_child = parsed["root"]["children"][0] + assert id_child["name"] == "id" + assert id_child["children"][0]["content"] == 123 + assert id_child["children"][0]["matcher"]["pact:matcher:type"] == "integer" + + name_child = parsed["root"]["children"][1] + assert name_child["children"][0]["matcher"]["pact:matcher:type"] == "type" + + def test_module_access_via_pact_xml(self) -> None: + result = xml.body(xml.element("user", xml.element("id", match.int(1)))) + assert result["root"]["name"] == "user" + + def test_direct_import(self) -> None: + result = body(element("user", element("id", 1))) + assert result["root"]["name"] == "user" From a9942ccaa65fd7aaa6076a6ded7eb31c3d1c19f1 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 24 Mar 2026 18:05:30 +1100 Subject: [PATCH 1290/1376] docs: update xml example to use new matcher Building on the awesome work from @benaduo, I am adapting the example to make use of the new `pact.xml` module and matchers. Signed-off-by: JP-Ellis --- examples/http/xml_example/README.md | 143 +++++++++++++++++++++ examples/http/xml_example/conftest.py | 13 +- examples/http/xml_example/consumer.py | 46 ++++++- examples/http/xml_example/provider.py | 47 ++++++- examples/http/xml_example/test_consumer.py | 96 ++++++++++++-- examples/http/xml_example/test_provider.py | 60 +++++++-- 6 files changed, 367 insertions(+), 38 deletions(-) create mode 100644 examples/http/xml_example/README.md diff --git a/examples/http/xml_example/README.md b/examples/http/xml_example/README.md new file mode 100644 index 000000000..894d2c329 --- /dev/null +++ b/examples/http/xml_example/README.md @@ -0,0 +1,143 @@ +# Example: requests Client and FastAPI Provider with XML Contract Testing + +This example demonstrates contract testing between a synchronous +[`requests`](https://docs.python-requests.org/en/latest/)-based client +(consumer) and a [FastAPI](https://fastapi.tiangolo.com/) web server (provider) +where the payload format is XML rather than JSON. It is designed to be +pedagogical, highlighting both the standard Pact contract testing workflow and +the specific constraints that arise when working with XML. + +## Overview + +- [**Consumer**][examples.http.xml_example.consumer]: Synchronous HTTP client + using requests, parsing XML with `xml.etree.ElementTree` +- [**Provider**][examples.http.xml_example.provider]: FastAPI web server + returning XML responses +- [**Consumer Tests**][examples.http.xml_example.test_consumer]: Contract + definition and consumer testing with Pact +- [**Provider Tests**][examples.http.xml_example.test_provider]: Provider + verification against contracts + +Use the above links to view additional documentation within. + +## What This Example Demonstrates + +### Consumer Side + +- Synchronous HTTP client implementation with requests +- Parsing XML responses using the standard library + [`xml.etree.ElementTree`][xml.etree.ElementTree] module +- Consumer contract testing with Pact mock servers +- Setting request headers (e.g., `Accept: application/xml`) using + `.with_header()` as a separate chain step + +### Provider Side + +- FastAPI web server returning `application/xml` responses built with + `xml.etree.ElementTree` +- Provider verification against consumer contracts +- Provider state setup and teardown for isolated, repeatable verification + +### Testing Patterns + +- Independent consumer and provider testing +- Contract-driven development workflow +- Using the [`pact.xml`][pact.xml] builder to apply Pact matchers to XML + bodies +- How matcher-based contracts are more flexible than exact XML string matching + +## XML Matchers + +Unlike JSON, XML bodies cannot be expressed as a plain Python dict of +field-matcher pairs. Instead, use the [`pact.xml`][pact.xml] builder, which +constructs the body description from nested +[`xml.element()`][pact.xml.element] calls: + +```python +from pact import match, xml + +response = xml.body( + xml.element("user", + xml.element("id", match.int(123)), + xml.element("name", match.str("Alice")), + ) +) +``` + +Pass the result to `.with_body()` with `content_type="application/xml"`. The +Pact FFI detects that the body is JSON, generates the example XML, and +registers matching rules for each annotated element. The resulting contract +will match _any_ XML response where `` contains an integer and `` +contains a string and not just the specific example values. + +For attribute matchers, pass matcher objects via the `attrs` keyword argument: + +```python +xml.element("user", attrs={"id": match.int(1), "type": "activity"}) +``` + +For repeating elements, chain [`.each()`][pact.xml.XmlElement.each] to add a +`type` matching rule: + +```python +( + xml.element( + "items", + xml.element("item", xml.element("id", match.int(1))).each(min=1, examples=2), + ) +) +``` + +For JSON-based examples using the same matchers, see +[`requests_and_fastapi/`](../requests_and_fastapi/). + +## Prerequisites + +- Python 3.10 or higher +- A dependency manager ([uv](https://docs.astral.sh/uv/) recommended, + [pip](https://pip.pypa.io/en/stable/) also works) + +## Running the Example + +### Using uv (Recommended) + +We recommend using [uv](https://docs.astral.sh/uv/) to manage the virtual +environment and dependencies. The following command will automatically set up +the virtual environment, install dependencies, and execute the tests: + +```console +uv run --group test pytest +``` + +### Using pip + +If using the [`venv`][venv] module, the required steps are: + +1. Create and activate a virtual environment: + + ```console + python -m venv .venv + source .venv/bin/activate # On macOS/Linux + .venv\Scripts\activate # On Windows + ``` + +2. Install dependencies: + + ```console + pip install -U pip # Pip 25.1 is required + pip install --group test -e . + ``` + +3. Run tests: + + ```console + pytest + ``` + +## Related Documentation + +- [Pact Documentation](https://docs.pact.io/) +- [requests Documentation](https://docs.python-requests.org/) +- [FastAPI Documentation](https://fastapi.tiangolo.com/) +- [pytest Documentation](https://docs.pytest.org/) +- [xml.etree.ElementTree Documentation](https://docs.python.org/3/library/xml.etree.elementtree.html) diff --git a/examples/http/xml_example/conftest.py b/examples/http/xml_example/conftest.py index d8c4a164c..acb15b98f 100644 --- a/examples/http/xml_example/conftest.py +++ b/examples/http/xml_example/conftest.py @@ -1,13 +1,8 @@ """ -Shared PyTest configuration. +Shared pytest configuration for the XML example. -In order to run the examples, we need to run the Pact broker. In order to avoid -having to run the Pact broker manually, or repeating the same code in each -example, we define a PyTest fixture to run the Pact broker. - -We also define a `pact_dir` fixture to define the directory where the generated -Pact files will be stored. You are encouraged to have a look at these files -after the examples have been run. +Defines the `pacts_path` fixture used by both consumer and provider tests to +locate the directory where generated Pact contract files are stored. """ from __future__ import annotations @@ -24,7 +19,7 @@ @pytest.fixture(scope="session") def pacts_path() -> Path: - """Fixture for the Pact directory.""" + """Fixture providing the path to the generated Pact contract files.""" return EXAMPLE_DIR / "pacts" diff --git a/examples/http/xml_example/consumer.py b/examples/http/xml_example/consumer.py index 798c6c003..d4230f34e 100644 --- a/examples/http/xml_example/consumer.py +++ b/examples/http/xml_example/consumer.py @@ -4,13 +4,23 @@ This module defines a simple [consumer](https://docs.pact.io/getting_started/terminology#service-consumer) using the synchronous [`requests`][requests] library which will be tested with -Pact in the [consumer test][examples.http.xml_example.test_consumer]. +Pact in the [consumer test][examples.http.xml_example.test_consumer]. As Pact +is a consumer-driven framework, the consumer defines the interactions which the +provider must then satisfy. The consumer sends requests expecting XML responses and parses them using the standard library [`xml.etree.ElementTree`][xml.etree.ElementTree] module. +This also showcases how Pact tests differ from merely testing adherence to an +OpenAPI specification. The Pact tests are more concerned with the practical use +of the API, rather than the formally defined specification. So you will see +below that the `User` class includes only the fields this consumer actually +needs, `id` and `name`, even though the provider could return additional fields +in its XML payload. + Note that the code in this module is agnostic of Pact (i.e., this would be your -production code). The `pact-python` dependency only appears in the tests. +production code). The `pact-python` dependency only appears in the tests. This +is because the consumer is not concerned with Pact, only the tests are. """ from __future__ import annotations @@ -34,11 +44,31 @@ class User: """ Represents a user as seen by the consumer. + + This class is intentionally minimal, including only the fields the consumer + actually uses. It may differ from the [provider's user + model][examples.http.xml_example.provider.User], which could expose + additional fields. This demonstrates the consumer-driven nature of contract + testing: the consumer defines what it needs, not what the provider exposes. """ id: int name: str + def __post_init__(self) -> None: + """ + Validate the user data after initialisation. + + Raises: + ValueError: If the name is empty or the ID is not a positive integer. + """ + if not self.name: + msg = "User must have a name" + raise ValueError(msg) + if self.id <= 0: + msg = "User ID must be a positive integer" + raise ValueError(msg) + class UserClient: """ @@ -103,8 +133,16 @@ def get_user(self, user_id: int) -> User: headers={"Accept": "application/xml"}, ) response.raise_for_status() + # S314: xml.etree.ElementTree is safe here because the XML comes from + # a trusted provider over a controlled HTTP connection, not from + # arbitrary user input. root = ET.fromstring(response.text) # noqa: S314 + id_text = root.findtext("id") + name_text = root.findtext("name") + if id_text is None or name_text is None: + msg = "Provider response missing required XML element 'id' or 'name'" + raise ValueError(msg) return User( - id=int(root.findtext("id") or 0), - name=root.findtext("name") or "", + id=int(id_text), + name=name_text, ) diff --git a/examples/http/xml_example/provider.py b/examples/http/xml_example/provider.py index 1a051874f..e777bfa71 100644 --- a/examples/http/xml_example/provider.py +++ b/examples/http/xml_example/provider.py @@ -4,14 +4,25 @@ This module defines a simple [provider](https://docs.pact.io/getting_started/terminology#service-provider) implemented with [`fastapi`](https://fastapi.tiangolo.com/) which will be tested -with Pact in the [provider test][examples.http.xml_example.test_provider]. +with Pact in the [provider test][examples.http.xml_example.test_provider]. As +Pact is a consumer-driven framework, the consumer defines the contract which the +provider must then satisfy. The provider receives requests from the consumer and returns XML responses built using the standard library [`xml.etree.ElementTree`][xml.etree.ElementTree] -module. +module. Serialisation is handled manually rather than via FastAPI's built-in +JSON serialisation, since FastAPI does not natively support XML response bodies. + +This also showcases how Pact tests differ from merely testing adherence to an +OpenAPI specification. The Pact tests are concerned with the practical use of +the API from the consumer's perspective. The `User` model here could contain +additional fields in a real application; if the provider later adds or removes +fields, Pact's consumer-driven testing will verify that the consumer remains +compatible with those changes. Note that the code in this module is agnostic of Pact (i.e., this would be your -production code). The `pact-python` dependency only appears in the tests. +production code). The `pact-python` dependency only appears in the tests. This +is because the provider is not concerned with Pact, only the tests are. """ from __future__ import annotations @@ -31,6 +42,17 @@ class User: """ Represents a user in the provider system. + + This class uses a plain dataclass rather than Pydantic to keep the focus + on the XML serialisation pattern. In a real FastAPI application you would + typically use a Pydantic model to benefit from automatic validation and + JSON serialisation; see the + [`requests_and_fastapi`][examples.http.requests_and_fastapi.provider.User] + example for that approach. + + The provider's model may contain more fields than any single consumer + requires. This demonstrates how provider-side data can be richer than what + appears in the consumer contract. """ id: int @@ -40,6 +62,13 @@ class User: class UserDb: """ A simple in-memory user database abstraction for demonstration purposes. + + This class simulates a user database using a class-level dictionary. In a + real application this would interface with a persistent database or external + service. For testing, the state handlers in the [provider + test][examples.http.xml_example.test_provider] populate and clean up this + database directly, ensuring each contract interaction runs against a + predictable state. """ _db: ClassVar[dict[int, User]] = {} @@ -48,6 +77,9 @@ class UserDb: def create(cls, user: User) -> None: """ Add a new user to the database. + + Args: + user: The User instance to add. """ cls._db[user.id] = user @@ -56,6 +88,9 @@ def delete(cls, user_id: int) -> None: """ Delete a user from the database by their ID. + Args: + user_id: The ID of the user to delete. + Raises: KeyError: If the user does not exist. """ @@ -68,6 +103,12 @@ def delete(cls, user_id: int) -> None: def get(cls, user_id: int) -> User | None: """ Retrieve a user by their ID. + + Args: + user_id: The ID of the user to retrieve. + + Returns: + The User instance if found, else None. """ return cls._db.get(user_id) diff --git a/examples/http/xml_example/test_consumer.py b/examples/http/xml_example/test_consumer.py index a7f758046..c96dfb2af 100644 --- a/examples/http/xml_example/test_consumer.py +++ b/examples/http/xml_example/test_consumer.py @@ -3,10 +3,54 @@ This module demonstrates how to test a consumer (see [`consumer.py`][examples.http.xml_example.consumer]) against a mock provider -using Pact. The key difference from JSON-based examples is that the response -body is specified as a plain XML string — no matchers are used, as XML matchers -do not exist in pact-python. The `Accept` header is set via a separate -`.with_header()` call after `.with_request()`. +using Pact. The mock provider is set up by Pact to validate that the consumer +makes the expected requests and can handle the provider's responses. Once +validated, the contract is written to a file for use in provider verification. + +## XML matchers + +Unlike a literal XML string, the response body can be expressed using the +`pact.xml` builder. This allows standard Pact matchers (such as +`match.int()` and `match.str()`) to be embedded in the body description, so +the contract specifies *structural constraints* rather than exact values. + +Whereas a JSON body can be described as: + +```python +response = {"id": match.int(123), "name": match.str("Alice")} +``` + +An equivalent XML body is described with the `xml` builder as: + +```python +from pact import match, xml + +response = xml.body( + xml.element( + "user", + xml.element("id", match.int(123)), + xml.element("name", match.str("Alice")), + ) +) +``` + +Pass this dict to `.with_body()` with `content_type="application/xml"`. The +Pact FFI detects that the body is JSON, generates the example XML, and +registers matching rules for each annotated element. The resulting contract +will match _any_ XML response where `` contains an integer and `` +contains a string and not just the specific example values. + +For attribute matchers, pass matcher objects via the `attrs` keyword argument: + +```python +xml.element("user", attrs={"id": match.int(1), "type": "activity"}) +``` + +## Setting request headers + +`with_request()` does not accept a `headers` argument. Instead, use a +subsequent `.with_header()` call on the same interaction chain, as shown in the +tests below. """ from __future__ import annotations @@ -18,7 +62,7 @@ import requests from examples.http.xml_example.consumer import UserClient -from pact import Pact +from pact import Pact, match, xml if TYPE_CHECKING: from collections.abc import Generator @@ -32,6 +76,18 @@ def pact(pacts_path: Path) -> Generator[Pact, None, None]: """ Set up a Pact mock provider for consumer tests. + This fixture creates a `Pact` object that acts as a mock + provider. Each test defines the expected request and response using the Pact + DSL, and Pact spins up a local HTTP server that validates the consumer + makes exactly those requests. This allows the consumer to be tested in + complete isolation from the real provider, no running provider is needed + at this stage. + + After all tests in a session have run, `write_file` serialises the + recorded interactions to a contract file. This file is then used by the + [provider test][examples.http.xml_example.test_provider] to verify that + the real provider honours the contract. + Args: pacts_path: The path where the generated pact file will be written. @@ -46,13 +102,23 @@ def pact(pacts_path: Path) -> Generator[Pact, None, None]: def test_get_user(pact: Pact) -> None: """ - Test the GET request for a user, expecting an XML response. - - The response body is a plain XML string. Note that `.with_header()` is - called as a separate chain step — `with_request()` does not accept a - headers argument. + Test the GET request for a user, expecting an XML response with matchers. + + This test defines the expected interaction for a successful user lookup. + It demonstrates how to use `xml.body()` and `xml.element()` to specify + structural constraints on + the response body: the `id` element must contain an integer and the `name` + element must contain a non-null string, but their exact values do not + matter. The `Accept: application/xml` request header is registered via a + separate `.with_header()` call after `.with_request()`. """ - response = "123Alice" + response = xml.body( + xml.element( + "user", + xml.element("id", match.int(123)), + xml.element("name", match.str("Alice")), + ) + ) ( pact .upon_receiving("A request for a user as XML") @@ -75,6 +141,14 @@ def test_get_user(pact: Pact) -> None: def test_get_unknown_user(pact: Pact) -> None: """ Test the GET request for an unknown user, expecting a 404 response. + + This test verifies that the consumer correctly handles a 404 error by + propagating a `requests.HTTPError` (via `raise_for_status()`). No response + body is matched: when the provider returns a 404, FastAPI produces a JSON + error body, but this consumer does not inspect the error body; it only + checks the status code and raises. Explicitly omitting `.with_body()` here + communicates that the consumer's contract requirement is limited to the + status code, not the error payload. """ ( pact diff --git a/examples/http/xml_example/test_provider.py b/examples/http/xml_example/test_provider.py index 1e988acad..538147771 100644 --- a/examples/http/xml_example/test_provider.py +++ b/examples/http/xml_example/test_provider.py @@ -6,8 +6,12 @@ using Pact. The mock consumer replays the requests defined by the consumer contract, and Pact validates that the provider responds as expected. -Provider state handlers set up the in-memory database before each interaction -is verified, ensuring repeatable and isolated contract verification. +These tests show how provider verification ensures that the provider remains +compatible with the consumer contract as the provider evolves. Provider state +management is handled by manipulating the in-memory database directly before +each interaction is replayed. For more, see the [Pact Provider +Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-provider-pact-test) +section of the Pact documentation. """ from __future__ import annotations @@ -60,11 +64,26 @@ def app_server() -> str: def test_provider(app_server: str, pacts_path: Path) -> None: """ - Test the provider against the consumer contract. - - Runs the Pact verifier against the FastAPI provider using the contract - generated by the consumer tests. State handlers ensure the database is - in the correct state for each interaction. + Test the provider against the mock consumer contract. + + This test runs the Pact verifier against the FastAPI provider, using the + contract generated by the consumer tests. + + Provider state handlers are essential in Pact contract testing. They allow + the provider to be set up in a specific state before each interaction is + verified. For example, if a consumer expects a user to exist for a certain + request, the provider state handler ensures the in-memory database is + populated accordingly. This enables repeatable and isolated contract + verification: each interaction is tested in the correct context without + relying on global or persistent state. + + In this example, `mock_user_exists` and `mock_user_does_not_exist` are + mapped to the state names declared in the consumer contract. They are + responsible for setting up (and tearing down) the database so that the + provider can respond correctly to each replayed request. + + For additional information on state handlers, see + [`Verifier.state_handler`][pact.verifier.Verifier.state_handler]. """ verifier = ( Verifier("xml-provider") @@ -89,12 +108,21 @@ def mock_user_exists( """ Mock the provider state where a user exists. + This handler is invoked by Pact before and after each interaction that + declares the state `"the user exists"`. On setup, a `User` record is + inserted into the in-memory database using the `id` and `name` parameters + from the consumer contract. On teardown, the record is removed so that + subsequent interactions start from a clean slate. + Args: action: - Either "setup" or "teardown". + Either `"setup"` (called before the interaction) or `"teardown"` + (called after). parameters: - Must contain "id" and optionally "name". + State parameters from the consumer contract. Must contain `"id"`; + `"name"` is optional and defaults to `"Alice"`. """ + logger.debug("mock_user_exists(%s, %r)", action, parameters) user = User( id=int(parameters.get("id", 1)), name=str(parameters.get("name", "Alice")), @@ -120,12 +148,21 @@ def mock_user_does_not_exist( """ Mock the provider state where a user does not exist. + This handler is invoked by Pact before and after each interaction that + declares the state `"the user doesn't exist"`. On setup, any existing user + with the given `id` is removed from the in-memory database, ensuring the + provider will return a 404. On teardown, nothing needs to be restored + because nothing was added during setup, the user simply remains absent. + Args: action: - Either "setup" or "teardown". + Either `"setup"` (called before the interaction) or `"teardown"` + (called after). parameters: - Must contain "id". + State parameters from the consumer contract. Must contain `"id"`. """ + logger.debug("mock_user_does_not_exist(%s, %r)", action, parameters) + if "id" not in parameters: msg = "State must contain an 'id' field to mock user non-existence" raise ValueError(msg) @@ -138,6 +175,7 @@ def mock_user_does_not_exist( return if action == "teardown": + # Nothing was added during setup, so there is nothing to clean up. return msg = f"Unknown action: {action}" From 364fda7f8b56c3c5cb0da5cbf04d90403509f137 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Sun, 29 Mar 2026 18:27:40 +0900 Subject: [PATCH 1291/1376] ci: fix coverage upload overwrite and add example coverage Each test run now writes to a named coverage file via --cov-report=xml: rather than all overwriting the same coverage.xml. The upload step collects all four files explicitly. Example runs now measure pact coverage via --with pytest-cov injected inline, and upload via a glob. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9d3e1d97..15de1e2e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,18 +96,18 @@ jobs: run: uv tool install hatch - name: Run tests - run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml + run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml --cov-report=xml:coverage-tests.xml - name: Run tests (v2) - run: hatch run v2-test.py${{ matrix.python-version }}:test --junit-xml=v2-junit.xml + run: hatch run v2-test.py${{ matrix.python-version }}:test --junit-xml=v2-junit.xml --cov-report=xml:coverage-v2.xml - name: Run tests (CLI) working-directory: pact-python-cli - run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml + run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml --cov-report=xml:coverage-cli.xml - name: Run tests (FFI) working-directory: pact-python-ffi - run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml + run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml --cov-report=xml:coverage-ffi.xml - name: Upload coverage if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest' @@ -115,6 +115,7 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests + files: coverage-tests.xml,coverage-v2.xml,pact-python-cli/coverage-cli.xml,pact-python-ffi/coverage-ffi.xml - name: Upload test results if: ${{ !cancelled() }} @@ -166,7 +167,7 @@ jobs: while IFS= read -r -d $'\0' file <&3; do cd "$(dirname "$file")" echo "Running example in $(pwd)" - uv run --python ${{ matrix.python-version }} --group test pytest --junit-xml=junit.xml + uv run --python ${{ matrix.python-version }} --group test --with pytest-cov pytest --junit-xml=junit.xml --cov=pact --cov-report=xml:coverage-example.xml done 3< <(find "$(pwd)/examples" -name pyproject.toml -print0) - name: Upload coverage @@ -175,6 +176,7 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} flags: examples + files: examples/**/coverage-example.xml - name: Upload test results if: ${{ !cancelled() }} From 763ca3ed7bf281960f720e943bc22cf311c5b3aa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:29:41 +1100 Subject: [PATCH 1292/1376] chore(deps): update astral-sh/setup-uv action to v8 (#1519) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 70e82ac4e..2080c779d 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 95771164d..f3ba888ed 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e16e247f3..1be117cdf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index eae390912..f2755266f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15de1e2e3..f94e3be01 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true @@ -151,7 +151,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true @@ -196,7 +196,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true @@ -228,7 +228,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true @@ -261,7 +261,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true @@ -301,7 +301,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true cache-suffix: prek From 70a70599ed40d2367b4cf02a2cf08820cac6bd07 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:31:59 +1100 Subject: [PATCH 1293/1376] chore(deps): update taiki-e/install-action action to v2.70.2 (#1520) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 2080c779d..43a71628d 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2.69.6 + uses: taiki-e/install-action@e9e8e031bcd90cdbe8ac6bb1d376f8596e587fbf # v2.70.2 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index f3ba888ed..d9677feec 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2.69.6 + uses: taiki-e/install-action@e9e8e031bcd90cdbe8ac6bb1d376f8596e587fbf # v2.70.2 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1be117cdf..d79e1ce92 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2.69.6 + uses: taiki-e/install-action@e9e8e031bcd90cdbe8ac6bb1d376f8596e587fbf # v2.70.2 with: tool: git-cliff,typos From e7257b8308c900b659a7e23541be528211ea2d36 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:59:26 +1100 Subject: [PATCH 1294/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.4.10 (#1521) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a73b3f85c..e03adf929 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.4.9 + rev: v2.4.10 hooks: - id: biome-check From 11c5a244f8f622c69e2cbc52148d2385e94ab755 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:59:51 +1100 Subject: [PATCH 1295/1376] chore(deps): update dependency mypy to v1.20.0 (#1522) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 0f29d03bc..7cac9a70f 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -56,7 +56,7 @@ requires-python = ">=3.10" # developper consistency. All other dependencies are as above. dev = ["ruff==0.15.8", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] -types = ["mypy==1.19.1"] +types = ["mypy==1.20.0"] ################################################################################ ## Build System diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 3b325c25d..07a44788f 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -43,7 +43,7 @@ dependencies = ["cffi~=2.0"] [dependency-groups] dev = ["ruff==0.15.8", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] -types = ["mypy==1.19.1", "typing-extensions~=4.0"] +types = ["mypy==1.20.0", "typing-extensions~=4.0"] ################################################################################ ## Build System diff --git a/pyproject.toml b/pyproject.toml index f747331a7..86bed9479 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ test = [ "testcontainers~=4.0", ] types = [ - "mypy==1.19.1", + "mypy==1.20.0", "types-grpcio~=1.0", "types-protobuf~=6.0", "types-requests~=2.0", From b1c44a4d3cd4d7d3820845b0b35d3ba923f1a868 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:00:13 +1100 Subject: [PATCH 1296/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.45.0 (#1523) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e03adf929..b8be03b32 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.44.0 + rev: v1.45.0 hooks: - id: typos exclude: | From 272e5dcf20af840e8382ff3b7fd317fd93188ac8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:00:33 +1100 Subject: [PATCH 1297/1376] chore(deps): update pypa/cibuildwheel action to v3.4.1 (#1525) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 43a71628d..1e4486aa5 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@ee02a1537ce3071a004a6b08c41e72f0fdc42d9a # v3.4.0 + uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 with: package-dir: pact-python-cli env: diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index d9677feec..47eebe53c 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -99,7 +99,7 @@ jobs: fetch-depth: 0 - name: Create wheels - uses: pypa/cibuildwheel@ee02a1537ce3071a004a6b08c41e72f0fdc42d9a # v3.4.0 + uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 with: package-dir: pact-python-ffi env: From 536e610fed89f8880f6707ac566d4533dedbf949 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:00:38 +1100 Subject: [PATCH 1298/1376] chore(deps): update ruff to v0.15.9 (#1526) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b8be03b32..885e347ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.8 + rev: v0.15.9 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 7cac9a70f..4e2d08622 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.15.8", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.9", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.20.0"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 07a44788f..85285e8b4 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.15.8", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.9", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.20.0", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 86bed9479..5ead66a1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.15.8", + "ruff==0.15.9", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From 3df06a84c8bf870f1edd996af44d885dd11435e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 09:24:40 +0000 Subject: [PATCH 1299/1376] chore(deps): update tests/compatibility_suite/definition digest to b03375f (#1527) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- tests/compatibility_suite/definition | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/compatibility_suite/definition b/tests/compatibility_suite/definition index 1acfa1ecb..b03375f00 160000 --- a/tests/compatibility_suite/definition +++ b/tests/compatibility_suite/definition @@ -1 +1 @@ -Subproject commit 1acfa1ecbd9d63e4465c687b3cdd7e0d3ac5811c +Subproject commit b03375f00f5adf1346bd76edb0c7968cc0b495cf From 9668714306167ca2c8fefc8a286d11978700ef34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 09:26:11 +0000 Subject: [PATCH 1300/1376] chore(deps): update dependency types-protobuf to v7 (#1524) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5ead66a1d..8c64205cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,7 +125,7 @@ test = [ types = [ "mypy==1.20.0", "types-grpcio~=1.0", - "types-protobuf~=6.0", + "types-protobuf~=7.34", "types-requests~=2.0", # This is required for Python 3.10 support "typing-extensions~=4.0", From b03a9b607e6c6ee7bb8f48d05095e39e880a5526 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:29:25 +1000 Subject: [PATCH 1301/1376] chore(deps): update python:3.14-slim docker digest to 5e59aae (#1530) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 9c34fe7ed..240c73511 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:fb83750094b46fd6b8adaa80f66e2302ecbe45d513f6cece637a841e1025b4ca +FROM python:3.14-slim@sha256:5e59aae31ff0e87511226be8e2b94d78c58f05216efda3b07dbbed938ec8583b ARG USERNAME=vscode ARG USER_UID=1000 From 2c60948a4296793ede399f5916fa025fbef4fe68 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:29:29 +1000 Subject: [PATCH 1302/1376] chore(deps): update pypa/gh-action-pypi-publish action to v1.14.0 (#1531) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 1e4486aa5..d10064a46 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -195,7 +195,7 @@ jobs: generate_release_notes: true - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: skip-existing: true packages-dir: wheelhouse diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 47eebe53c..22cc58eb3 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -196,7 +196,7 @@ jobs: generate_release_notes: true - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: skip-existing: true packages-dir: wheelhouse diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d79e1ce92..c2b9ad232 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -162,7 +162,7 @@ jobs: generate_release_notes: true - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: skip-existing: true packages-dir: wheelhouse From 8447a104a937a95ea732ae6d763dc10b02ef14f4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:29:42 +1000 Subject: [PATCH 1303/1376] chore(deps): update taiki-e/install-action action to v2.75.0 (#1529) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index d10064a46..a7af9e042 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@e9e8e031bcd90cdbe8ac6bb1d376f8596e587fbf # v2.70.2 + uses: taiki-e/install-action@cf39a74df4a72510be4e5b63348d61067f11e64a # v2.75.0 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 22cc58eb3..046074bbc 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@e9e8e031bcd90cdbe8ac6bb1d376f8596e587fbf # v2.70.2 + uses: taiki-e/install-action@cf39a74df4a72510be4e5b63348d61067f11e64a # v2.75.0 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c2b9ad232..b214a5339 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@e9e8e031bcd90cdbe8ac6bb1d376f8596e587fbf # v2.70.2 + uses: taiki-e/install-action@cf39a74df4a72510be4e5b63348d61067f11e64a # v2.75.0 with: tool: git-cliff,typos From 84bc2c6cb586530799ffd1106cc9423a102bb024 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:22:17 +1000 Subject: [PATCH 1304/1376] chore(deps): update ruff to v0.15.10 (#1533) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 885e347ae..a782df06c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.9 + rev: v0.15.10 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 4e2d08622..d70467cab 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.15.9", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.10", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.20.0"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 85285e8b4..8f05cef2e 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.15.9", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.10", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] types = ["mypy==1.20.0", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 8c64205cc..da9715748 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.15.9", + "ruff==0.15.10", { include-group = "docs" }, { include-group = "example" }, { include-group = "test" }, From d0032b1da27767776435c16b871ac3dfab0b3240 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:22:38 +1000 Subject: [PATCH 1305/1376] chore(deps): update actions/upload-artifact action to v7.0.1 (#1536) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 4 ++-- .github/workflows/build-ffi.yml | 4 ++-- .github/workflows/build.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index a7af9e042..5d1d98308 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -70,7 +70,7 @@ jobs: run: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-sdist path: pact-python-cli/dist/*.tar* @@ -106,7 +106,7 @@ jobs: CIBW_BUILD: cp${{ env.STABLE_PYTHON_VERSION }}-* - name: Upload wheels - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-${{ matrix.os }} path: wheelhouse/*.whl diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 046074bbc..db9f990f6 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -70,7 +70,7 @@ jobs: run: hatch build --target sdist - name: Upload sdist - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-sdist path: pact-python-ffi/dist/*.tar* @@ -107,7 +107,7 @@ jobs: HATCH_VERBOSE: '1' - name: Upload wheels - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-${{ matrix.os }} path: wheelhouse/*.whl diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b214a5339..9478d3c05 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: run: hatch build - name: Upload sdist - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-sdist path: ./dist/*.tar* @@ -76,7 +76,7 @@ jobs: compression-level: 0 - name: Upload wheel - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-whl path: ./dist/*.whl From f7ca52170474314386967cecc672548eea62b815 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:22:55 +1000 Subject: [PATCH 1306/1376] chore(deps): update python:3.14-slim docker digest to bc389f7 (#1532) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 240c73511..0b48c700b 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:5e59aae31ff0e87511226be8e2b94d78c58f05216efda3b07dbbed938ec8583b +FROM python:3.14-slim@sha256:bc389f7dfcb21413e72a28f491985326994795e34d2b86c8ae2f417b4e7818aa ARG USERNAME=vscode ARG USER_UID=1000 From be9fc70e5c6db966087028746ddd32328153731c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:22:57 +1000 Subject: [PATCH 1307/1376] chore(deps): update peter-evans/create-pull-request action to v8.1.1 (#1535) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 5d1d98308..191829649 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -201,7 +201,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index db9f990f6..eff5a320c 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -202,7 +202,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9478d3c05..1c7145922 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -168,7 +168,7 @@ jobs: packages-dir: wheelhouse - name: Create PR for changelog update - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: token: ${{ secrets.GH_TOKEN }} commit-message: 'docs: update changelog for ${{ github.ref_name }}' From 92e5281b8adce8da75b8509a167c706d38e28b8f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:23:15 +1000 Subject: [PATCH 1308/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.4.11 (#1534) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a782df06c..fb9f9f4c8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.4.10 + rev: v2.4.11 hooks: - id: biome-check From f3c0b2bfc144c1aa5cf923c478e5f937aa9944e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:54:54 +1000 Subject: [PATCH 1309/1376] chore(deps): update softprops/action-gh-release action to v3 (#1538) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 191829649..bc2062634 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -186,7 +186,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index eff5a320c..8f052f93b 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -187,7 +187,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1c7145922..4122ea7ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,7 +153,7 @@ jobs: - name: Generate release id: release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: files: wheelhouse/* body_path: ${{ runner.temp }}/release-changelog.md From 4071dac127255ccbc674ed3c6c3da305a900b838 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:35:16 +1000 Subject: [PATCH 1310/1376] chore(deps): update dependency mypy to v1.20.1 (#1540) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index d70467cab..0410ef9da 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -56,7 +56,7 @@ requires-python = ">=3.10" # developper consistency. All other dependencies are as above. dev = ["ruff==0.15.10", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] -types = ["mypy==1.20.0"] +types = ["mypy==1.20.1"] ################################################################################ ## Build System diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 8f05cef2e..3be085cdb 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -43,7 +43,7 @@ dependencies = ["cffi~=2.0"] [dependency-groups] dev = ["ruff==0.15.10", { include-group = "test" }, { include-group = "types" }] test = ["pytest-cov~=7.0", "pytest~=9.0"] -types = ["mypy==1.20.0", "typing-extensions~=4.0"] +types = ["mypy==1.20.1", "typing-extensions~=4.0"] ################################################################################ ## Build System diff --git a/pyproject.toml b/pyproject.toml index da9715748..e79229bec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ test = [ "testcontainers~=4.0", ] types = [ - "mypy==1.20.0", + "mypy==1.20.1", "types-grpcio~=1.0", "types-protobuf~=7.34", "types-requests~=2.0", From cd6be98985624c33342ef2f0cc6f542aefd38782 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:35:52 +1000 Subject: [PATCH 1311/1376] chore(deps): update taiki-e/install-action action to v2.75.7 (#1539) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-ffi.yml | 2 +- .github/workflows/build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index bc2062634..5e632d053 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -140,7 +140,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@cf39a74df4a72510be4e5b63348d61067f11e64a # v2.75.0 + uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 with: tool: git-cliff,typos diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml index 8f052f93b..32d9008bd 100644 --- a/.github/workflows/build-ffi.yml +++ b/.github/workflows/build-ffi.yml @@ -141,7 +141,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@cf39a74df4a72510be4e5b63348d61067f11e64a # v2.75.0 + uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 with: tool: git-cliff,typos diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4122ea7ee..fe0f64b75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,7 +109,7 @@ jobs: fetch-depth: 0 - name: Install git cliff and typos - uses: taiki-e/install-action@cf39a74df4a72510be4e5b63348d61067f11e64a # v2.75.0 + uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 with: tool: git-cliff,typos From 68795d88acc3d88f9dd35f38fe407d7a95852c0c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 13 Apr 2026 11:26:01 +1000 Subject: [PATCH 1312/1376] chore: add .worktrees to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index f92d2bf56..bda0103d6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ src/pact/__version__.py # Wheels from CIBuildWheel wheelhouse/ +# Git worktrees +.worktrees/ + ################################################################################ ## Standard Templates ################################################################################ From f87c777f0e48bd3f85f44bada38f2f3bdeb57d50 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 01:45:48 +0000 Subject: [PATCH 1313/1376] chore(deps): update actions/cache action to v5.0.5 (#1542) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f94e3be01..6ff52cdf2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -287,7 +287,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Cache prek - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: |- ~/.cache/prek From 6cb61a699949e1a7ff7ccfff2e21bc89092aec27 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 14 Apr 2026 11:14:19 +1000 Subject: [PATCH 1314/1376] chore(ci): reduce ci usage Instead of testing every supported version of Python, only test the extremes, i.e., the oldest and most recent. Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ff52cdf2..c66a4df6f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,9 +63,6 @@ jobs: - windows-latest python-version: - '3.10' - - '3.11' - - '3.12' - - '3.13' - '3.14' steps: @@ -139,9 +136,6 @@ jobs: - windows-latest python-version: - '3.10' - - '3.11' - - '3.12' - - '3.13' - '3.14' steps: From ae9472e64daa6e7b3939037abc99def5e81e00d7 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 14 Apr 2026 11:22:30 +1000 Subject: [PATCH 1315/1376] chore(ci): downgrade stable python version Default to the _oldest_ Python version as the 'stable' one in CI. Signed-off-by: JP-Ellis --- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f2755266f..a463b2b12 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,7 +10,7 @@ on: - main env: - STABLE_PYTHON_VERSION: '3.13' + STABLE_PYTHON_VERSION: '3.10' FORCE_COLOR: '1' HATCH_VERBOSE: '1' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c66a4df6f..b17eadda2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ permissions: contents: read env: - STABLE_PYTHON_VERSION: '3.13' + STABLE_PYTHON_VERSION: '3.10' PYTEST_ADDOPTS: --color=yes HATCH_VERBOSE: '1' FORCE_COLOR: '1' From fa0292ae6109841dd3acfd4daad6438fed54f46f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 14 Apr 2026 11:32:27 +1000 Subject: [PATCH 1316/1376] chore(ci): remove unused workflows Signed-off-by: JP-Ellis --- .../workflows/smartbear-issue-label-added.yml | 14 ------------ .github/workflows/triage.yml | 16 -------------- .../workflows/trigger_pact_docs_update.yml | 22 ------------------- 3 files changed, 52 deletions(-) delete mode 100644 .github/workflows/smartbear-issue-label-added.yml delete mode 100644 .github/workflows/triage.yml delete mode 100644 .github/workflows/trigger_pact_docs_update.yml diff --git a/.github/workflows/smartbear-issue-label-added.yml b/.github/workflows/smartbear-issue-label-added.yml deleted file mode 100644 index 2966ba5f4..000000000 --- a/.github/workflows/smartbear-issue-label-added.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: SmartBear Supported Issue Label Added - -on: - issues: - types: - - labeled - -jobs: - call-workflow: - uses: pact-foundation/.github/.github/workflows/smartbear-issue-label-added.yml@master - permissions: - issues: write - secrets: inherit diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml deleted file mode 100644 index ed0263f8a..000000000 --- a/.github/workflows/triage.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Triage Issue - -on: - issues: - types: - - opened - - labeled - pull_request: - types: - - labeled - -jobs: - call-workflow: - uses: pact-foundation/.github/.github/workflows/triage.yml@master - secrets: inherit diff --git a/.github/workflows/trigger_pact_docs_update.yml b/.github/workflows/trigger_pact_docs_update.yml deleted file mode 100644 index 046143f23..000000000 --- a/.github/workflows/trigger_pact_docs_update.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: Trigger update to docs.pact.io - -on: - push: - branches: - - main - paths: - - '**.md' - -jobs: - run: - runs-on: ubuntu-latest - steps: - - name: Trigger docs.pact.io update workflow - run: | - curl -X POST https://api.github.com/repos/pact-foundation/docs.pact.io/dispatches \ - -H 'Accept: application/vnd.github.everest-preview+json' \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -d '{"event_type": "pact-python-docs-updated"}' - env: - GITHUB_TOKEN: ${{ secrets.GHTOKENFORTRIGGERINGPACTDOCSUPDATE }} From a1e3caf32d942323aeb5eafcdc851e67753a2f63 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 02:12:40 +0000 Subject: [PATCH 1317/1376] chore(deps): update actions/upload-pages-artifact action to v5 (#1544) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a463b2b12..346ae3868 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -45,7 +45,7 @@ jobs: hatch run mkdocs build --strict - name: Upload artifact - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 with: path: site From 41c7f34ae71feb5d05a59823e2460bb5997d3627 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 04:21:56 +0000 Subject: [PATCH 1318/1376] chore(deps): update pre-commit hook crate-ci/typos to v1.45.1 (#1543) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb9f9f4c8..862ee8155 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: - id: markdownlint-cli2 - repo: https://github.com/crate-ci/typos - rev: v1.45.0 + rev: v1.45.1 hooks: - id: typos exclude: | From 57c60b5b586a5dfe93c66f33c2f4da04f420a8e2 Mon Sep 17 00:00:00 2001 From: Aditya Giri <74224708+adityagiri3600@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:21:48 +0530 Subject: [PATCH 1319/1376] docs(examples): add service consumer/provider HTTP example Implement an example that is simultaneously a consumer and provider. Co-authored-by: Aditya Giri <74224708+adityagiri3600@users.noreply.github.com> Co-authored-by: JP-Ellis Signed-off-by: JP-Ellis --- examples/README.md | 7 + examples/http/README.md | 1 + .../http/service_consumer_provider/README.md | 37 +++ .../service_consumer_provider/__init__.py | 1 + .../service_consumer_provider/auth_client.py | 76 ++++++ .../service_consumer_provider/conftest.py | 37 +++ .../frontend_client.py | 98 ++++++++ .../service_consumer_provider/pyproject.toml | 26 +++ .../test_consumer_auth.py | 128 ++++++++++ .../test_consumer_frontend.py | 144 ++++++++++++ .../test_provider.py | 218 ++++++++++++++++++ .../service_consumer_provider/user_service.py | 195 ++++++++++++++++ tests/compatibility_suite/definition | 2 +- 13 files changed, 969 insertions(+), 1 deletion(-) create mode 100644 examples/http/service_consumer_provider/README.md create mode 100644 examples/http/service_consumer_provider/__init__.py create mode 100644 examples/http/service_consumer_provider/auth_client.py create mode 100644 examples/http/service_consumer_provider/conftest.py create mode 100644 examples/http/service_consumer_provider/frontend_client.py create mode 100644 examples/http/service_consumer_provider/pyproject.toml create mode 100644 examples/http/service_consumer_provider/test_consumer_auth.py create mode 100644 examples/http/service_consumer_provider/test_consumer_frontend.py create mode 100644 examples/http/service_consumer_provider/test_provider.py create mode 100644 examples/http/service_consumer_provider/user_service.py diff --git a/examples/README.md b/examples/README.md index 0651f9e45..2af1fdc04 100644 --- a/examples/README.md +++ b/examples/README.md @@ -30,6 +30,13 @@ The code within the examples is intended to be well-documented and you are encou - **Consumer**: requests-based HTTP client - **Provider**: FastAPI-based HTTP server +#### [Service as Consumer and Provider](./http/service_consumer_provider/README.md) + +- **Location**: `examples/http/service_consumer_provider/` +- **Scenario**: A single service (`user-service`) acting as both: + - **Provider** to a frontend client + - **Consumer** of an upstream auth service + ### Message Examples - **Location**: `examples/message/` diff --git a/examples/http/README.md b/examples/http/README.md index 1579c6312..aa7eb21a4 100644 --- a/examples/http/README.md +++ b/examples/http/README.md @@ -6,4 +6,5 @@ This directory contains examples of HTTP-based contract testing with Pact. - [`aiohttp_and_flask/`](aiohttp_and_flask/) - Async aiohttp consumer with Flask provider - [`requests_and_fastapi/`](requests_and_fastapi/) - requests consumer with FastAPI provider +- [`service_consumer_provider/`](service_consumer_provider/) - One service acting as both consumer and provider - [`xml_example/`](xml_example/) - requests consumer with FastAPI provider using XML bodies diff --git a/examples/http/service_consumer_provider/README.md b/examples/http/service_consumer_provider/README.md new file mode 100644 index 000000000..78a9a1398 --- /dev/null +++ b/examples/http/service_consumer_provider/README.md @@ -0,0 +1,37 @@ +# Service as Consumer and Provider + +This example demonstrates a common microservice pattern where one service plays both roles in contract testing: + +- **Provider** to a frontend client (`frontend-web → user-service`) +- **Consumer** of an upstream auth service (`user-service → auth-service`) + +## Overview + +- [**Frontend Client**][examples.http.service_consumer_provider.frontend_client]: Consumer-facing client used by `frontend-web` +- [**Auth Client**][examples.http.service_consumer_provider.auth_client]: Upstream client used by `user-service` to call `auth-service` +- [**User Service**][examples.http.service_consumer_provider.user_service]: FastAPI app under test (the service in the middle) +- [**Frontend Consumer Tests**][examples.http.service_consumer_provider.test_consumer_frontend]: Defines `frontend-web`'s expectations of `user-service` +- [**Auth Consumer Tests**][examples.http.service_consumer_provider.test_consumer_auth]: Defines `user-service`'s expectations of `auth-service` +- [**Provider Verification**][examples.http.service_consumer_provider.test_provider]: Verifies `user-service` against the frontend pact + +Use the links above to view detailed documentation within each file. + +## What This Example Demonstrates + +- One service owning two separate contracts in opposite directions +- Consumer tests for each dependency boundary +- Provider verification with state handlers that model upstream `auth-service` behaviour without needing it to run +- A `Protocol`-based seam that allows the real FastAPI application to run during verification while the upstream dependency is replaced in-process + +## Running the Example + +```console +uv run --group test pytest +``` + +## Related Documentation + +- [Pact Documentation](https://docs.pact.io/) +- [Provider States](https://docs.pact.io/getting_started/provider_states) +- [FastAPI Documentation](https://fastapi.tiangolo.com/) +- [pytest Documentation](https://docs.pytest.org/) diff --git a/examples/http/service_consumer_provider/__init__.py b/examples/http/service_consumer_provider/__init__.py new file mode 100644 index 000000000..6e031999e --- /dev/null +++ b/examples/http/service_consumer_provider/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/examples/http/service_consumer_provider/auth_client.py b/examples/http/service_consumer_provider/auth_client.py new file mode 100644 index 000000000..b95b60463 --- /dev/null +++ b/examples/http/service_consumer_provider/auth_client.py @@ -0,0 +1,76 @@ +""" +HTTP client used by `user-service` to call `auth-service`. + +This module is intentionally free of any Pact dependency, it is production code. +The Pact dependency only appears in +[`test_consumer_auth`][examples.http.service_consumer_provider.test_consumer_auth], +which exercises this client against a Pact mock server to define the contract +between [`user_service`][examples.http.service_consumer_provider.user_service] +(consumer) and `auth-service` (provider). + +This also demonstrates the consumer-driven philosophy: the client only requests +and parses the fields it actually needs (`valid`), even though `auth-service` +may return additional information in its response. +""" + +from __future__ import annotations + +import requests + + +class AuthClient: + """ + HTTP client for credential validation against `auth-service`. + + This client is used by + [`user_service`][examples.http.service_consumer_provider.user_service] to + verify user credentials before creating accounts. It satisfies the + [`CredentialsVerifier`][examples.http.service_consumer_provider.user_service.CredentialsVerifier] + protocol and is the real implementation that would run in production. + + The matching Pact consumer tests live in + [`test_consumer_auth`][examples.http.service_consumer_provider.test_consumer_auth]. + """ + + def __init__(self, base_url: str) -> None: + """ + Initialise the auth client. + + Args: + base_url: + Base URL of `auth-service`, e.g. `http://auth-service`. Trailing + slashes are stripped automatically. + """ + self._base_url = base_url.rstrip("/") + self._session = requests.Session() + + def validate_credentials(self, username: str, password: str) -> bool: + """ + Validate credentials against `auth-service`. + + Sends a `POST /auth/validate` request with the supplied credentials and + returns whether `auth-service` considers them valid. This is the only + field the client reads from the response: an example of how + consumer-driven contracts focus on what the consumer *actually uses*. + + Args: + username: + Username to validate. + + password: + Password to validate. + + Returns: + `True` when credentials are valid; otherwise `False`. + + Raises: + requests.HTTPError: + If `auth-service` responds with a non-2xx status. + """ + response = self._session.post( + f"{self._base_url}/auth/validate", + json={"username": username, "password": password}, + ) + response.raise_for_status() + body = response.json() + return bool(body.get("valid", False)) diff --git a/examples/http/service_consumer_provider/conftest.py b/examples/http/service_consumer_provider/conftest.py new file mode 100644 index 000000000..0182939b6 --- /dev/null +++ b/examples/http/service_consumer_provider/conftest.py @@ -0,0 +1,37 @@ +""" +Shared PyTest configuration for the service-as-consumer/provider example. +""" + +from __future__ import annotations + +import contextlib +from pathlib import Path + +import pytest + +import pact_ffi + +EXAMPLE_DIR = Path(__file__).parent.resolve() + + +@pytest.fixture(scope="session") +def pacts_path() -> Path: + """ + Fixture for the Pact directory. + + Returns: + Path to the directory where Pact contract files are written. + """ + return EXAMPLE_DIR / "pacts" + + +@pytest.fixture(scope="session", autouse=True) +def _setup_pact_logging() -> None: + """ + Set up logging for the pact package. + """ + # If the logger is already configured (e.g., when running alongside other + # examples), log_to_stderr raises RuntimeError. We suppress it here so + # that the first configuration wins. + with contextlib.suppress(RuntimeError): + pact_ffi.log_to_stderr("INFO") diff --git a/examples/http/service_consumer_provider/frontend_client.py b/examples/http/service_consumer_provider/frontend_client.py new file mode 100644 index 000000000..093517ab7 --- /dev/null +++ b/examples/http/service_consumer_provider/frontend_client.py @@ -0,0 +1,98 @@ +""" +HTTP client representing `frontend-web` calling `user-service`. + +This module is intentionally free of any Pact dependency; it is production code. +The Pact dependency only appears in +[`test_consumer_frontend`][examples.http.service_consumer_provider.test_consumer_frontend], +which exercises this client against a Pact mock server to define the contract +between `frontend-web` (consumer) and +[`user_service`][examples.http.service_consumer_provider.user_service] +(provider). + +Notice that the +[`Account`][examples.http.service_consumer_provider.frontend_client.Account] +dataclass only captures the fields `frontend-web` cares about (`id`, `username`, +`status`). This is a deliberate illustration of how consumer-driven contracts +differ from testing an OpenAPI specification: the contract describes what *this +consumer* uses, not everything the provider exposes. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +import requests + + +@dataclass +class Account: + """ + Minimal account model as seen by `frontend-web`. + + This class intentionally reflects only the fields the frontend consumer + needs. It may differ from the internal representation in + [`user_service`][examples.http.service_consumer_provider.user_service], + which stores additional state. This asymmetry is expected and is a key + feature of consumer-driven contract testing. + """ + + id: int + username: str + status: str + + +class FrontendClient: + """ + HTTP client used by `frontend-web` to call `user-service`. + + This client is the consumer under test in + [`test_consumer_frontend`][examples.http.service_consumer_provider.test_consumer_frontend]. + Keeping it free of Pact dependencies means it can be used as-is in + production while the tests handle all contract verification. + """ + + def __init__(self, base_url: str) -> None: + """ + Initialise the frontend client. + + Args: + base_url: + Base URL of `user-service`, e.g. `http://user-service`. Trailing + slashes are stripped automatically. + """ + self._base_url = base_url.rstrip("/") + self._session = requests.Session() + + def create_account(self, username: str, password: str) -> Account: + """ + Create an account through `user-service`. + + Sends a `POST /accounts` request and deserialises the response into an + [`Account`][examples.http.service_consumer_provider.frontend_client.Account]. + Only the `id`, `username`, and `status` fields are read from the + response, and any additional fields returned by the provider are + ignored. + + Args: + username: + Desired username for the new account. + + password: + Password forwarded to `user-service`, which validates it against + `auth-service` before creating the account. + + Returns: + Account data returned by `user-service`. + + Raises: + requests.HTTPError: + If `user-service` responds with a non-2xx status (e.g., `401` + when credentials are rejected). + """ + response = self._session.post( + f"{self._base_url}/accounts", + json={"username": username, "password": password}, + ) + response.raise_for_status() + body = response.json() + return Account(id=body["id"], username=body["username"], status=body["status"]) diff --git a/examples/http/service_consumer_provider/pyproject.toml b/examples/http/service_consumer_provider/pyproject.toml new file mode 100644 index 000000000..96ece6ee1 --- /dev/null +++ b/examples/http/service_consumer_provider/pyproject.toml @@ -0,0 +1,26 @@ +#:schema https://www.schemastore.org/pyproject.json +[project] +name = "example-service-consumer-provider" + +description = "Example of a service acting as both a Pact consumer and provider" + +dependencies = ["fastapi~=0.0", "requests~=2.0", "typing-extensions~=4.0"] +requires-python = ">=3.10" +version = "1.0.0" + +[dependency-groups] + +test = ["pact-python", "pytest~=9.0", "uvicorn~=0.29"] + +[tool.uv.sources] +pact-python = { path = "../../../" } + +[tool.ruff] +extend = "../../../pyproject.toml" + +[tool.pytest] +addopts = ["--import-mode=importlib"] + +log_date_format = "%H:%M:%S" +log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" +log_level = "NOTSET" diff --git a/examples/http/service_consumer_provider/test_consumer_auth.py b/examples/http/service_consumer_provider/test_consumer_auth.py new file mode 100644 index 000000000..8bfdd6bbf --- /dev/null +++ b/examples/http/service_consumer_provider/test_consumer_auth.py @@ -0,0 +1,128 @@ +""" +Consumer contract tests for `user-service` → `auth-service`. + +This module defines the contract that +[`user_service`][examples.http.service_consumer_provider.user_service] (acting +as a *consumer*) expects `auth-service` (the *provider*) to honour. When these +tests run, Pact starts a mock server in place of `auth-service` and verifies +that +[`AuthClient`][examples.http.service_consumer_provider.auth_client.AuthClient] +makes exactly the requests specified here and can handle the responses. + +The generated Pact file (written to the `pacts/` directory) would normally be +published to a Pact Broker so that the `auth-service` team can run provider +verification against it. In this self-contained example the file is consumed +locally by the provider verification tests. + +For background on consumer testing, see the [Pact consumer test +guide](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-consumer-pact-test). +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from examples.http.service_consumer_provider.auth_client import AuthClient +from pact import Pact + +if TYPE_CHECKING: + from collections.abc import Generator + from pathlib import Path + + +@pytest.fixture +def pact(pacts_path: Path) -> Generator[Pact, None, None]: + """ + Pact fixture for `user-service` as consumer of `auth-service`. + + Creates a V4 Pact between `user-service` (consumer) and `auth-service` + (provider). Each test in this module can define one or more expected + interactions on the returned `Pact` object; the mock provider will validate + that the consumer code sends exactly those requests and handles the + responses correctly. After the test, the contract is written to *pacts_path* + for use in provider verification. + + Args: + pacts_path: + Directory where the generated Pact file is written. + + Yields: + Pact configured for `user-service` → `auth-service`. + """ + pact = Pact("user-service", "auth-service").with_specification("V4") + yield pact + pact.write_file(pacts_path) + + +@pytest.mark.parametrize( + ("password", "expected_valid"), + [ + pytest.param("correct-horse-battery-staple", True, id="valid"), + pytest.param("wrong-password", False, id="invalid"), + ], +) +def test_validate_credentials( + pact: Pact, + password: str, + *, + expected_valid: bool, +) -> None: + """ + Verify the `AuthClient` contract for both valid and invalid credentials. + + This parametrised test covers two interactions in a single contract: + + - **Valid credentials**: `auth-service` responds `{"valid": true}`, and + [`AuthClient.validate_credentials`][examples.http.service_consumer_provider.auth_client.AuthClient.validate_credentials] + returns `True`. + - **Invalid credentials**: `auth-service` responds `{"valid": false}`, and + `AuthClient.validate_credentials` returns `False`. + + Both cases map to the same endpoint (`POST /auth/validate`) but are modelled + as separate Pact interactions with different provider states. This ensures + that `auth-service` must support both outcomes, not just the happy path. + + Args: + pact: + Pact fixture for `user-service` → `auth-service`. + + password: + Password sent to `auth-service`; determines which provider state + (and therefore which mock response) is used. + + expected_valid: + The validation result the consumer expects to receive and act on. + """ + state = ( + "user credentials are valid" + if expected_valid + else "user credentials are invalid" + ) + + ( + pact + .upon_receiving(f"Credential validation for {state}") + .given(state) + .with_request("POST", "/auth/validate") + .with_body( + { + "username": "alice", + "password": password, + }, + content_type="application/json", + ) + .will_respond_with(200) + .with_body( + { + "valid": expected_valid, + "subject": "alice", + }, + content_type="application/json", + ) + ) + + with pact.serve() as srv: + client = AuthClient(str(srv.url)) + assert client.validate_credentials("alice", password) is expected_valid diff --git a/examples/http/service_consumer_provider/test_consumer_frontend.py b/examples/http/service_consumer_provider/test_consumer_frontend.py new file mode 100644 index 000000000..e7ded130d --- /dev/null +++ b/examples/http/service_consumer_provider/test_consumer_frontend.py @@ -0,0 +1,144 @@ +""" +Consumer contract tests for `frontend-web` → `user-service`. + +This module defines the contract that `frontend-web` (acting as a *consumer*) +expects [`user_service`][examples.http.service_consumer_provider.user_service] +(the *provider*) to honour. When these tests run, Pact starts a mock server in +place of `user-service` and verifies that +[`FrontendClient`][examples.http.service_consumer_provider.frontend_client.FrontendClient] +makes exactly the requests specified here and can handle the responses. + +The generated Pact file is used by the provider verification test in +[`test_provider`][examples.http.service_consumer_provider.test_provider], which +runs the real `user-service` and replays these interactions against it. + +For background on consumer testing, see the [Pact consumer test +guide](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-consumer-pact-test). +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +import requests + +from examples.http.service_consumer_provider.frontend_client import FrontendClient +from pact import Pact, match + +if TYPE_CHECKING: + from collections.abc import Generator + from pathlib import Path + + +@pytest.fixture +def pact(pacts_path: Path) -> Generator[Pact, None, None]: + """ + Pact fixture for `frontend-web` as consumer of `user-service`. + + Creates a V4 Pact between `frontend-web` (consumer) and `user-service` + (provider). Each test in this module defines one or more expected + interactions on the returned `Pact` object; Pact validates that + `FrontendClient` sends exactly those requests and handles the responses + correctly. After the test, the contract is written to *pacts_path* for + provider verification. + + Args: + pacts_path: + Directory where the generated Pact file is written. + + Yields: + Pact configured for `frontend-web` → `user-service`. + """ + pact = Pact("frontend-web", "user-service").with_specification("V4") + yield pact + pact.write_file(pacts_path) + + +def test_create_account_success(pact: Pact) -> None: + """ + Verify `FrontendClient` behaviour when credentials are valid. + + This test defines the happy-path interaction: `frontend-web` POSTs an + account creation request with valid credentials, and `user-service` responds + with `201 Created` and the new account details. + + Note the use of `match.int(1001)` for the `id` field. This tells Pact to + verify that the field *type* is an integer, not that the value is exactly + `1001`. This makes the contract resilient to auto-incremented IDs while + still ensuring the consumer receives a numeric identifier it can work with. + + The provider state `"auth accepts credentials"` signals to the provider + verification test (see + [`test_provider`][examples.http.service_consumer_provider.test_provider]) + that it must configure a stub `auth-service` that accepts the supplied + credentials. + """ + ( + pact + .upon_receiving("A request to create an account") + .given("auth accepts credentials") + .with_request("POST", "/accounts") + .with_body( + { + "username": "alice", + "password": "correct-horse-battery-staple", + }, + content_type="application/json", + ) + .will_respond_with(201) + .with_body( + { + "id": match.int(1001), + "username": "alice", + "status": "created", + }, + content_type="application/json", + ) + ) + + with pact.serve() as srv: + client = FrontendClient(str(srv.url)) + account = client.create_account("alice", "correct-horse-battery-staple") + assert account.id == 1001 + assert account.username == "alice" + assert account.status == "created" + + +def test_create_account_invalid_credentials(pact: Pact) -> None: + """ + Verify `FrontendClient` behaviour when credentials are invalid. + + This test defines the failure-path interaction: `frontend-web` POSTs an + account creation request with invalid credentials, and `user-service` + responds with `401 Unauthorized`. The consumer is expected to propagate the + error as a `requests.HTTPError`. + + Testing error paths in Pact contracts is important: it ensures the provider + contract covers not just the happy path but also the error responses that + consumers must handle gracefully. + + The provider state `"auth rejects credentials"` signals to the provider + verification test that the stub `auth-service` must reject the supplied + credentials. + """ + ( + pact + .upon_receiving("A request with invalid credentials") + .given("auth rejects credentials") + .with_request("POST", "/accounts") + .with_body( + { + "username": "alice", + "password": "wrong-password", + }, + content_type="application/json", + ) + .will_respond_with(401) + .with_body({"detail": "Invalid credentials"}, content_type="application/json") + ) + + with pact.serve() as srv: + client = FrontendClient(str(srv.url)) + with pytest.raises(requests.HTTPError): + client.create_account("alice", "wrong-password") diff --git a/examples/http/service_consumer_provider/test_provider.py b/examples/http/service_consumer_provider/test_provider.py new file mode 100644 index 000000000..cea79b7cc --- /dev/null +++ b/examples/http/service_consumer_provider/test_provider.py @@ -0,0 +1,218 @@ +""" +Provider verification for `user-service` against the `frontend-web` contract. + +This module runs the Pact verifier against the real +[`user_service`][examples.http.service_consumer_provider.user_service] FastAPI +application to confirm that it honours the contract defined by the consumer +tests in +[`test_consumer_frontend`][examples.http.service_consumer_provider.test_consumer_frontend]. + +## How provider verification works + +Pact replays each interaction from the contract file against the running +`user-service`. Before each interaction it calls the appropriate *provider state +handler* to put the service in the right state. For example, the interaction `"A +request to create an account"` requires the state `"auth accepts credentials"`, +so Pact calls `set_auth_accepts` first, which installs a +[`StubAuthVerifier`][examples.http.service_consumer_provider.test_provider.StubAuthVerifier] +that always returns `True`. + +This lets the entire `user-service` run for real while still being independent +of a live `auth-service`. The +[`CredentialsVerifier`][examples.http.service_consumer_provider.user_service.CredentialsVerifier] +protocol in `user_service.py` is the seam that makes this possible. + +For more background, see the [Pact provider test +guide](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-provider-pact-test) +and the documentation for +[`Verifier.state_handler`][pact.verifier.Verifier.state_handler]. +""" + +from __future__ import annotations + +import contextlib +import time +from threading import Thread +from typing import TYPE_CHECKING + +import pytest +import requests +import uvicorn + +import pact._util +from examples.http.service_consumer_provider.user_service import ( + app, + reset_state, + set_auth_verifier, +) +from pact import Verifier + +if TYPE_CHECKING: + from pathlib import Path + + +class StubAuthVerifier: + """ + In-process stub for `auth-service`, used by provider state handlers. + + Rather than starting a real `auth-service` during provider verification, the + tests replace the + [`AuthClient`][examples.http.service_consumer_provider.auth_client.AuthClient] + with this stub via + [`set_auth_verifier`][examples.http.service_consumer_provider.user_service.set_auth_verifier]. + The stub satisfies the + [`CredentialsVerifier`][examples.http.service_consumer_provider.user_service.CredentialsVerifier] + protocol and returns a fixed result for every call, making provider states + simple and deterministic. + + The real `AuthClient` behaviour is separately verified by the consumer tests + in + [`test_consumer_auth`][examples.http.service_consumer_provider.test_consumer_auth]. + """ + + def __init__(self, *, valid: bool) -> None: + """ + Create a stub verifier. + + Args: + valid: + Result to return for all validations. + """ + self._valid = valid + + def validate_credentials(self, username: str, password: str) -> bool: + """ + Validate credentials. + + Args: + username: + Ignored in this stub. + + password: + Ignored in this stub. + + Returns: + The configured validation result. + """ + del username, password + return self._valid + + +@pytest.fixture(scope="session") +def app_server() -> str: + """ + Start the `user-service` FastAPI application for provider verification. + + Launches the application in a daemon thread so it is torn down when the test + process exits. The fixture polls the `/docs` endpoint until the server is + accepting connections (up to 5 seconds), which avoids race conditions when + the verifier immediately begins replaying interactions. + + Returns: + Base URL of the running `user-service`, e.g. `http://localhost:54321`. + """ + hostname = "localhost" + port = pact._util.find_free_port() # noqa: SLF001 + Thread( + target=uvicorn.run, + args=(app,), + kwargs={"host": hostname, "port": port}, + daemon=True, + ).start() + + base_url = f"http://{hostname}:{port}" + for _ in range(50): + with contextlib.suppress(requests.RequestException): + response = requests.get(f"{base_url}/docs", timeout=0.2) + if response.status_code < 500: + return base_url + time.sleep(0.1) + + msg = f"user-service did not start at {base_url}" + raise RuntimeError(msg) + + +def set_auth_accepts(parameters: dict[str, object] | None = None) -> None: + """ + Provider state: `auth-service` accepts credentials. + + Configures `user-service` so that any credential validation attempt + succeeds. This models the scenario where the upstream `auth-service` + considers the supplied credentials valid, allowing account creation to + proceed normally. + + Called by the Pact verifier before interactions that carry the provider + state `"auth accepts credentials"`. + + Args: + parameters: + Optional Pact state parameters. Not used by this state. + """ + del parameters + reset_state() + set_auth_verifier(StubAuthVerifier(valid=True)) + + +def set_auth_rejects(parameters: dict[str, object] | None = None) -> None: + """ + Provider state: `auth-service` rejects credentials. + + Configures `user-service` so that any credential validation attempt fails. + This models the scenario where the upstream `auth-service` considers the + supplied credentials invalid, causing `user-service` to return `401 + Unauthorized`. + + Called by the Pact verifier before interactions that carry the provider + state `"auth rejects credentials"`. + + Args: + parameters: + Optional Pact state parameters. Not used by this state. + """ + del parameters + reset_state() + set_auth_verifier(StubAuthVerifier(valid=False)) + + +def test_provider(app_server: str, pacts_path: Path) -> None: + """ + Verify `user-service` against the `frontend-web` consumer contract. + + This test uses the Pact verifier to replay each interaction from the + contract generated by + [`test_consumer_frontend`][examples.http.service_consumer_provider.test_consumer_frontend] + against the running `user-service`. Before each interaction, the verifier + calls the appropriate provider state handler to configure the service. After + all interactions have been replayed, Pact reports any mismatches. + + Provider state handlers are the mechanism Pact uses to decouple verification + from infrastructure: instead of wiring up a real `auth-service`, each state + handler installs a `StubAuthVerifier` that returns a predetermined result. + This makes verification fast, deterministic, and free of external + dependencies. + + Note that `teardown=False` is set on the state handler because the handlers + use `reset_state()` at the *start* of each setup call. Explicit teardown is + unnecessary when the next setup always resets to a clean slate. + + Args: + app_server: + Base URL of the running `user-service` provider. + + pacts_path: + Directory containing the Pact contract files to verify against. + """ + verifier = ( + Verifier("user-service") + .add_source(pacts_path) + .add_transport(url=app_server) + .state_handler( + { + "auth accepts credentials": set_auth_accepts, + "auth rejects credentials": set_auth_rejects, + }, + teardown=False, + ) + ) + + verifier.verify() diff --git a/examples/http/service_consumer_provider/user_service.py b/examples/http/service_consumer_provider/user_service.py new file mode 100644 index 000000000..8d144fb43 --- /dev/null +++ b/examples/http/service_consumer_provider/user_service.py @@ -0,0 +1,195 @@ +""" +FastAPI service acting as both a Pact consumer and a Pact provider. + +This module is the centrepiece of the example. `user-service` sits in the middle +of a two-hop request path: + +```text +frontend-web → user-service → auth-service +``` + +This means it plays two Pact roles simultaneously: + +- **Provider** of the `POST /accounts` endpoint consumed by `frontend-web`. The + provider verification test lives in + [`test_provider`][examples.http.service_consumer_provider.test_provider]. + +- **Consumer** of `auth-service`'s `POST /auth/validate` endpoint. The consumer + contract test lives in + [`test_consumer_auth`][examples.http.service_consumer_provider.test_consumer_auth]. + +## Testability design + +Provider verification requires the service to be started as a real HTTP server. +To avoid needing a real `auth-service` during those tests, this module uses the +[`CredentialsVerifier`][examples.http.service_consumer_provider.user_service.CredentialsVerifier] +protocol as a seam. In production the seam is filled by +[`AuthClient`][examples.http.service_consumer_provider.auth_client.AuthClient]; +in tests it is replaced with a +[`StubAuthVerifier`][examples.http.service_consumer_provider.test_provider.StubAuthVerifier] +via +[`set_auth_verifier`][examples.http.service_consumer_provider.user_service.set_auth_verifier]. + +This avoids mocking at the HTTP level. The real FastAPI application runs, and +only the collaborator that calls `auth-service` is swapped out. + +## Module-level state + +[`SERVICE_STATE`][examples.http.service_consumer_provider.user_service.SERVICE_STATE] +and +[`ACCOUNT_STORE`][examples.http.service_consumer_provider.user_service.ACCOUNT_STORE] +are intentional module-level globals. Because provider state handlers in Pact +run in the same process as the application, these globals are the simplest way +for the test harness to reconfigure the service between interactions without an +additional HTTP endpoint. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol + +from fastapi import FastAPI, HTTPException, status +from pydantic import BaseModel + +from examples.http.service_consumer_provider.auth_client import AuthClient + + +class CredentialsVerifier(Protocol): + """ + Behaviour required for credential verification. + """ + + def validate_credentials(self, username: str, password: str) -> bool: + """ + Validate credentials. + """ + + +@dataclass +class UserAccount: + """ + Stored account record. + """ + + id: int + username: str + + +class InMemoryAccountStore: + """ + Small in-memory store for example purposes. + """ + + def __init__(self) -> None: + """ + Initialise the in-memory store. + """ + self._next_id = 1 + self._accounts: dict[int, UserAccount] = {} + + def create(self, username: str) -> UserAccount: + """ + Create and store a new account. + + Args: + username: + Username for the new account. + + Returns: + The created account. + """ + account = UserAccount(id=self._next_id, username=username) + self._accounts[account.id] = account + self._next_id += 1 + return account + + def reset(self) -> None: + """ + Reset all stored accounts. + """ + self._next_id = 1 + self._accounts.clear() + + +class CreateAccountRequest(BaseModel): + """ + Request payload used by frontend-web. + """ + + username: str + password: str + + +class CreateAccountResponse(BaseModel): + """ + Response payload returned to frontend-web. + """ + + id: int + username: str + status: str = "created" + + +ACCOUNT_STORE = InMemoryAccountStore() + + +class _ServiceState: + """ + Mutable state used by provider-state handlers in tests. + """ + + def __init__(self) -> None: + """ + Initialise default collaborators. + """ + self.auth_verifier: CredentialsVerifier = AuthClient("http://auth-service") + + +SERVICE_STATE = _ServiceState() + +app = FastAPI() + + +def set_auth_verifier(verifier: CredentialsVerifier) -> None: + """ + Replace the auth verifier implementation. + + Args: + verifier: + New verifier implementation. + """ + SERVICE_STATE.auth_verifier = verifier + + +def reset_state() -> None: + """ + Reset internal provider state. + """ + ACCOUNT_STORE.reset() + + +@app.post("/accounts", status_code=status.HTTP_201_CREATED) +async def create_account(payload: CreateAccountRequest) -> CreateAccountResponse: + """ + Create an account after validating credentials with auth-service. + + Args: + payload: + Account request payload. + + Returns: + Created account response. + + Raises: + HTTPException: + If credentials are invalid. + """ + if not SERVICE_STATE.auth_verifier.validate_credentials( + payload.username, + payload.password, + ): + raise HTTPException(status_code=401, detail="Invalid credentials") + + account = ACCOUNT_STORE.create(payload.username) + return CreateAccountResponse(id=account.id, username=account.username) diff --git a/tests/compatibility_suite/definition b/tests/compatibility_suite/definition index b03375f00..1acfa1ecb 160000 --- a/tests/compatibility_suite/definition +++ b/tests/compatibility_suite/definition @@ -1 +1 @@ -Subproject commit b03375f00f5adf1346bd76edb0c7968cc0b495cf +Subproject commit 1acfa1ecbd9d63e4465c687b3cdd7e0d3ac5811c From 3d726294a4d6976dc48d3b8dd0c421cf0998d2bc Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 14 Apr 2026 19:19:59 +1000 Subject: [PATCH 1320/1376] chore: remove versioningit, switch to static version in pyproject.toml Signed-off-by: JP-Ellis --- .gitignore | 3 --- pact-python-cli/.gitignore | 1 - pact-python-cli/pyproject.toml | 29 ++------------------- pact-python-cli/src/pact_cli/__version__.py | 10 +++++++ pact-python-ffi/.gitignore | 1 - pact-python-ffi/pyproject.toml | 29 ++------------------- pact-python-ffi/src/pact_ffi/__version__.py | 10 +++++++ pyproject.toml | 29 ++------------------- src/pact/__version__.py | 10 +++++++ 9 files changed, 36 insertions(+), 86 deletions(-) create mode 100644 pact-python-cli/src/pact_cli/__version__.py create mode 100644 pact-python-ffi/src/pact_ffi/__version__.py create mode 100644 src/pact/__version__.py diff --git a/.gitignore b/.gitignore index bda0103d6..761f69890 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,6 @@ # Test outputs examples/tests/pacts -# Version is determined from the VCS -src/pact/__version__.py - # Wheels from CIBuildWheel wheelhouse/ diff --git a/pact-python-cli/.gitignore b/pact-python-cli/.gitignore index e9472099d..ff74a364b 100644 --- a/pact-python-cli/.gitignore +++ b/pact-python-cli/.gitignore @@ -1,4 +1,3 @@ -src/pact_cli/__version__.py src/pact_cli/bin src/pact_cli/data src/pact_cli/lib diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 0410ef9da..fb8ce0569 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -2,8 +2,7 @@ [project] description = "Pact CLI bundle for Python" name = "pact-python-cli" - -dynamic = ["version"] +version = "2.5.7.0" keywords = ["pact", "cli", "pact-python", "contract-testing"] license = "MIT" readme = "README.md" @@ -63,15 +62,11 @@ types = ["mypy==1.20.1"] ################################################################################ [build-system] build-backend = "hatchling.build" -requires = ["hatchling", "packaging", "versioningit"] +requires = ["hatchling", "packaging"] [tool.hatch] - [tool.hatch.version] - source = "versioningit" - [tool.hatch.build] - artifacts = ["src/pact_cli/__version__.py"] packages = ["src/pact_cli"] [tool.hatch.build.targets.wheel.hooks.custom] @@ -114,26 +109,6 @@ requires = ["hatchling", "packaging", "versioningit"] [[tool.hatch.envs.test.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.14"] -################################################################################ -## Versioningit Configuration -################################################################################ -[tool.versioningit] - [tool.versioningit.vcs] - match = ["pact-python-cli/*"] - method = "git" - - [tool.versioningit.tag2version] - rmprefix = "pact-python-cli/" - - [tool.versioningit.write] - file = "src/pact_cli/__version__.py" - template = '''\ -# This file is auto-generated by versioningit. Do not edit. -__version__ = "{version}" -__version_tuple__ = {version_tuple} -__commit_id__ = "{revision}" -''' - ################################################################################ ## PyTest Configuration ################################################################################ diff --git a/pact-python-cli/src/pact_cli/__version__.py b/pact-python-cli/src/pact_cli/__version__.py new file mode 100644 index 000000000..9ecfb7454 --- /dev/null +++ b/pact-python-cli/src/pact_cli/__version__.py @@ -0,0 +1,10 @@ +"""Version information for pact-python-cli.""" + +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("pact-python-cli") +except PackageNotFoundError: + __version__ = "unknown" + +__version_tuple__ = tuple(int(x) for x in __version__.split(".") if x.isdigit()) diff --git a/pact-python-ffi/.gitignore b/pact-python-ffi/.gitignore index f3039de16..48af8469c 100644 --- a/pact-python-ffi/.gitignore +++ b/pact-python-ffi/.gitignore @@ -1,5 +1,4 @@ src/pact_ffi/data -src/pact_ffi/__version__.py src/pact_ffi/*.pyd src/pact_ffi/*.so src/pact_ffi/*.dylib diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 3be085cdb..35dc65a8d 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -2,8 +2,7 @@ [project] description = "Python bindings for the Pact FFI library" name = "pact-python-ffi" - -dynamic = ["version"] +version = "0.4.28.2" keywords = ["pact", "ffi", "pact-python", "contract-testing"] license = "MIT" readme = "README.md" @@ -50,15 +49,11 @@ types = ["mypy==1.20.1", "typing-extensions~=4.0"] ################################################################################ [build-system] build-backend = "hatchling.build" -requires = ["hatchling", "packaging", "cffi", "setuptools", "versioningit"] +requires = ["hatchling", "packaging", "cffi", "setuptools"] [tool.hatch] - [tool.hatch.version] - source = "versioningit" - [tool.hatch.build] - artifacts = ["src/pact_ffi/__version__.py"] packages = ["src/pact_ffi"] [tool.hatch.build.targets.wheel.hooks.custom] @@ -108,26 +103,6 @@ requires = ["hatchling", "packaging", "cffi", "setuptools", "versioningit"] [[tool.hatch.envs.test.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.14"] -################################################################################ -## Versioningit Configuration -################################################################################ -[tool.versioningit] - [tool.versioningit.vcs] - match = ["pact-python-ffi/*"] - method = "git" - - [tool.versioningit.tag2version] - rmprefix = "pact-python-ffi/" - - [tool.versioningit.write] - file = "src/pact_ffi/__version__.py" - template = '''\ -# This file is auto-generated by versioningit. Do not edit. -__version__ = "{version}" -__version_tuple__ = {version_tuple} -__commit_id__ = "{revision}" -''' - ################################################################################ ## PyTest Configuration ################################################################################ diff --git a/pact-python-ffi/src/pact_ffi/__version__.py b/pact-python-ffi/src/pact_ffi/__version__.py new file mode 100644 index 000000000..bb75cdc87 --- /dev/null +++ b/pact-python-ffi/src/pact_ffi/__version__.py @@ -0,0 +1,10 @@ +"""Version information for pact-python-ffi.""" + +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("pact-python-ffi") +except PackageNotFoundError: + __version__ = "unknown" + +__version_tuple__ = tuple(int(x) for x in __version__.split(".") if x.isdigit()) diff --git a/pyproject.toml b/pyproject.toml index e79229bec..b5ae3f8bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,8 +2,7 @@ [project] description = "Tool for creating and verifying consumer-driven contracts using the Pact framework." name = "pact-python" - -dynamic = ["version"] +version = "3.2.1" keywords = ["contract-testing", "pact", "testing"] license = { file = "LICENSE" } readme = "README.md" @@ -156,15 +155,11 @@ test-v2 = [ ################################################################################ [build-system] build-backend = "hatchling.build" -requires = ["hatchling", "versioningit"] +requires = ["hatchling"] [tool.hatch] - [tool.hatch.version] - source = "versioningit" - [tool.hatch.build] - artifacts = ["src/pact/__version__.py"] packages = ["src/pact"] ######################################## @@ -254,26 +249,6 @@ requires = ["hatchling", "versioningit"] [[tool.hatch.envs.v2-example.matrix]] python = ["3.10", "3.11", "3.12", "3.13", "3.14"] -################################################################################ -## Versioningit Configuration -################################################################################ -[tool.versioningit] - [tool.versioningit.vcs] - match = ["pact-python/*"] - method = "git" - - [tool.versioningit.tag2version] - rmprefix = "pact-python/" - - [tool.versioningit.write] - file = "src/pact/__version__.py" - template = '''\ -# This file is auto-generated by versioningit. Do not edit. -__version__ = "{version}" -__version_tuple__ = {version_tuple} -__commit_id__ = "{revision}" -''' - ################################################################################ ## UV Workspace ################################################################################ diff --git a/src/pact/__version__.py b/src/pact/__version__.py new file mode 100644 index 000000000..3b6fe0250 --- /dev/null +++ b/src/pact/__version__.py @@ -0,0 +1,10 @@ +"""Version information for pact-python.""" + +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("pact-python") +except PackageNotFoundError: + __version__ = "unknown" + +__version_tuple__ = tuple(int(x) for x in __version__.split(".") if x.isdigit()) From cbba1c92937bf7df8d56eb5a58a7b74ecfb5f186 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 14 Apr 2026 19:39:32 +1000 Subject: [PATCH 1321/1376] chore: add release script Move the release logic away from tags, and instead have a script to help manage/automate this. Primarily inspired by Rust's `release-plz`, but with some tweaks for the wrapper packages. Signed-off-by: JP-Ellis --- .github/workflows/build-cli.yml | 212 --------- .github/workflows/build-ffi.yml | 213 --------- .github/workflows/build.yml | 179 -------- .github/workflows/release-cli.yml | 280 ++++++++++++ .github/workflows/release-core.yml | 238 ++++++++++ .github/workflows/release-ffi.yml | 281 ++++++++++++ docs/releases.md | 77 +++- scripts/release.py | 687 +++++++++++++++++++++++++++++ 8 files changed, 1539 insertions(+), 628 deletions(-) delete mode 100644 .github/workflows/build-cli.yml delete mode 100644 .github/workflows/build-ffi.yml delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release-cli.yml create mode 100644 .github/workflows/release-core.yml create mode 100644 .github/workflows/release-ffi.yml create mode 100755 scripts/release.py diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml deleted file mode 100644 index 5e632d053..000000000 --- a/.github/workflows/build-cli.yml +++ /dev/null @@ -1,212 +0,0 @@ ---- -name: build cli - -on: - push: - tags: - - pact-python-cli/* - branches: - - main - pull_request: - branches: - - main - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -env: - STABLE_PYTHON_VERSION: '310' - HATCH_VERBOSE: '1' - FORCE_COLOR: '1' - CIBW_BUILD_FRONTEND: build - -jobs: - complete: - name: Build CLI completion check - if: always() - - permissions: - contents: none - - runs-on: ubuntu-latest - needs: - - build-sdist - - build-wheels - - publish - - steps: - - name: Failed - run: exit 1 - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') - - build-sdist: - name: Build CLI source distribution - - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - with: - enable-cache: true - - - name: Install Python - run: uv python install ${{ env.STABLE_PYTHON_VERSION }} - - - name: Install hatch - run: uv tool install hatch - - - name: Create source distribution - working-directory: pact-python-cli - run: hatch build --target sdist - - - name: Upload sdist - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: wheels-sdist - path: pact-python-cli/dist/*.tar* - if-no-files-found: error - compression-level: 0 - - build-wheels: - name: Build CLI wheels on ${{ matrix.os }} - - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: macos-15-intel - - os: macos-latest - - os: ubuntu-24.04-arm - - os: ubuntu-latest - # - os: windows-11-arm # Not supported upstream - - os: windows-latest - - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Create wheels - uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 - with: - package-dir: pact-python-cli - env: - CIBW_BUILD: cp${{ env.STABLE_PYTHON_VERSION }}-* - - - name: Upload wheels - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: wheels-${{ matrix.os }} - path: wheelhouse/*.whl - if-no-files-found: error - compression-level: 0 - - publish: - name: Publish CLI wheels and sdist - - if: >- - github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/pact-python-cli/') - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/pact-python-cli - - needs: - - build-sdist - - build-wheels - - permissions: - # Required for creating the release - contents: write - # Required for trusted publishing - id-token: write - - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Install git cliff and typos - uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 - with: - tool: git-cliff,typos - - - name: Update changelog - run: git cliff --verbose - working-directory: pact-python-cli - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - - name: Generate release changelog - id: release-changelog - working-directory: pact-python-cli - run: | - if ! git cliff \ - --current \ - --strip header \ - --output ${{ runner.temp }}/release-changelog.md; then - { - echo "> [!WARNING]" - echo ">" - echo "> No changelog generated. To be filled in." - } > ${{ runner.temp }}/release-changelog.md - fi - - { - echo "" - echo "
" - echo "" - echo "" - echo "## Pull Requests" - echo "" - echo "" - echo "" - } >> ${{ runner.temp }}/release-changelog.md - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - - name: Download wheels and sdist - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - path: wheelhouse - merge-multiple: true - - - name: Generate release - id: release - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 - with: - files: wheelhouse/* - body_path: ${{ runner.temp }}/release-changelog.md - draft: false - prerelease: false - generate_release_notes: true - - - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - with: - skip-existing: true - packages-dir: wheelhouse - - - name: Create PR for changelog update - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - token: ${{ secrets.GH_TOKEN }} - commit-message: 'docs: update changelog for ${{ github.ref_name }}' - title: 'docs: update cli changelog' - body: | - This PR updates the changelog for ${{ github.ref_name }}. - branch: docs/update-changelog - base: main diff --git a/.github/workflows/build-ffi.yml b/.github/workflows/build-ffi.yml deleted file mode 100644 index 32d9008bd..000000000 --- a/.github/workflows/build-ffi.yml +++ /dev/null @@ -1,213 +0,0 @@ ---- -name: build ffi - -on: - push: - tags: - - pact-python-ffi/* - branches: - - main - pull_request: - branches: - - main - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -env: - STABLE_PYTHON_VERSION: '310' - HATCH_VERBOSE: '1' - FORCE_COLOR: '1' - CIBW_BUILD_FRONTEND: build - -jobs: - complete: - name: Build FFI completion check - if: always() - - permissions: - contents: none - - runs-on: ubuntu-latest - needs: - - build-sdist - - build-wheels - - publish - - steps: - - name: Failed - run: exit 1 - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') - - build-sdist: - name: Build FFI source distribution - - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - with: - enable-cache: true - - - name: Install Python - run: uv python install ${{ env.STABLE_PYTHON_VERSION }} - - - name: Install hatch - run: uv tool install hatch - - - name: Create source distribution - working-directory: pact-python-ffi - run: hatch build --target sdist - - - name: Upload sdist - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: wheels-sdist - path: pact-python-ffi/dist/*.tar* - if-no-files-found: error - compression-level: 0 - - build-wheels: - name: Build FFI wheels on ${{ matrix.os }} - - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: macos-15-intel - - os: macos-latest - - os: ubuntu-24.04-arm - - os: ubuntu-latest - - os: windows-11-arm - - os: windows-latest - - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Create wheels - uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 - with: - package-dir: pact-python-ffi - env: - CIBW_BUILD: cp${{ env.STABLE_PYTHON_VERSION }}-* - HATCH_VERBOSE: '1' - - - name: Upload wheels - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: wheels-${{ matrix.os }} - path: wheelhouse/*.whl - if-no-files-found: error - compression-level: 0 - - publish: - name: Publish FFI wheels and sdist - - if: >- - github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/pact-python-ffi/') - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/pact-python-ffi - - needs: - - build-sdist - - build-wheels - - permissions: - # Required for creating the release - contents: write - # Required for trusted publishing - id-token: write - - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Install git cliff and typos - uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 - with: - tool: git-cliff,typos - - - name: Update changelog - run: git cliff --verbose - working-directory: pact-python-ffi - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - - name: Generate release changelog - id: release-changelog - working-directory: pact-python-ffi - run: | - if ! git cliff \ - --current \ - --strip header \ - --output ${{ runner.temp }}/release-changelog.md ; then - { - echo "> [!WARNING]" - echo ">" - echo "> No changelog generated. To be filled in." - } > ${{ runner.temp }}/release-changelog.md - fi - - { - echo "" - echo "
" - echo "" - echo "" - echo "## Pull Requests" - echo "" - echo "" - echo "" - } >> ${{ runner.temp }}/release-changelog.md - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - - name: Download wheels and sdist - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - path: wheelhouse - merge-multiple: true - - - name: Generate release - id: release - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 - with: - files: wheelhouse/* - body_path: ${{ runner.temp }}/release-changelog.md - draft: false - prerelease: false - generate_release_notes: true - - - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - with: - skip-existing: true - packages-dir: wheelhouse - - - name: Create PR for changelog update - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - token: ${{ secrets.GH_TOKEN }} - commit-message: 'docs: update changelog for ${{ github.ref_name }}' - title: 'docs: update ffi changelog' - body: | - This PR updates the changelog for ${{ github.ref_name }}. - branch: docs/update-changelog - base: main diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index fe0f64b75..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,179 +0,0 @@ ---- -name: build - -on: - push: - tags: - - pact-python/* - branches: - - main - pull_request: - branches: - - main - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -env: - STABLE_PYTHON_VERSION: '310' - HATCH_VERBOSE: '1' - FORCE_COLOR: '1' - CIBW_BUILD_FRONTEND: build - -jobs: - complete: - name: Build completion check - if: always() - - permissions: - contents: none - - runs-on: ubuntu-latest - needs: - - build - - publish - - steps: - - name: Failed - run: exit 1 - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') - - build: - name: Build source distribution and wheel - - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - with: - enable-cache: true - - - name: Install Python - run: uv python install ${{ env.STABLE_PYTHON_VERSION }} - - - name: Install hatch - run: uv tool install hatch - - - name: Create source distribution and wheel - run: hatch build - - - name: Upload sdist - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: wheels-sdist - path: ./dist/*.tar* - if-no-files-found: error - compression-level: 0 - - - name: Upload wheel - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: wheels-whl - path: ./dist/*.whl - if-no-files-found: error - compression-level: 0 - - publish: - name: Publish wheels and sdist - - if: >- - github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/pact-python/') - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/pact-python - - needs: - - build - - permissions: - # Required for creating the release - contents: write - # Required for trusted publishing - id-token: write - - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Install git cliff and typos - uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 - with: - tool: git-cliff,typos - - - name: Update changelog - run: git cliff --verbose - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - - name: Generate release changelog - id: release-changelog - run: | - if ! git cliff \ - --current \ - --strip header \ - --output ${{ runner.temp }}/release-changelog.md; then - { - echo "> [!WARNING]" - echo ">" - echo "> No changelog generated. To be filled in." - } > ${{ runner.temp }}/release-changelog.md - fi - - { - echo "" - echo "
" - echo "" - echo "" - echo "## Pull Requests" - echo "" - echo "" - echo "" - } >> ${{ runner.temp }}/release-changelog.md - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - - name: Download wheels and sdist - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - path: wheelhouse - merge-multiple: true - - - name: Generate release - id: release - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 - with: - files: wheelhouse/* - body_path: ${{ runner.temp }}/release-changelog.md - draft: false - prerelease: false - generate_release_notes: true - - - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - with: - skip-existing: true - packages-dir: wheelhouse - - - name: Create PR for changelog update - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - token: ${{ secrets.GH_TOKEN }} - commit-message: 'docs: update changelog for ${{ github.ref_name }}' - title: 'docs: update changelog' - body: | - This PR updates the changelog for ${{ github.ref_name }}. - branch: docs/update-changelog - base: main diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml new file mode 100644 index 000000000..85ecfbd81 --- /dev/null +++ b/.github/workflows/release-cli.yml @@ -0,0 +1,280 @@ +--- +# Release workflow for pact-python-cli (CLI wrapper package). +# +# This package tracks the upstream pact-foundation/pact-standalone project and +# versions itself as `{upstream_version}.{N}` (e.g. `2.4.0.0`). The release +# lifecycle runs in three stages: +# +# Stage 1: Prepare (trigger: push to main) +# The `prepare` job runs `scripts/release.py prepare cli`, which fetches the +# latest `v*` release from pact-foundation/pact-standalone, computes the next +# wrapper version, updates pyproject.toml and CHANGELOG.md, and force-pushes +# those changes to the fixed branch `release/pact-python-cli`. It then +# creates (or updates the title and body of) the release PR targeting main. +# +# Stage 2: Tag (trigger: release PR merged → `closed` event, merged == true) +# When the release PR on `release/pact-python-cli` is merged, the `tag` job +# runs `scripts/release.py tag cli`, which reads the version from +# pyproject.toml and pushes a git tag of the form `pact-python-cli/X.Y.Z.N`. +# +# Stage 3: Publish (trigger: tag push matching `pact-python-cli/*`) +# The `build-sdist`, `build-wheels`, and `publish` jobs build the full wheel +# matrix (macOS/Linux/Windows, multiple architectures), create a GitHub +# release with the changelog, and publish all artifacts to PyPI. +# +# Additional: `build-sdist` and `build-wheels` also run on open/updated release +# PRs (not closed) to verify builds before merging. +# +# The `closed` PR type is listed in the trigger so that Stage 2 fires on merge. +# All jobs except `tag` guard against closed-but-not-merged events with +# explicit `if` conditions. +name: release cli + +on: + push: + branches: + - main + tags: + - pact-python-cli/* + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + - closed + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' && github.event.action != 'closed' }} + +env: + # Bump to 3.11 to access tomllib, otherwise, we would use the oldest supported Python version + STABLE_PYTHON_VERSION: '311' + HATCH_VERBOSE: '1' + FORCE_COLOR: '1' + CIBW_BUILD_FRONTEND: build + +jobs: + complete: + name: Release CLI completion check + if: always() + runs-on: ubuntu-latest + needs: + - prepare + - tag + - build-sdist + - build-wheels + - publish + steps: + - name: Failed + run: exit 1 + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + + prepare: + name: Update CLI release PR + if: >- + github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: + name: release-pr + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Need full history for git-cliff to determine the next version and + # generate the changelog + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN }} + + - name: Install git-cliff and typos + uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 + with: + tool: git-cliff,typos + + - name: Set up uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + + - name: Install Python + run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + + - name: Update release PR + run: uv run python scripts/release.py prepare cli + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + + tag: + name: Create CLI release tag + if: >- + github.event_name == 'pull_request' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, + 'release') && github.event.pull_request.head.ref == 'release/pact-python-cli' + runs-on: ubuntu-latest + environment: + name: release-pr + steps: + - name: Checkout main + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: main + token: ${{ secrets.GH_TOKEN }} + + - name: Set up uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + + - name: Install Python + run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + + - name: Create and push release tag + run: uv run python scripts/release.py tag cli + env: + # Require GH_TOKEN (PAT) instead of GITHUB_TOKEN so that the PR can + # itself trigger workflows + GH_TOKEN: ${{ secrets.GH_TOKEN }} + + build-sdist: + name: Build CLI source distribution + # Prepare the wheels for upload, and also verify that the build works on + # release PRs before merging. + if: >- + ( + github.event_name == 'push' && + startsWith(github.ref, 'refs/tags/pact-python-cli/') + ) || ( + github.event_name == 'pull_request' && + github.event.action != 'closed' && + github.event.pull_request.head.ref == 'release/pact-python-cli' + ) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + + - name: Install Python + run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + + - name: Install hatch + run: uv tool install hatch + + - name: Build source distribution + working-directory: pact-python-cli + run: hatch build --target sdist + + - name: Upload sdist + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: wheels-sdist + path: pact-python-cli/dist/*.tar* + if-no-files-found: error + compression-level: 0 + + build-wheels: + name: Build CLI wheel (release) on ${{ matrix.os }} + # Prepare the wheels for upload, and also verify that the build works on + # release PRs before merging. + if: >- + ( + github.event_name == 'push' && + startsWith(github.ref, 'refs/tags/pact-python-cli/') + ) || ( + github.event_name == 'pull_request' && + github.event.action != 'closed' && + github.event.pull_request.head.ref == 'release/pact-python-cli' + ) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - macos-15-intel + - macos-latest + - ubuntu-24.04-arm + - ubuntu-latest + - windows-11-arm + - windows-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Build wheels + uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 + with: + package-dir: pact-python-cli + env: + # Only target the oldest supported Python version, as the CLI wrapper + # is thin and should be compatible with future Python versions. + CIBW_BUILD: cp${{ env.STABLE_PYTHON_VERSION }}-* + + - name: Upload wheels + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: wheels-${{ matrix.os }} + path: wheelhouse/*.whl + if-no-files-found: error + compression-level: 0 + + publish: + name: Publish CLI wheels and sdist + if: >- + github.event_name == 'push' && startsWith(github.ref, 'refs/tags/pact-python-cli/') + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/pact-python-cli + needs: + - build-sdist + - build-wheels + permissions: + contents: write + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Extract release changelog + run: | + version="${{ github.ref_name }}" + version="${version#*/}" + out="${{ runner.temp }}/release-changelog.md" + awk -v ver="$version" ' + /^## / { if (found) exit; if (index($0, ver)) found=1; next } + found { print } + ' pact-python-cli/CHANGELOG.md > "$out" + if [ ! -s "$out" ]; then + printf '> [!WARNING]\n>\n> No changelog entry found for %s.\n' "$version" > "$out" + fi + + - name: Find previous release tag + id: previous-tag + run: | + git fetch --tags --quiet + previous=$(git tag --list 'pact-python-cli/*' --sort=-version:refname \ + | grep -v "^${{ github.ref_name }}$" | head -1) + echo "tag=${previous}" >> "$GITHUB_OUTPUT" + + - name: Download wheels and sdist + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: wheelhouse + merge-multiple: true + + - name: Create GitHub release + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + with: + files: wheelhouse/* + body_path: ${{ runner.temp }}/release-changelog.md + previous_tag: ${{ steps.previous-tag.outputs.tag }} + draft: false + prerelease: false + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + skip-existing: true + packages-dir: wheelhouse diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml new file mode 100644 index 000000000..6959795fc --- /dev/null +++ b/.github/workflows/release-core.yml @@ -0,0 +1,238 @@ +--- +# Release workflow for pact-python (core package). +# +# This workflow handles the full release lifecycle in three stages: +# +# Stage 1: Prepare (trigger: push to main) +# The `prepare` job runs `scripts/release.py prepare core`, which uses +# git-cliff to compute the next semver from conventional commits, updates +# pyproject.toml and CHANGELOG.md, and force-pushes those changes to the +# fixed branch `release/pact-python`. It then creates (or updates the title +# and body of) the release PR targeting main. +# +# Stage 2: Tag (trigger: release PR merged → `closed` event, merged == true) +# When the release PR on `release/pact-python` is merged, the `tag` job runs +# `scripts/release.py tag core`, which reads the version from pyproject.toml +# and pushes a git tag of the form `pact-python/X.Y.Z`. +# +# Stage 3: Publish (trigger: tag push matching `pact-python/*`) +# The `build` and `publish` jobs build the sdist and wheel, create a GitHub +# release with the changelog, and publish the artifacts to PyPI. +# +# Additional: the `build` job also runs on open/updated PRs (not closed) so +# that release PRs can be verified to build correctly before merging. +# +# The `closed` PR type is listed in the trigger so that Stage 2 fires on merge. +# All jobs except `tag` guard against closed-but-not-merged events with +# explicit `if` conditions. +name: release + +on: + push: + branches: + - main + tags: + - pact-python/* + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + - closed + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' && github.event.action != 'closed' }} + +env: + # Bump to 3.11 to access tomllib, otherwise, we would use the oldest supported Python version + STABLE_PYTHON_VERSION: '311' + HATCH_VERBOSE: '1' + FORCE_COLOR: '1' + +jobs: + complete: + name: Release completion check + if: always() + runs-on: ubuntu-latest + needs: + - prepare + - tag + - build + - publish + steps: + - name: Failed + run: exit 1 + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + + prepare: + name: Update release PR + if: >- + github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: + name: release-pr + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Need full history for git-cliff to determine the next version and + # generate the changelog + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN }} + + - name: Install git-cliff and typos + uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 + with: + tool: git-cliff,typos + + - name: Set up uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + + - name: Install Python + run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + + - name: Update release PR + run: uv run python scripts/release.py prepare core + env: + # Require GH_TOKEN (PAT) instead of GITHUB_TOKEN so that the PR can + # itself trigger workflows + GH_TOKEN: ${{ secrets.GH_TOKEN }} + + tag: + name: Create release tag + if: >- + github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.head.ref + == 'release/pact-python' && contains(github.event.pull_request.labels.*.name, 'release') + runs-on: ubuntu-latest + environment: + name: release-pr + steps: + - name: Checkout main + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: main + token: ${{ secrets.GH_TOKEN }} + + - name: Set up uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + + - name: Install Python + run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + + - name: Create and push release tag + run: uv run python scripts/release.py tag core + env: + # Require GH_TOKEN (PAT) instead of GITHUB_TOKEN so that the PR can + # itself trigger workflows + GH_TOKEN: ${{ secrets.GH_TOKEN }} + + build: + name: Build source distribution + # Prepare the wheels for upload, and also verify that the build works on + # release PRs before merging. + if: >- + ( + github.event_name == 'push' && + startsWith(github.ref, 'refs/tags/pact-python/') + ) || ( + github.event_name == 'pull_request' && + github.event.action != 'closed' && + github.event.pull_request.head.ref == 'release/pact-python' + ) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + + - name: Install Python + run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + + - name: Install hatch + run: uv tool install hatch + + - name: Build source distribution and wheel + run: hatch build + + - name: Upload sdist + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: wheels-sdist + path: ./dist/*.tar* + if-no-files-found: error + compression-level: 0 + + - name: Upload wheel + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: wheels-whl + path: ./dist/*.whl + if-no-files-found: error + compression-level: 0 + + publish: + name: Publish wheel and sdist + if: >- + github.event_name == 'push' && startsWith(github.ref, 'refs/tags/pact-python/') + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/pact-python + needs: + - build + permissions: + contents: write + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Extract release changelog + run: | + version="${{ github.ref_name }}" + version="${version#*/}" + out="${{ runner.temp }}/release-changelog.md" + awk -v ver="$version" ' + /^## / { if (found) exit; if (index($0, ver)) found=1; next } + found { print } + ' CHANGELOG.md > "$out" + if [ ! -s "$out" ]; then + printf '> [!WARNING]\n>\n> No changelog entry found for %s.\n' "$version" > "$out" + fi + + - name: Find previous release tag + id: previous-tag + run: | + git fetch --tags --quiet + previous=$(git tag --list 'pact-python/*' --sort=-version:refname \ + | grep -v "^${{ github.ref_name }}$" | head -1) + echo "tag=${previous}" >> "$GITHUB_OUTPUT" + + - name: Download wheels and sdist + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: wheelhouse + merge-multiple: true + + - name: Create GitHub release + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + with: + files: wheelhouse/* + body_path: ${{ runner.temp }}/release-changelog.md + previous_tag: ${{ steps.previous-tag.outputs.tag }} + draft: false + prerelease: false + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + skip-existing: true + packages-dir: wheelhouse diff --git a/.github/workflows/release-ffi.yml b/.github/workflows/release-ffi.yml new file mode 100644 index 000000000..014aef7b8 --- /dev/null +++ b/.github/workflows/release-ffi.yml @@ -0,0 +1,281 @@ +--- +# Release workflow for pact-python-ffi (FFI wrapper package). +# +# This package tracks the upstream pact-foundation/pact-reference project and +# versions itself as `{upstream_version}.{N}` (e.g. `0.4.28.0`). The release +# lifecycle runs in three stages: +# +# Stage 1: Prepare (trigger: push to main) +# The `prepare` job runs `scripts/release.py prepare ffi`, which fetches the +# latest `libpact_ffi-v*` release from pact-foundation/pact-reference, +# computes the next wrapper version, updates pyproject.toml and CHANGELOG.md, +# and force-pushes those changes to the fixed branch `release/pact-python-ffi`. +# It then creates (or updates the title and body of) the release PR targeting +# main. +# +# Stage 2: Tag (trigger: release PR merged → `closed` event, merged == true) +# When the release PR on `release/pact-python-ffi` is merged, the `tag` job +# runs `scripts/release.py tag ffi`, which reads the version from +# pyproject.toml and pushes a git tag of the form `pact-python-ffi/X.Y.Z.N`. +# +# Stage 3: Publish (trigger: tag push matching `pact-python-ffi/*`) +# The `build-sdist`, `build-wheels`, and `publish` jobs build the full wheel +# matrix (macOS/Linux/Windows, multiple architectures), create a GitHub +# release with the changelog, and publish all artifacts to PyPI. +# +# Additional: `build-sdist` and `build-wheels` also run on open/updated release +# PRs (not closed) to verify builds before merging. +# +# The `closed` PR type is listed in the trigger so that Stage 2 fires on merge. +# All jobs except `tag` guard against closed-but-not-merged events with +# explicit `if` conditions. +name: release ffi + +on: + push: + branches: + - main + tags: + - pact-python-ffi/* + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + - closed + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' && github.event.action != 'closed' }} + +env: + # Bump to 3.11 to access tomllib, otherwise, we would use the oldest supported Python version + STABLE_PYTHON_VERSION: '311' + HATCH_VERBOSE: '1' + FORCE_COLOR: '1' + CIBW_BUILD_FRONTEND: build + +jobs: + complete: + name: Release FFI completion check + if: always() + runs-on: ubuntu-latest + needs: + - prepare + - tag + - build-sdist + - build-wheels + - publish + steps: + - name: Failed + run: exit 1 + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + + prepare: + name: Update FFI release PR + if: >- + github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: + name: release-pr + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Need full history for git-cliff to determine the next version and + # generate the changelog + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN }} + + - name: Install git-cliff and typos + uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 + with: + tool: git-cliff,typos + + - name: Set up uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + + - name: Install Python + run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + + - name: Update release PR + run: uv run python scripts/release.py prepare ffi + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + + tag: + name: Create FFI release tag + if: >- + github.event_name == 'pull_request' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, + 'release') && github.event.pull_request.head.ref == 'release/pact-python-ffi' + runs-on: ubuntu-latest + environment: + name: release-pr + steps: + - name: Checkout main + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: main + token: ${{ secrets.GH_TOKEN }} + + - name: Set up uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + + - name: Install Python + run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + + - name: Create and push release tag + run: uv run python scripts/release.py tag ffi + env: + # Require GH_TOKEN (PAT) instead of GITHUB_TOKEN so that the PR can + # itself trigger workflows + GH_TOKEN: ${{ secrets.GH_TOKEN }} + + build-sdist: + name: Build FFI source distribution + # Prepare the wheels for upload, and also verify that the build works on + # release PRs before merging. + if: >- + ( + github.event_name == 'push' && + startsWith(github.ref, 'refs/tags/pact-python-ffi/') + ) || ( + github.event_name == 'pull_request' && + github.event.action != 'closed' && + github.event.pull_request.head.ref == 'release/pact-python-ffi' + ) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + + - name: Install Python + run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + + - name: Install hatch + run: uv tool install hatch + + - name: Build source distribution + working-directory: pact-python-ffi + run: hatch build --target sdist + + - name: Upload sdist + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: wheels-sdist + path: pact-python-ffi/dist/*.tar* + if-no-files-found: error + compression-level: 0 + + build-wheels: + name: Build FFI wheel on ${{ matrix.os }} + # Prepare the wheels for upload, and also verify that the build works on + # release PRs before merging. + if: >- + ( + github.event_name == 'push' && + startsWith(github.ref, 'refs/tags/pact-python-ffi/') + ) || ( + github.event_name == 'pull_request' && + github.event.action != 'closed' && + github.event.pull_request.head.ref == 'release/pact-python-ffi' + ) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - macos-15-intel + - macos-latest + - ubuntu-24.04-arm + - ubuntu-latest + - windows-11-arm + - windows-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Build wheels + uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 + with: + package-dir: pact-python-ffi + env: + # Only target the oldest supported Python version, as we build against + # the stable ABI + CIBW_BUILD: cp${{ env.STABLE_PYTHON_VERSION }}-* + + - name: Upload wheels + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: wheels-${{ matrix.os }} + path: wheelhouse/*.whl + if-no-files-found: error + compression-level: 0 + + publish: + name: Publish FFI wheels and sdist + if: >- + github.event_name == 'push' && startsWith(github.ref, 'refs/tags/pact-python-ffi/') + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/pact-python-ffi + needs: + - build-sdist + - build-wheels + permissions: + contents: write + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Extract release changelog + run: | + version="${{ github.ref_name }}" + version="${version#*/}" + out="${{ runner.temp }}/release-changelog.md" + awk -v ver="$version" ' + /^## / { if (found) exit; if (index($0, ver)) found=1; next } + found { print } + ' pact-python-ffi/CHANGELOG.md > "$out" + if [ ! -s "$out" ]; then + printf '> [!WARNING]\n>\n> No changelog entry found for %s.\n' "$version" > "$out" + fi + + - name: Find previous release tag + id: previous-tag + run: | + git fetch --tags --quiet + previous=$(git tag --list 'pact-python-ffi/*' --sort=-version:refname \ + | grep -v "^${{ github.ref_name }}$" | head -1) + echo "tag=${previous}" >> "$GITHUB_OUTPUT" + + - name: Download wheels and sdist + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: wheelhouse + merge-multiple: true + + - name: Create GitHub release + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + with: + files: wheelhouse/* + body_path: ${{ runner.temp }}/release-changelog.md + previous_tag: ${{ steps.previous-tag.outputs.tag }} + draft: false + prerelease: false + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + skip-existing: true + packages-dir: wheelhouse diff --git a/docs/releases.md b/docs/releases.md index 4bbc6f658..0160be4bf 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,49 +2,78 @@ Pact Python is made available through both GitHub releases and PyPI. The GitHub releases also come with a summary of changes and contributions since the last release. -The entire process is automated through the [build](https://github.com/pact-foundation/pact-python/actions/workflows/build.yml?query=branch%3Amain) GitHub Action. A description of the process is provided [below](#build-pipeline). +The entire process is automated through three GitHub Actions workflows (one per package), each running a three-stage pipeline. A description of the process is provided [below](#release-pipeline). + +## Packages + +Pact Python is split into three independently versioned packages: + +- **[`pact-python`](https://pypi.org/p/pact-python)**: the core library. Versioned with [semantic versioning](https://semver.org/), derived from conventional commits via [git-cliff](https://git-cliff.org/). +- **[`pact-python-ffi`](https://pypi.org/p/pact-python-ffi)**: Python bindings for the [pact-reference](https://github.com/pact-foundation/pact-reference) Rust library. Versioned as `{upstream}.{N}` (e.g. `0.4.28.0`), tracking upstream `libpact_ffi` releases. +- **[`pact-python-cli`](https://pypi.org/p/pact-python-cli)**: Python wrapper for the [pact-standalone](https://github.com/pact-foundation/pact-standalone) CLI tools. Versioned as `{upstream}.{N}` (e.g. `2.4.0.0`), tracking upstream releases. ## Versioning -Pact Python follows [semantic versioning](https://semver.org/). Breaking changes are indicated by a major version bump, new features by a minor version bump, and bug fixes by a patch version bump. +### pact-python (core) + +The core package follows [semantic versioning](https://semver.org/). Breaking changes are indicated by a major version bump, new features by a minor version bump, and bug fixes by a patch version bump. There are a couple of exceptions to the [semantic versioning](https://semver.org/) rules: - Dropping support for a Python version is not considered a breaking change and is not necessarily accompanied by a major version bump. -- Private APIs are not considered part of the public API and are not subject to the same rules as the public API. They can be changed at any time without a major version bump. Private APIs are denoted by a leading underscore in the name. Please be aware that the distinction between public and private APIs will be made concrete from version 3 onwards, and best judgement is used in the meantime to determine what is public and what is private. +- Private APIs are not considered part of the public API and are not subject to the same rules as the public API. They can be changed at any time without a major version bump. Private APIs are denoted by a leading underscore in the name. - Deprecations are not considered breaking changes and are not necessarily accompanied by a major version bump. Their removal is considered a breaking change and is accompanied by a major version bump. - Changes to the type annotations will not be considered breaking changes, unless they are accompanied by a change to the runtime behaviour. -Any deviation from the the standard semantic versioning rules will be clearly documented in the release notes. +Any deviation from the standard semantic versioning rules will be clearly documented in the release notes. + +The next version is computed automatically by [git-cliff](https://git-cliff.org/) from the [conventional commit](https://www.conventionalcommits.org/) history since the last release tag. + +### pact-python-ffi and pact-python-cli + +These packages follow their upstream projects' versioning, extended with a fourth component `N` that starts at `0` for each new upstream version and increments with each packaging-only release. When a new upstream version is released, `N` resets to `0`. + +### Version storage + +Each package stores its version as a static string in its `pyproject.toml`. The version is updated automatically by the release pipeline during the prepare stage and committed to the release branch. + +## Release Pipeline + +Each package has its own release workflow (`.github/workflows/release-{core,ffi,cli}.yml`). All three follow the same three-stage process: + +### Stage 1: Prepare + +Triggered by every push to `main`. + +The `prepare` job runs `scripts/release.py prepare `, which: -The version is stored in `pact/__version__.py`. This file is automatically generated by [`versioningit`](https://versioningit.readthedocs.io/en/stable/) and generates a version based on the latest version tag and the number of commits since that tag. Specifically: +1. Computes the next version (via git-cliff for core; by fetching the latest upstream release for ffi/cli). +2. Updates `pyproject.toml` with the new version. +3. Prepends the new release entry to `CHANGELOG.md` using git-cliff, preserving all previous entries (including any manual edits). +4. Force-pushes those changes to a fixed release branch (e.g. `release/pact-python`). +5. Creates the release PR if it does not exist, or updates its title and body if it does. -- If the latest tag is `v1.2.3` and there have been no commits since then and the repository is clean, the version will be `1.2.3`. -- Otherwise, the version will take the form of a post-release based on the last version. +The release PR gives maintainers an opportunity to review and manually adjust the changelog before it is published. -## Build Pipeline +A PAT (`GH_TOKEN`) is used to create/update the release PR so that the PR triggers the expected downstream workflow runs (GitHub's loop-prevention rules block `GITHUB_TOKEN`-triggered events from starting new workflow runs). -The build pipeline is defined in `.github/workflows/build.yml`. It is triggered on PRs targeting `main`, pushes to the `main` branch, and on every new tag. The pipeline is responsible for building the package (both as source distribution, and compiled wheels), creating the GitHub release, and uploading artifacts to PyPI. +### Stage 2: Tag -### Build Steps +Triggered when the release PR is merged (GitHub fires a `pull_request` event with `action: closed` and `merged: true`). -The build steps generates the source distribution and wheels. This is done using [cibuildwheel](https://cibuildwheel.readthedocs.io/) to ensure that the wheels are compatible with a wide range of Python versions and platforms. +The `tag` job runs `scripts/release.py tag `, which reads the version from `pyproject.toml` on `main` and pushes a git tag of the form `/` (e.g. `pact-python/3.2.2`). -In order to reduce the build time, the pipeline builds different sets of wheels depending on the trigger: +A PAT (`GH_TOKEN`) is used instead of the default `GITHUB_TOKEN` so that the tag push is able to trigger the downstream Stage 3 workflow run (GitHub's loop-prevention rules block `GITHUB_TOKEN`-triggered events from starting new workflow runs). -| Trigger | Platforms | Wheels | -| ------------ | ----------------- | --------- | -| Tag | `x86_64`, `arm64` | all | -| `main` | `x86_64` | all | -| Pull Request | `x86_64` | `cp312-*` | +### Stage 3: Publish -### Publish Step +Triggered by a tag push matching the package's tag prefix. -The publish step uses the `pypi` GitHub environment, and is gated behind a manual approval. The publish step is responsible for the following: +The `publish` job: -- Generating a changelog based on the conventional commits since the latest release. -- Generating a new GitHub release with the changelog. -- Uploading the source distribution and wheels to PyPI. -- Creating a PR to update the `CHANGELOG.md` file with the new release notes. +1. Extracts the changelog entry for the tagged version directly from the committed `CHANGELOG.md` (preserving any manual edits made to the release PR). +2. Builds the source distribution and wheels across all supported platforms and architectures. +3. Creates a GitHub release with the extracted changelog. +4. Publishes all artifacts to PyPI. -While the generated changelog should be accurate, it may require some manual adjustments on the release page and in the PR. +The `publish` job uses the `pypi` GitHub environment, which can be configured to require manual approval before publishing. diff --git a/scripts/release.py b/scripts/release.py new file mode 100755 index 000000000..4f6420e02 --- /dev/null +++ b/scripts/release.py @@ -0,0 +1,687 @@ +#!/usr/bin/env python3 +# ruff: noqa: S603, S607 +"""Release management script for pact-python packages. + +Usage: + python scripts/release.py prepare core [--dry-run] + python scripts/release.py prepare ffi [--dry-run] + python scripts/release.py prepare cli [--dry-run] + + python scripts/release.py tag core + python scripts/release.py tag ffi + python scripts/release.py tag cli +""" + +from __future__ import annotations + +import argparse +import json +import logging +import re +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +import tomllib + +ROOT = Path(__file__).parent.parent +logger = logging.getLogger(__name__) + + +@dataclass +class PullRequest: + """A GitHub pull request as returned by `gh pr list`.""" + + number: int + """GitHub PR number.""" + + head_ref_name: str + """The head branch name (JSON field `headRefName`).""" + + +@dataclass +class Package: + """Configuration for a releasable package in the monorepo.""" + + name: str + """PyPI package name, e.g. `"pact-python"`.""" + + key: str + """Short identifier used as the CLI argument and as the `PACKAGES` lookup key.""" + + directory: Path + """Absolute path to the package root (where `pyproject.toml` lives).""" + + tag_prefix: str + """Git tag prefix, e.g. `"pact-python/"` → tag `"pact-python/3.2.2"`.""" + + upstream_repo: str | None + """GitHub repo to track for the upstream version (`owner/repo`). + `None` for the core package, which derives its version from git history + via git cliff instead of following an external release.""" + + upstream_tag_prefix: str | None + """Prefix used to identify and strip upstream release tags. + + Releases whose `tagName` does not start with this prefix are ignored, and + the prefix is stripped to produce the bare version string. Examples: + + - `"libpact_ffi-v"` for `pact-foundation/pact-reference` (only libpact_ffi + releases, not mock_server / verifier / etc.) + - `"v"` for `pact-foundation/pact-standalone` + + `None` when `upstream_repo` is `None` (i.e., the core package). + """ + + release_branch: str + """Fixed branch name for release PRs, e.g. `"release/pact-python"`.""" + + def read_version(self) -> str: + """Read the static version from the package's pyproject.toml.""" + with (self.directory / "pyproject.toml").open("rb") as f: + data = tomllib.load(f) + return data["project"]["version"] + + def write_version(self, version: str) -> None: + """Update the version field in the package's pyproject.toml. + + Uses a regex substitution rather than TOML round-tripping so that + comments, formatting, and key ordering in pyproject.toml are preserved. + """ + toml_path = self.directory / "pyproject.toml" + content = toml_path.read_text() + new_content = re.sub( + r'^(version\s*=\s*")[^"]*(")', + rf"\g<1>{version}\g<2>", + content, + count=1, + flags=re.MULTILINE, + ) + if new_content == content: + msg = f"Could not find version field to update in {toml_path}" + raise ValueError(msg) + logger.debug("Writing version %s to %s", version, toml_path) + toml_path.write_text(new_content) + + def compute_tag_name(self, version: str) -> str: + """Return the full git tag name for a package version.""" + return f"{self.tag_prefix}{version}" + + def fetch_upstream_version(self) -> str: + """Fetch the latest upstream release version (without prefix) via gh CLI. + + Uses `gh release list` filtered by + [`upstream_tag_prefix`][scripts.release.Package.upstream_tag_prefix] + to find the most recent matching release, then strips the prefix to + return the bare version string. + + Returns: + The upstream version string without any prefix, e.g. `"0.4.28"`. + + Raises: + ValueError: If this package has no upstream repo or tag prefix configured. + """ + if self.upstream_repo is None or self.upstream_tag_prefix is None: + msg = f"Package {self.name!r} has no upstream repo configured" + raise ValueError(msg) + + prefix = self.upstream_tag_prefix + logger.debug( + "Fetching upstream version from %s (tag prefix: %r)", + self.upstream_repo, + prefix, + ) + jq_filter = ( + f'[.[] | select(.tagName | startswith("{prefix}"))] | first | .tagName' + ) + result = subprocess.check_output( + [ + "gh", + "release", + "list", + "--repo", + self.upstream_repo, + "--json", + "tagName", + "--jq", + jq_filter, + ], + text=True, + ) + tag = result.strip() + if not tag or tag == "null": + msg = f"No release with prefix {prefix!r} found in {self.upstream_repo!r}" + raise ValueError(msg) + version = tag.removeprefix(prefix) + logger.debug("Upstream tag: %s → version: %s", tag, version) + return version + + def compute_semver_version(self) -> str | None: + """Return the next semver via git cliff, or None. + + Returns `None` when there are no unreleased commits that would produce a + version bump (git cliff exits non-zero or returns empty output). + + Returns: + The next version string (e.g. `"3.2.2"`), or `None` if no bump is + needed. + """ + logger.debug("Running git cliff --bumped-version in %s", self.directory) + result = subprocess.run( + ["git", "cliff", "--bumped-version"], + capture_output=True, + text=True, + check=False, + cwd=self.directory, + ) + if result.returncode != 0 or not result.stdout.strip(): + logger.debug("git cliff: no version bump needed") + return None + raw = result.stdout.strip() + # git cliff outputs the full tag name (e.g. "pact-python/3.2.2"); strip prefix + version = strip_tag_prefix(raw, self.tag_prefix) + logger.debug("git cliff bumped version: %s", version) + return version + + def compute_next_version(self) -> str | None: + """Return the proposed next version string, or None if no release is needed. + + Dispatches to one of two strategies based on whether the package tracks + an upstream: + + - **Core** (`upstream_repo` is `None`): asks git cliff for the next + semver implied by unreleased conventional commits; returns `None` when + there is nothing to release. + - **FFI / CLI** (`upstream_repo` set): fetches the latest upstream GitHub + release and derives the 4-part version via `compute_wrapper_version`. + + Returns: + The next version string, or `None` if nothing has changed since the + last release. + """ + if self.upstream_repo is None: + logger.debug("Computing next version for %s via git cliff", self.name) + return self.compute_semver_version() + logger.debug( + "Computing next version for %s via upstream %s", + self.name, + self.upstream_repo, + ) + upstream = self.fetch_upstream_version() + current = self.read_version() + logger.debug("Current: %s Upstream: %s", current, upstream) + return compute_wrapper_version(upstream, current) + + def find_open_release_pr(self) -> PullRequest | None: + """Return the open release PR for this package, or None.""" + logger.debug( + "Looking for open release PR on branch %r", + self.release_branch, + ) + result = subprocess.check_output( + [ + "gh", + "pr", + "list", + "--head", + self.release_branch, + "--label", + "release", + "--state", + "open", + "--json", + "number,headRefName", + "--jq", + "first", + ], + text=True, + ) + pr = parse_existing_pr(result) + if pr is not None: + logger.debug( + "Found existing PR #%d on branch %s", pr.number, pr.head_ref_name + ) + else: + logger.debug("No existing release PR found") + return pr + + def generate_changelog_body(self, version: str) -> str: + """ + Generate the changelog entry body for a proposed version using git cliff. + + Uses `--tag` to assign the version to unreleased commits and + `--strip header` to return only the entry body without the changelog + header. + + Args: + version: + The proposed next version string. + + Returns: + The rendered changelog body as a string. + """ + tag = self.compute_tag_name(version) + logger.debug("Generating changelog body for tag %s", tag) + return subprocess.check_output( + ["git", "cliff", "--tag", tag, "--unreleased", "--strip", "header"], + text=True, + cwd=self.directory, + ) + + def update_changelog_file(self, version: str) -> None: + """ + Prepend the changelog entry for unreleased commits to CHANGELOG.md. + + Uses `--prepend` rather than `--output` so that only the new entry is + inserted at the top of the file, leaving all previous entries (including + any manual edits) untouched. + + Args: + version: + The version to assign to the unreleased commits. + """ + tag = self.compute_tag_name(version) + logger.debug( + "Prepending changelog entry to CHANGELOG.md in %s for tag %s", + self.directory, + tag, + ) + subprocess.check_call( + ["git", "cliff", "--tag", tag, "--unreleased", "--prepend", "CHANGELOG.md"], + text=True, + cwd=self.directory, + ) + + def _configure_git_for_ci(self) -> None: + """Configure git identity and authenticate the remote for CI operations. + + Sets `user.name` and `user.email` only when they are not already + configured (preserving a developer's local git config when running + outside CI). If `GH_TOKEN` is set, embeds the token in the HTTPS + remote URL so that `git push` and `gh` operations authenticate without + a separate credential helper. + """ + logger.debug("Configuring git identity for CI") + if not subprocess.run( + ["git", "config", "--get", "user.name"], + text=True, + check=False, + capture_output=True, + cwd=ROOT, + ).stdout.strip(): + logger.debug("Setting git user.name to github-actions[bot]") + subprocess.check_call( + ["git", "config", "user.name", "github-actions[bot]"], + text=True, + cwd=ROOT, + ) + if not subprocess.run( + ["git", "config", "--get", "user.email"], + text=True, + check=False, + capture_output=True, + cwd=ROOT, + ).stdout.strip(): + logger.debug("Setting git user.email to github-actions[bot]@...") + subprocess.check_call( + [ + "git", + "config", + "user.email", + "github-actions[bot]@users.noreply.github.com", + ], + text=True, + cwd=ROOT, + ) + + def _push_release_branch( + self, + pr_title: str, + changelog: str, + existing_pr: PullRequest | None, + ) -> None: + """Create or update the fixed release branch, then create or update the PR. + + Resets the fixed release branch to the current HEAD (main), commits the + prepared files, force-pushes, and creates or updates the PR title and body. + Using a fixed branch name means no old PRs need to be closed when the + proposed version changes — the existing PR is simply updated in place. + + Args: + pr_title: + The PR title, e.g. `"chore(release): pact-python v3.2.2"`. + changelog: + The rendered changelog body to set as the PR description. + existing_pr: + The open [`PullRequest`][scripts.release.PullRequest] to update, + or `None` to create a new PR. + """ + branch = self.release_branch + pkg_files = [ + str(self.directory / "pyproject.toml"), + str(self.directory / "CHANGELOG.md"), + ] + if existing_pr is not None: + logger.debug( + "Updating existing release branch %s (PR #%d)", + branch, + existing_pr.number, + ) + else: + logger.debug("Creating new release branch %s", branch) + subprocess.check_call( + ["git", "checkout", "-B", branch, "origin/main"], + text=True, + cwd=ROOT, + ) + subprocess.check_call( + ["git", "add", *pkg_files], + text=True, + cwd=ROOT, + ) + subprocess.check_call( + ["git", "commit", "-m", pr_title], + text=True, + cwd=ROOT, + ) + subprocess.check_call( + ["git", "push", "--force", "origin", branch], + text=True, + cwd=ROOT, + ) + if existing_pr is not None: + subprocess.check_call( + [ + "gh", + "pr", + "edit", + str(existing_pr.number), + "--title", + pr_title, + "--body", + changelog, + ], + text=True, + ) + else: + subprocess.check_call( + [ + "gh", + "pr", + "create", + "--title", + pr_title, + "--body", + changelog, + "--label", + "release", + "--base", + "main", + "--draft", + ], + text=True, + ) + + def prepare(self, *, dry_run: bool) -> None: + """Create or update the release PR for this package. + + Determines the next version, generates a changelog, writes the version + and changelog to disk, then (unless `dry_run` is set) creates or updates + the release PR on GitHub. The release branch is a fixed name + (`release_branch`) that is force-pushed on every run, so the same PR + is updated in place even when the proposed version changes. + + File writes (`pyproject.toml`, `CHANGELOG.md`) always happen so the + output can be inspected locally; they are easily reverted with + `git checkout`. Git branch operations (push, PR create/edit) are + skipped when `dry_run` is `True`. + + Args: + dry_run: + When `True`, write files to disk and print a summary of the + GitHub actions that *would* be taken, but do not push, create, + or update any branches or pull requests. + """ + version = self.compute_next_version() + if version is None: + logger.info("No version bump needed for %s. Nothing to do.", self.name) + return + + logger.info("Proposed next version for %s: %s", self.name, version) + changelog = self.generate_changelog_body(version) + pr_title = f"chore(release): {self.name} v{version}" + existing_pr = self.find_open_release_pr() + + # Write version and changelog to disk. These are always applied so the + # result can be inspected locally; `git checkout` reverts them cleanly. + logger.debug("Writing version and changelog to disk") + self.write_version(version) + self.update_changelog_file(version) + + if dry_run: + logger.info( + "\n--- Changelog for %s v%s ---\n%s", + self.name, + version, + changelog, + ) + if existing_pr is not None: + logger.info( + "[dry-run] Would update PR #%d title and body on branch %r.", + existing_pr.number, + self.release_branch, + ) + else: + logger.info( + "[dry-run] Would create PR %r on branch %r targeting main.", + pr_title, + self.release_branch, + ) + logger.info("[dry-run] Files written — use `git checkout` to revert.") + return + + try: + self._configure_git_for_ci() + self._push_release_branch(pr_title, changelog, existing_pr) + finally: + # Return to main so the local repo is left in a clean state, even on failure + logger.debug("Returning to main branch") + subprocess.check_call( + ["git", "checkout", "main"], + text=True, + cwd=ROOT, + ) + + logger.info("Release PR for %s v%s created/updated.", self.name, version) + + def tag(self, *, dry_run: bool = False) -> None: + """Create and push the release git tag for this package. + + Reads the version from `pyproject.toml` (the unconditional source of truth) + and pushes a tag of the form `{tag_prefix}{version}`. Idempotent: exits + cleanly with code 0 if the tag already exists, so the workflow can be + re-triggered safely after a transient failure. + + Args: + dry_run: + When `True`, print the tag that would be created without + pushing anything. + """ + logger.debug("Reading version from %s/pyproject.toml", self.directory) + version = self.read_version() + tag_name = self.compute_tag_name(version) + logger.debug("Computed tag name: %s", tag_name) + + # Fetch tags so the local repo reflects the remote state before checking + logger.debug("Fetching tags from origin") + subprocess.check_call( + ["git", "fetch", "--tags", "origin"], + cwd=ROOT, + ) + + # Check if the tag already exists + logger.debug("Checking whether tag %s already exists", tag_name) + result = subprocess.check_output( + ["git", "tag", "-l", tag_name], + text=True, + cwd=ROOT, + ) + if result.strip(): + logger.info("Tag %r already exists. Nothing to do.", tag_name) + sys.exit(0) + + if dry_run: + logger.info("[dry-run] Would create and push tag %r.", tag_name) + return + + logger.info("Creating tag %r...", tag_name) + subprocess.check_call( + ["git", "tag", tag_name], + text=True, + cwd=ROOT, + ) + logger.debug("Pushing tag %s to origin", tag_name) + subprocess.check_call( + ["git", "push", "origin", tag_name], + text=True, + cwd=ROOT, + ) + logger.info("Tag %r pushed.", tag_name) + + +# Each package uses a different versioning strategy: +# core — git cliff --bumped-version derives the next semver from commit history. +# ffi — tracks pact-foundation/pact-reference; version is {upstream}.{N}. +# cli — tracks pact-foundation/pact-standalone; version is {upstream}.{N}. +# The {N} suffix increments when the upstream semver is unchanged, resets to 0 +# on a new upstream release (see compute_wrapper_version). +PACKAGES: dict[str, Package] = { + "core": Package( + name="pact-python", + key="core", + directory=ROOT, + tag_prefix="pact-python/", + upstream_repo=None, + upstream_tag_prefix=None, + release_branch="release/pact-python", + ), + "ffi": Package( + name="pact-python-ffi", + key="ffi", + directory=ROOT / "pact-python-ffi", + tag_prefix="pact-python-ffi/", + upstream_repo="pact-foundation/pact-reference", + # Filter to libpact_ffi releases only — pact-reference hosts many crates + upstream_tag_prefix="libpact_ffi-v", + release_branch="release/pact-python-ffi", + ), + "cli": Package( + name="pact-python-cli", + key="cli", + directory=ROOT / "pact-python-cli", + tag_prefix="pact-python-cli/", + upstream_repo="pact-foundation/pact-standalone", + upstream_tag_prefix="v", + release_branch="release/pact-python-cli", + ), +} + + +def strip_tag_prefix(tag_or_version: str, prefix: str) -> str: + """Strip a tag prefix from a version string if present. + + Args: + tag_or_version: + A full tag name (e.g. `"pact-python/3.2.2"`) or a bare version + string (e.g. `"3.2.2"`). + prefix: + The prefix to remove (e.g. `"pact-python/"`). + + Returns: + The version string with the prefix removed, or the original string if + the prefix is not present. + """ + return tag_or_version.removeprefix(prefix) + + +def compute_wrapper_version(upstream_version: str, current_version: str) -> str: + """Compute the next 4-part version for wrapper packages. + + The version format is `{upstream_version}.{N}` where `N` is a per-packaging + suffix, and upstream version will (typically) be a semver-compatible + version. When the upstream semver portion matches the current version's + first three components the suffix is incremented; otherwise the suffix + resets to 0. + + Args: + upstream_version: + The latest upstream semver string, e.g. `"0.4.28"`. + current_version: + The current 4-part package version, e.g. `"0.4.28.2"`. + + Returns: + The next 4-part version string, e.g. `"0.4.28.3"` or `"0.4.29.0"`. + """ + current_parts = current_version.split(".") + current_upstream = ".".join(current_parts[:3]) + if upstream_version == current_upstream: + return f"{upstream_version}.{int(current_parts[3]) + 1}" + return f"{upstream_version}.0" + + +def parse_existing_pr(gh_output: str) -> PullRequest | None: + """Parse JSON output from `gh pr list` into a `PullRequest`, or None. + + Args: + gh_output: + Raw stdout from a `gh pr list --json` call. + + Returns: + A `PullRequest`, or `None` if the output is empty or the JSON + literal `"null"`. + """ + text = gh_output.strip() + if not text or text == "null": + return None + data = json.loads(text) + return PullRequest(number=data["number"], head_ref_name=data["headRefName"]) + + +def main() -> None: + """Entry point for the release management script.""" + parser = argparse.ArgumentParser( + description="Release management for pact-python packages" + ) + parser.add_argument("command", choices=["prepare", "tag"]) + parser.add_argument("package", choices=["core", "ffi", "cli"]) + parser.add_argument( + "--dry-run", + action="store_true", + help="Write files but skip all git/GitHub operations", + ) + parser.add_argument( + "--debug", + action="store_true", + help="Enable debug logging to trace each step", + ) + args = parser.parse_args() + + level = logging.DEBUG if args.debug else logging.INFO + # Show the level prefix only in debug mode to keep normal output clean + fmt = "%(levelname)s: %(message)s" if args.debug else "%(message)s" + logging.basicConfig(level=level, format=fmt) + + pkg = PACKAGES[args.package] + + if args.command == "tag": + pkg.tag(dry_run=args.dry_run) + elif args.command == "prepare": + pkg.prepare(dry_run=args.dry_run) + else: + sys.stderr.write(f"Unknown command {args.command!r}\n") + sys.exit(1) + + +if __name__ == "__main__": + main() From 17b0a5e2b00c3d5a14f6d45c24f0fd5065db9f3b Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 16 Apr 2026 07:56:55 +1000 Subject: [PATCH 1322/1376] chore: minor update to cliff config In aprticular, remove the `output` field so that it is handled by the CLI caller, I found this can cause issue when trying to generate the latest/unreleased section. Signed-off-by: JP-Ellis --- cliff.toml | 7 ++----- pact-python-cli/cliff.toml | 9 +++------ pact-python-ffi/cliff.toml | 7 ++----- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/cliff.toml b/cliff.toml index 49677c920..de71106f5 100644 --- a/cliff.toml +++ b/cliff.toml @@ -1,4 +1,3 @@ -#:schema https://www.schemastore.org/any.json # git-cliff configuration file # https://git-cliff.org/docs/configuration @@ -63,8 +62,6 @@ trim = true postprocessors = [] # render body even when there are no releases to process # render_always = true -# output file path -output = "CHANGELOG.md" [git] tag_pattern = "^(v.*|pact-python/.*)$" @@ -79,7 +76,7 @@ commit_preprocessors = [ # Remove the PR number added by GitHub when merging PR in UI { pattern = '\s*\(#([0-9]+)\)$', replace = "" }, # Check spelling of the commit with https://github.com/crate-ci/typos - { pattern = '.*', replace_command = 'typos --write-changes -' }, + { pattern = ".*", replace_command = "typos --write-changes -" }, ] # regex for parsing and grouping commits commit_parsers = [ @@ -109,4 +106,4 @@ exclude_paths = ["pact-python-cli/", "pact-python-ffi/"] [remote.github] owner = "pact-foundation" -repo = "pact-python" +repo = "pact-python" diff --git a/pact-python-cli/cliff.toml b/pact-python-cli/cliff.toml index f8a223678..0d5368605 100644 --- a/pact-python-cli/cliff.toml +++ b/pact-python-cli/cliff.toml @@ -1,4 +1,3 @@ -#:schema https://www.schemastore.org/any.json # git-cliff configuration file # https://git-cliff.org/docs/configuration @@ -66,8 +65,6 @@ trim = true postprocessors = [] # render body even when there are no releases to process # render_always = true -# output file path -output = "CHANGELOG.md" [git] tag_pattern = "^pact-python-cli/.*$" @@ -82,7 +79,7 @@ commit_preprocessors = [ # Remove the PR number added by GitHub when merging PR in UI { pattern = '\s*\(#([0-9]+)\)$', replace = "" }, # Check spelling of the commit with https://github.com/crate-ci/typos - { pattern = '.*', replace_command = 'typos --write-changes -' }, + { pattern = ".*", replace_command = "typos --write-changes -" }, ] # regex for parsing and grouping commits commit_parsers = [ @@ -108,8 +105,8 @@ topo_order = false # sort the commits inside sections by oldest/newest order sort_commits = "oldest" # Only include the current directory (relative to the .git directory) -include_paths = ["pact-python-ffi/"] +include_paths = ["pact-python-cli/"] [remote.github] owner = "pact-foundation" -repo = "pact-python" +repo = "pact-python" diff --git a/pact-python-ffi/cliff.toml b/pact-python-ffi/cliff.toml index 8cc69a39a..dacd83159 100644 --- a/pact-python-ffi/cliff.toml +++ b/pact-python-ffi/cliff.toml @@ -1,4 +1,3 @@ -#:schema https://www.schemastore.org/any.json # git-cliff configuration file # https://git-cliff.org/docs/configuration @@ -66,8 +65,6 @@ trim = true postprocessors = [] # render body even when there are no releases to process # render_always = true -# output file path -output = "CHANGELOG.md" [git] tag_pattern = "^pact-python-ffi/.*$" @@ -82,7 +79,7 @@ commit_preprocessors = [ # Remove the PR number added by GitHub when merging PR in UI { pattern = '\s*\(#([0-9]+)\)$', replace = "" }, # Check spelling of the commit with https://github.com/crate-ci/typos - { pattern = '.*', replace_command = 'typos --write-changes -' }, + { pattern = ".*", replace_command = "typos --write-changes -" }, ] # regex for parsing and grouping commits commit_parsers = [ @@ -112,4 +109,4 @@ include_paths = ["pact-python-ffi/"] [remote.github] owner = "pact-foundation" -repo = "pact-python" +repo = "pact-python" From 9fd66ebff099d32b33e69d1639930b8596cc96da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:11:48 +0000 Subject: [PATCH 1323/1376] chore(deps): update dependency mkdocstrings to v1.0.4 (#1549) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b5ae3f8bb..31e55c8e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ docs = [ "mkdocs-material[recommended,git,imaging]==9.7.6", "mkdocs-section-index==0.3.11", "mkdocs==1.6.1", - "mkdocstrings[python]==1.0.3", + "mkdocstrings[python]==1.0.4", "pathspec==1.0.4", ] example = [ From e03c914e1e25587345bf59097e4811ed3b450e54 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:12:39 +0000 Subject: [PATCH 1324/1376] chore(deps): update pre-commit hook biomejs/pre-commit to v2.4.12 (#1550) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 862ee8155..710a0f22c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: check-json5 - repo: https://github.com/biomejs/pre-commit - rev: v2.4.11 + rev: v2.4.12 hooks: - id: biome-check From 917bfe2a05a579420b5135534706316c2265aeee Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 16 Apr 2026 12:14:42 +1000 Subject: [PATCH 1325/1376] chore: authenticate gh api calls Signed-off-by: JP-Ellis --- scripts/release.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scripts/release.py b/scripts/release.py index 4f6420e02..823930152 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -17,6 +17,7 @@ import argparse import json import logging +import os import re import subprocess import sys @@ -648,6 +649,38 @@ def parse_existing_pr(gh_output: str) -> PullRequest | None: return PullRequest(number=data["number"], head_ref_name=data["headRefName"]) +def check_github_token() -> None: + """ + Ensure that a GitHub token is set for authenticated API access. + + In CI, the PAT is expected to be provided via the `GH_TOKEN` environment + variable. When running locally, if `GITHUB_TOKEN` is not already set, this + function will attempt to retrieve a token using the `gh` CLI tool (which may + be authenticated via `gh auth login`) and set it in the environment for + subsequent API calls. + + If no token can be found, a warning is logged and API requests may be + subject to stricter rate limits. + """ + if os.getenv("GITHUB_TOKEN"): + return + + if token := os.getenv("GH_TOKEN"): + logger.debug("Using GH_TOKEN from environment variable for authentication") + os.environ["GITHUB_TOKEN"] = token + return + + if "CI" not in os.environ: + logger.info("Generating a GitHub token for authenticated API access") + if token := subprocess.check_output( + ["gh", "auth", "token"], + text=True, + ).strip(): + os.environ["GITHUB_TOKEN"] = token + return + logger.warning("No GitHub token found. API requests may be rate-limited.") + + def main() -> None: """Entry point for the release management script.""" parser = argparse.ArgumentParser( @@ -674,6 +707,8 @@ def main() -> None: pkg = PACKAGES[args.package] + check_github_token() + if args.command == "tag": pkg.tag(dry_run=args.dry_run) elif args.command == "prepare": From 5eac330af953559832dbbdf68ade7ce963112baf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:22:50 +0000 Subject: [PATCH 1326/1376] chore(deps): update tests/compatibility_suite/definition digest to b03375f --- tests/compatibility_suite/definition | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/compatibility_suite/definition b/tests/compatibility_suite/definition index 1acfa1ecb..b03375f00 160000 --- a/tests/compatibility_suite/definition +++ b/tests/compatibility_suite/definition @@ -1 +1 @@ -Subproject commit 1acfa1ecbd9d63e4465c687b3cdd7e0d3ac5811c +Subproject commit b03375f00f5adf1346bd76edb0c7968cc0b495cf From 3e6a60251ad53f2fec6659dc0ec361af759c072a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 16 Apr 2026 12:46:43 +1000 Subject: [PATCH 1327/1376] chore: remove release label Signed-off-by: JP-Ellis --- .github/workflows/release-cli.yml | 4 ++-- .github/workflows/release-core.yml | 2 +- .github/workflows/release-ffi.yml | 4 ++-- scripts/release.py | 4 ---- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 85ecfbd81..0d4486891 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -110,8 +110,8 @@ jobs: tag: name: Create CLI release tag if: >- - github.event_name == 'pull_request' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, - 'release') && github.event.pull_request.head.ref == 'release/pact-python-cli' + github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.head.ref + == 'release/pact-python-cli' runs-on: ubuntu-latest environment: name: release-pr diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index 6959795fc..c47f706e0 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -108,7 +108,7 @@ jobs: name: Create release tag if: >- github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.head.ref - == 'release/pact-python' && contains(github.event.pull_request.labels.*.name, 'release') + == 'release/pact-python' runs-on: ubuntu-latest environment: name: release-pr diff --git a/.github/workflows/release-ffi.yml b/.github/workflows/release-ffi.yml index 014aef7b8..fb64dfe06 100644 --- a/.github/workflows/release-ffi.yml +++ b/.github/workflows/release-ffi.yml @@ -111,8 +111,8 @@ jobs: tag: name: Create FFI release tag if: >- - github.event_name == 'pull_request' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, - 'release') && github.event.pull_request.head.ref == 'release/pact-python-ffi' + github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.head.ref + == 'release/pact-python-ffi' runs-on: ubuntu-latest environment: name: release-pr diff --git a/scripts/release.py b/scripts/release.py index 823930152..9d152b271 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -227,8 +227,6 @@ def find_open_release_pr(self) -> PullRequest | None: "list", "--head", self.release_branch, - "--label", - "release", "--state", "open", "--json", @@ -415,8 +413,6 @@ def _push_release_branch( pr_title, "--body", changelog, - "--label", - "release", "--base", "main", "--draft", From 04032c5b4629d76ec646de8d0ca99ecaeec1d4ea Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 16 Apr 2026 14:19:31 +1000 Subject: [PATCH 1328/1376] chore: replace taplo with tombi The maintainer for taplo has gone on an indefinite break, so switching to tombi for TOML formatting and linting. This is also an opportunity to implement a few changes to the hatch config: - https://github.com/pypa/hatch/issues/1852 - https://github.com/pypa/hatch/issues/1639 Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 6 + .taplo.toml | 10 - .tombi.toml | 5 + .vscode/extensions.json | 4 +- committed.toml | 7 +- docs/scripts/.ruff.toml | 3 +- examples/catalog/pyproject.toml | 22 +- .../http/aiohttp_and_flask/pyproject.toml | 28 +- .../http/requests_and_fastapi/pyproject.toml | 27 +- .../service_consumer_provider/pyproject.toml | 25 +- examples/http/xml_example/pyproject.toml | 26 +- pact-python-cli/pyproject.toml | 244 +++++----- pact-python-ffi/pyproject.toml | 275 +++++------ pact-python-ffi/tests/.ruff.toml | 11 +- pyproject.toml | 430 ++++++++---------- tests/.ruff.toml | 11 +- 16 files changed, 518 insertions(+), 616 deletions(-) delete mode 100644 .taplo.toml create mode 100644 .tombi.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 710a0f22c..a0e865751 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -74,6 +74,12 @@ repos: mascot\.svg )$ + - repo: https://github.com/tombi-toml/tombi-pre-commit + rev: v0.9.3 + hooks: + - id: tombi-format + - id: tombi-lint + - repo: local hooks: # Mypy is difficult to run pre-commit's isolated environment as it needs diff --git a/.taplo.toml b/.taplo.toml deleted file mode 100644 index e5eeae78c..000000000 --- a/.taplo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[schema] -path = "taplo://taplo.toml" - -[formatting] -align_entries = true -indent_entries = false -indent_tables = true -reorder_arrays = false -reorder_inline_tables = true -reorder_keys = true diff --git a/.tombi.toml b/.tombi.toml new file mode 100644 index 000000000..622f59bd6 --- /dev/null +++ b/.tombi.toml @@ -0,0 +1,5 @@ +toml-version = "v1.1.0" + +[format] + [format.rules] + indent-sub-tables = true diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 59194417f..360cbadb8 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -11,7 +11,7 @@ "ms-python.mypy-type-checker", "ms-python.python", "redhat.vscode-yaml", - "tamasfe.even-better-toml", - "yzhang.markdown-all-in-one" + "yzhang.markdown-all-in-one", + "tombi-toml.tombi" ] } diff --git a/committed.toml b/committed.toml index 9cc157ebf..c6cbb305d 100644 --- a/committed.toml +++ b/committed.toml @@ -2,11 +2,12 @@ ## Configuration for committed ## ## See + style = "conventional" -line_length = 80 -merge_commit = false -no_fixup = false +line_length = 80 +merge_commit = false +no_fixup = false subject_capitalized = false allowed_types = [ diff --git a/docs/scripts/.ruff.toml b/docs/scripts/.ruff.toml index 21a6c84ee..ee6a3f3bc 100644 --- a/docs/scripts/.ruff.toml +++ b/docs/scripts/.ruff.toml @@ -1,7 +1,8 @@ #:schema https://www.schemastore.org/ruff.json + extend = "../../pyproject.toml" [lint] ignore = [ - "INP001", # Forbid implicit namespaces + "INP001", # Forbid implicit namespaces ] diff --git a/examples/catalog/pyproject.toml b/examples/catalog/pyproject.toml index 9e9ec1dac..7de8d8171 100644 --- a/examples/catalog/pyproject.toml +++ b/examples/catalog/pyproject.toml @@ -1,6 +1,7 @@ #:schema https://www.schemastore.org/pyproject.toml + [project] -name = "pact-python-catalog" +name = "pact-python-catalog" version = "1.0.0" dependencies = [ @@ -15,15 +16,16 @@ requires-python = ">=3.10" [dependency-groups] test = ["pact-python", "pytest~=9.0", "uvicorn~=0.30"] -[tool.uv.sources] -pact-python = { path = "../../" } +[tool] + [tool.uv.sources] + pact-python = { path = "../../" } -[tool.ruff] -extend = "../../pyproject.toml" + [tool.ruff] + extend = "../../pyproject.toml" -[tool.pytest] -addopts = ["--import-mode=importlib"] + [tool.pytest] + addopts = ["--import-mode=importlib"] -log_date_format = "%H:%M:%S" -log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" -log_level = "NOTSET" + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" diff --git a/examples/http/aiohttp_and_flask/pyproject.toml b/examples/http/aiohttp_and_flask/pyproject.toml index c783c3f1f..dba9a40ea 100644 --- a/examples/http/aiohttp_and_flask/pyproject.toml +++ b/examples/http/aiohttp_and_flask/pyproject.toml @@ -1,27 +1,29 @@ #:schema https://www.schemastore.org/pyproject.json + [project] name = "example-aiohttp-and-flask" description = "Example of using an aiohttp client and Flask server with Pact Python" -dependencies = ["aiohttp~=3.0", "flask~=3.0", "typing-extensions~=4.0"] +version = "1.0.0" requires-python = ">=3.10" -version = "1.0.0" +dependencies = ["aiohttp~=3.0", "flask~=3.0", "typing-extensions~=4.0"] [dependency-groups] -test = ["pact-python", "pytest-asyncio~=1.0", "pytest~=9.0"] +test = ["pact-python", "pytest~=9.0", "pytest-asyncio~=1.0"] -[tool.uv.sources] -pact-python = { path = "../../../" } +[tool] + [tool.pytest] + addopts = ["--import-mode=importlib"] -[tool.ruff] -extend = "../../../pyproject.toml" + asyncio_default_fixture_loop_scope = "session" -[tool.pytest] -addopts = ["--import-mode=importlib"] + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" -asyncio_default_fixture_loop_scope = "session" + [tool.ruff] + extend = "../../../pyproject.toml" -log_date_format = "%H:%M:%S" -log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" -log_level = "NOTSET" + [tool.uv.sources] + pact-python = { path = "../../../" } diff --git a/examples/http/requests_and_fastapi/pyproject.toml b/examples/http/requests_and_fastapi/pyproject.toml index be469ec87..03b32dfdf 100644 --- a/examples/http/requests_and_fastapi/pyproject.toml +++ b/examples/http/requests_and_fastapi/pyproject.toml @@ -1,28 +1,29 @@ #:schema https://www.schemastore.org/pyproject.json + [project] name = "example-requests-and-fastapi" description = "Example of using a requests client and FastAPI server with Pact Python" -dependencies = ["requests~=2.0", "fastapi~=0.0", "typing-extensions~=4.0"] +version = "1.0.0" requires-python = ">=3.10" -version = "1.0.0" +dependencies = ["fastapi~=0.0", "requests~=2.0", "typing-extensions~=4.0"] [dependency-groups] - test = ["pact-python", "pytest~=9.0", "uvicorn~=0.29"] -[tool.uv.sources] -pact-python = { path = "../../../" } +[tool] + [tool.pytest] + addopts = ["--import-mode=importlib"] -[tool.ruff] -extend = "../../../pyproject.toml" + asyncio_default_fixture_loop_scope = "session" -[tool.pytest] -addopts = ["--import-mode=importlib"] + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" -asyncio_default_fixture_loop_scope = "session" + [tool.ruff] + extend = "../../../pyproject.toml" -log_date_format = "%H:%M:%S" -log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" -log_level = "NOTSET" + [tool.uv.sources] + pact-python = { path = "../../../" } diff --git a/examples/http/service_consumer_provider/pyproject.toml b/examples/http/service_consumer_provider/pyproject.toml index 96ece6ee1..48fcbe198 100644 --- a/examples/http/service_consumer_provider/pyproject.toml +++ b/examples/http/service_consumer_provider/pyproject.toml @@ -1,26 +1,27 @@ #:schema https://www.schemastore.org/pyproject.json + [project] name = "example-service-consumer-provider" description = "Example of a service acting as both a Pact consumer and provider" -dependencies = ["fastapi~=0.0", "requests~=2.0", "typing-extensions~=4.0"] +version = "1.0.0" requires-python = ">=3.10" -version = "1.0.0" +dependencies = ["fastapi~=0.0", "requests~=2.0", "typing-extensions~=4.0"] [dependency-groups] - test = ["pact-python", "pytest~=9.0", "uvicorn~=0.29"] -[tool.uv.sources] -pact-python = { path = "../../../" } +[tool] + [tool.pytest] + addopts = ["--import-mode=importlib"] -[tool.ruff] -extend = "../../../pyproject.toml" + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" -[tool.pytest] -addopts = ["--import-mode=importlib"] + [tool.ruff] + extend = "../../../pyproject.toml" -log_date_format = "%H:%M:%S" -log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" -log_level = "NOTSET" + [tool.uv.sources] + pact-python = { path = "../../../" } diff --git a/examples/http/xml_example/pyproject.toml b/examples/http/xml_example/pyproject.toml index 3275a020e..3e72798e8 100644 --- a/examples/http/xml_example/pyproject.toml +++ b/examples/http/xml_example/pyproject.toml @@ -1,27 +1,29 @@ #:schema https://www.schemastore.org/pyproject.json + [project] name = "example-xml" description = "Example of XML contract testing with Pact Python" -dependencies = ["requests~=2.0", "fastapi~=0.0", "typing-extensions~=4.0"] +version = "1.0.0" requires-python = ">=3.10" -version = "1.0.0" +dependencies = ["fastapi~=0.0", "requests~=2.0", "typing-extensions~=4.0"] [dependency-groups] test = ["pact-python", "pytest~=9.0", "uvicorn~=0.29"] -[tool.uv.sources] -pact-python = { path = "../../../" } +[tool] + [tool.pytest] + addopts = ["--import-mode=importlib"] -[tool.ruff] -extend = "../../../pyproject.toml" + asyncio_default_fixture_loop_scope = "session" -[tool.pytest] -addopts = ["--import-mode=importlib"] + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" -asyncio_default_fixture_loop_scope = "session" + [tool.ruff] + extend = "../../../pyproject.toml" -log_date_format = "%H:%M:%S" -log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" -log_level = "NOTSET" + [tool.uv.sources] + pact-python = { path = "../../../" } diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index fb8ce0569..5e15d93d4 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -1,13 +1,14 @@ #:schema https://www.schemastore.org/pyproject.json + [project] +name = "pact-python-cli" +version = "2.5.7.0" description = "Pact CLI bundle for Python" -name = "pact-python-cli" -version = "2.5.7.0" -keywords = ["pact", "cli", "pact-python", "contract-testing"] -license = "MIT" -readme = "README.md" +readme = "README.md" +license = "MIT" +keywords = ["cli", "contract-testing", "pact", "pact-python"] -authors = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] +authors = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] maintainers = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] classifiers = [ @@ -18,160 +19,137 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", + "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", - "Programming Language :: Python", "Topic :: Software Development :: Testing", ] requires-python = ">=3.10" [project.urls] - "Bug Tracker" = "https://github.com/pact-foundation/pact-python/issues" - "Changelog" = "https://github.com/pact-foundation/pact-python/blob/main/pact-python-cli/CHANGELOG.md" + "Bug Tracker" = "https://github.com/pact-foundation/pact-python/issues" + "Changelog" = "https://github.com/pact-foundation/pact-python/blob/main/pact-python-cli/CHANGELOG.md" "Documentation" = "https://docs.pact.io" - "Homepage" = "https://pact.io" - "Repository" = "https://github.com/pact-foundation/pact-python" + "Homepage" = "https://pact.io" + "Repository" = "https://github.com/pact-foundation/pact-python" [project.scripts] - pact = "pact_cli:_exec" - pact-broker = "pact_cli:_exec" - pact-message = "pact_cli:_exec" - pact-mock-service = "pact_cli:_exec" - pact-plugin-cli = "pact_cli:_exec" + pact = "pact_cli:_exec" + pact-broker = "pact_cli:_exec" + pact-message = "pact_cli:_exec" + pact_mock_server_cli = "pact_cli:_exec" + pact-mock-service = "pact_cli:_exec" + pact-plugin-cli = "pact_cli:_exec" pact-provider-verifier = "pact_cli:_exec" - pact-stub-server = "pact_cli:_exec" - pact-stub-service = "pact_cli:_exec" - pact_mock_server_cli = "pact_cli:_exec" - pact_verifier_cli = "pact_cli:_exec" - pactflow = "pact_cli:_exec" + pact-stub-server = "pact_cli:_exec" + pact-stub-service = "pact_cli:_exec" + pact_verifier_cli = "pact_cli:_exec" + pactflow = "pact_cli:_exec" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.15.10", { include-group = "test" }, { include-group = "types" }] -test = ["pytest-cov~=7.0", "pytest~=9.0"] +dev = ["ruff==0.15.10", { include-group = "test" }, { include-group = "types" }] +test = ["pytest~=9.0", "pytest-cov~=7.0"] types = ["mypy==1.20.1"] -################################################################################ ## Build System -################################################################################ [build-system] +requires = ["hatchling", "packaging"] build-backend = "hatchling.build" -requires = ["hatchling", "packaging"] - -[tool.hatch] - - [tool.hatch.build] - packages = ["src/pact_cli"] - [tool.hatch.build.targets.wheel.hooks.custom] - patch = "hatch_build.py" - - ######################################## - ## Hatch Environment Configuration - ######################################## - [tool.hatch.envs] - - # Install dev dependencies in the default environment to simplify the developer - # workflow. - [tool.hatch.envs.default] - extra-dependencies = [ - "hatchling", - "packaging", - "requests", - "setuptools ; python_version >= '3.12'", +[tool] + [tool.cibuildwheel] + # The repair tool unfortunately did not like the bundled Ruby distributable, + # with false-positives missing libraries despite being bundled. + repair-wheel-command = "" + + [tool.cibuildwheel.windows] + archs = ["auto64"] + + [[tool.cibuildwheel.overrides]] + environment.MACOSX_DEPLOYMENT_TARGET = "10.13" + select = "*-macosx_x86_64" + + [[tool.cibuildwheel.overrides]] + environment.MACOSX_DEPLOYMENT_TARGET = "11.0" + select = "*-macosx_arm64" + + [tool.coverage] + [tool.coverage.paths] + pact-cli = ["/src/pact_cli"] + tests = ["/tests"] + + [tool.coverage.report] + exclude_lines = [ + "@(abc\\.)?abstractmethod", # Ignore abstract methods + "if TYPE_CHECKING:", # Ignore typing + "if __name__ == .__main__.:", # Ignore non-runnable code + "raise NotImplementedError", # Ignore defensive assertions ] - installer = "uv" - path = ".venv" - pre-install-commands = ["uv pip install --group dev -e ."] - - [tool.hatch.envs.default.scripts] - all = ["format", "lint", "test", "typecheck"] - format = "ruff format {args}" - lint = "ruff check --show-fixes {args}" - test = "pytest tests/ {args}" - typecheck = ["typecheck-src", "typecheck-tests"] - typecheck-src = "mypy src/ {args}" - typecheck-tests = "mypy tests/ {args}" - - # Test environment for running unit tests. This automatically tests against all - # supported Python versions. - [tool.hatch.envs.test] - installer = "uv" - path = ".venv/test" - pre-install-commands = ["uv pip install --group test -e ."] - - [[tool.hatch.envs.test.matrix]] - python = ["3.10", "3.11", "3.12", "3.13", "3.14"] - -################################################################################ -## PyTest Configuration -################################################################################ -[tool.pytest] -addopts = [ - "--import-mode=importlib", - # Coverage options - "--cov-config=pyproject.toml", - "--cov-report=xml", - "--cov=pact_cli", -] -log_date_format = "%H:%M:%S" -log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" -log_level = "NOTSET" - -################################################################################ -## Coverage -################################################################################ -[tool.coverage] - - [tool.coverage.paths] - pact-cli = ["/src/pact_cli"] - tests = ["/tests"] - - [tool.coverage.report] - exclude_lines = [ - "@(abc\\.)?abstractmethod", # Ignore abstract methods - "if TYPE_CHECKING:", # Ignore typing - "if __name__ == .__main__.:", # Ignore non-runnable code - "raise NotImplementedError", # Ignore defensive assertions + [tool.hatch] + [tool.hatch.build] + packages = ["src/pact_cli"] + + [tool.hatch.build.targets.wheel.hooks.custom] + patch = "hatch_build.py" + + [tool.hatch.envs] + [tool.hatch.envs.default] + installer = "uv" + path = ".venv" + extra-dependencies = [ + "hatchling", + "packaging", + "requests", + "setuptools ; python_version >= '3.12'", + ] + dependency-groups = ["dev"] + + [tool.hatch.envs.default.scripts] + all = ["format", "lint", "test", "typecheck"] + format = "ruff format {args}" + lint = "ruff check --show-fixes {args}" + test = "pytest tests/ {args}" + typecheck = ["typecheck-src", "typecheck-tests"] + typecheck-src = "mypy src/ {args}" + typecheck-tests = "mypy tests/ {args}" + + # Test environment for running unit tests. + [tool.hatch.envs.test] + installer = "uv" + path = ".venv/test" + dependency-groups = ["test"] + + [[tool.hatch.envs.test.matrix]] + python = ["3.10", "3.11", "3.12", "3.13", "3.14"] + + [tool.mypy] + # Overwrite the exclusions from the root pyproject.toml. + exclude = "" + + ## PyTest Configuration + [tool.pytest] + addopts = [ + # Coverage options + "--cov-config=pyproject.toml", + "--cov-report=xml", + "--cov=pact_cli", + "--import-mode=importlib", ] -################################################################################ -## Ruff Configuration -################################################################################ -[tool.ruff] -extend = "../pyproject.toml" - -exclude = [] - -################################################################################ -## Mypy Configuration -################################################################################ -[tool.mypy] -# Overwrite the exclusions from the root pyproject.toml. -exclude = '' - -################################################################################ -## CI Build Wheel -################################################################################ -[tool.cibuildwheel] -# The repair tool unfortunately did not like the bundled Ruby distributable, -# with false-positives missing libraries despite being bundled. -repair-wheel-command = "" - - [tool.cibuildwheel.windows] - archs = ["auto64"] - - [[tool.cibuildwheel.overrides]] - environment.MACOSX_DEPLOYMENT_TARGET = "10.13" - select = "*-macosx_x86_64" - - [[tool.cibuildwheel.overrides]] - environment.MACOSX_DEPLOYMENT_TARGET = "11.0" - select = "*-macosx_arm64" + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" + + ## Ruff Configuration + [tool.ruff] + extend = "../pyproject.toml" + + exclude = [] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 35dc65a8d..dc0e64993 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -1,13 +1,14 @@ #:schema https://www.schemastore.org/pyproject.json + [project] +name = "pact-python-ffi" +version = "0.4.28.2" description = "Python bindings for the Pact FFI library" -name = "pact-python-ffi" -version = "0.4.28.2" -keywords = ["pact", "ffi", "pact-python", "contract-testing"] -license = "MIT" -readme = "README.md" +readme = "README.md" +license = "MIT" +keywords = ["contract-testing", "ffi", "pact", "pact-python"] -authors = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] +authors = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] maintainers = [{ name = "Joshua Ellis", email = "josh@jpellis.me" }] classifiers = [ @@ -18,13 +19,13 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", + "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", - "Programming Language :: Python", "Topic :: Software Development :: Testing", ] @@ -33,163 +34,127 @@ requires-python = ">=3.10" dependencies = ["cffi~=2.0"] [project.urls] - "Bug Tracker" = "https://github.com/pact-foundation/pact-python/issues" - "Changelog" = "https://github.com/pact-foundation/pact-python/blob/main/pact-python-ffi/CHANGELOG.md" + "Bug Tracker" = "https://github.com/pact-foundation/pact-python/issues" + "Changelog" = "https://github.com/pact-foundation/pact-python/blob/main/pact-python-ffi/CHANGELOG.md" "Documentation" = "https://docs.pact.io" - "Homepage" = "https://pact.io" - "Repository" = "https://github.com/pact-foundation/pact-python" + "Homepage" = "https://pact.io" + "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.15.10", { include-group = "test" }, { include-group = "types" }] -test = ["pytest-cov~=7.0", "pytest~=9.0"] +dev = ["ruff==0.15.10", { include-group = "test" }, { include-group = "types" }] +test = ["pytest~=9.0", "pytest-cov~=7.0"] types = ["mypy==1.20.1", "typing-extensions~=4.0"] -################################################################################ -## Build System -################################################################################ [build-system] +requires = ["cffi", "hatchling", "packaging", "setuptools"] build-backend = "hatchling.build" -requires = ["hatchling", "packaging", "cffi", "setuptools"] - -[tool.hatch] - - [tool.hatch.build] - packages = ["src/pact_ffi"] - - [tool.hatch.build.targets.wheel.hooks.custom] - patch = "hatch_build.py" - - ######################################## - ## Hatch Environment Configuration - ######################################## - [tool.hatch.envs] - - # Install dev dependencies in the default environment to simplify the developer - # workflow. - [tool.hatch.envs.default] - extra-dependencies = ["hatchling", "packaging", "cffi"] - installer = "uv" - path = ".venv" - pre-install-commands = ["uv pip install --group dev -e ."] - - # Update paths to ensure the shared library can be found - # TODO: See if this can be overridden on a per-platform basis - # https://github.com/pypa/hatch/discussions/2024 - # [tool.hatch.envs.default.overrides] - # platform.windows.env-vars = { PATH = "{root}/src/pact_ffi;{env:PATH}" } - - [tool.hatch.envs.default.scripts] - all = ["format", "lint", "test", "typecheck"] - format = "ruff format {args}" - lint = "ruff check --show-fixes {args}" - test = "pytest tests/ {args}" - typecheck = ["typecheck-src", "typecheck-tests"] - typecheck-src = "mypy src/ {args}" - typecheck-tests = "mypy tests/ {args}" - - # Test environment for running unit tests. This automatically tests against all - # supported Python versions. - [tool.hatch.envs.test] - installer = "uv" - path = ".venv/test" - pre-install-commands = ["uv pip install --group test -e ."] - - # Update paths to ensure the shared library can be found - # TODO: See if this can be overridden on a per-platform basis - # https://github.com/pypa/hatch/discussions/2024 - # [tool.hatch.envs.default.overrides] - # platform.windows.env-vars = { PATH = "{root}/src/pact_ffi;{env:PATH}" } - - [[tool.hatch.envs.test.matrix]] - python = ["3.10", "3.11", "3.12", "3.13", "3.14"] - -################################################################################ -## PyTest Configuration -################################################################################ -[tool.pytest] -addopts = [ - "--import-mode=importlib", - # Coverage options - "--cov-config=pyproject.toml", - "--cov-report=xml", - "--cov=pact_ffi", -] -log_date_format = "%H:%M:%S" -log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" -log_level = "NOTSET" - -################################################################################ -## Coverage -################################################################################ -[tool.coverage] - - [tool.coverage.paths] - pact-ffi = ["/src/pact_ffi"] - tests = ["/tests"] - - [tool.coverage.report] - exclude_lines = [ - "@(abc\\.)?abstractmethod", # Ignore abstract methods - "if TYPE_CHECKING:", # Ignore typing - "if __name__ == .__main__.:", # Ignore non-runnable code - "raise NotImplementedError", # Ignore defensive assertions +[tool] + [tool.cibuildwheel] + environment.HATCH_VERBOSE = "1" + + [tool.cibuildwheel.linux] + before-build = ["uv pip install --system abi3audit"] + environment.PACT_LIB_DIR = "/tmp/pact_ffi" + repair-wheel-command = [ + "export LD_LIBRARY_PATH=\"$PACT_LIB_DIR:$LD_LIBRARY_PATH\"", + "auditwheel repair -w {dest_dir} {wheel}", + "abi3audit --strict --report {wheel}", + ] + + [tool.cibuildwheel.macos] + before-build = ["pip install abi3audit"] + environment.PACT_LIB_DIR = "/tmp/pact_ffi" + repair-wheel-command = [ + "export DYLD_LIBRARY_PATH=\"$PACT_LIB_DIR:$DYLD_LIBRARY_PATH\"", + "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}", + "abi3audit --strict --report {wheel}", + ] + + [tool.cibuildwheel.windows] + archs = ["auto64"] + before-build = ["pip install abi3audit delvewheel"] + environment.PACT_LIB_DIR = "C:/tmp/pact_ffi" + repair-wheel-command = [ + "delvewheel repair -vv --add-path \"%PACT_LIB_DIR%\" -w {dest_dir} {wheel}", + "abi3audit --strict --report {wheel}", + ] + + [[tool.cibuildwheel.overrides]] + environment.MACOSX_DEPLOYMENT_TARGET = "12.0" + inherit.environment = "append" + select = "*-macosx_x86_64" + + [[tool.cibuildwheel.overrides]] + environment.MACOSX_DEPLOYMENT_TARGET = "12.0" + inherit.environment = "append" + select = "*-macosx_arm64" + + [tool.coverage] + [tool.coverage.paths] + pact-ffi = ["/src/pact_ffi"] + tests = ["/tests"] + + [tool.coverage.report] + exclude_lines = [ + "@(abc\\.)?abstractmethod", # Ignore abstract methods + "if TYPE_CHECKING:", # Ignore typing + "if __name__ == .__main__.:", # Ignore non-runnable code + "raise NotImplementedError", # Ignore defensive assertions + ] + + [tool.hatch] + [tool.hatch.build] + packages = ["src/pact_ffi"] + + [tool.hatch.build.targets.wheel.hooks.custom] + patch = "hatch_build.py" + + [tool.hatch.envs] + [tool.hatch.envs.default] + installer = "uv" + path = ".venv" + extra-dependencies = ["hatchling", "packaging", "cffi"] + dependency-groups = ["dev"] + + [tool.hatch.envs.default.scripts] + all = ["format", "lint", "test", "typecheck"] + format = "ruff format {args}" + lint = "ruff check --show-fixes {args}" + test = "pytest tests/ {args}" + typecheck = ["typecheck-src", "typecheck-tests"] + typecheck-src = "mypy src/ {args}" + typecheck-tests = "mypy tests/ {args}" + + # Test environment for running unit tests. + [tool.hatch.envs.test] + installer = "uv" + path = ".venv/test" + dependency-groups = ["test"] + + [[tool.hatch.envs.test.matrix]] + python = ["3.10", "3.11", "3.12", "3.13", "3.14"] + + [tool.mypy] + # Overwrite the exclusions from the root pyproject.toml. + exclude = "" + + ## PyTest Configuration + [tool.pytest] + addopts = [ + # Coverage options + "--cov-config=pyproject.toml", + "--cov-report=xml", + "--cov=pact_ffi", + "--import-mode=importlib", ] -################################################################################ -## Ruff Configuration -################################################################################ -[tool.ruff] -extend = "../pyproject.toml" - -exclude = [] - -################################################################################ -## Mypy Configuration -################################################################################ -[tool.mypy] -# Overwrite the exclusions from the root pyproject.toml. -exclude = '' - -################################################################################ -## CI Build Wheel -################################################################################ -[tool.cibuildwheel] -environment.HATCH_VERBOSE = "1" - - [tool.cibuildwheel.linux] - before-build = ["uv pip install --system abi3audit"] - environment.PACT_LIB_DIR = "/tmp/pact_ffi" - repair-wheel-command = [ - "export LD_LIBRARY_PATH=\"$PACT_LIB_DIR:$LD_LIBRARY_PATH\"", - "auditwheel repair -w {dest_dir} {wheel}", - "abi3audit --strict --report {wheel}", - ] - - [tool.cibuildwheel.macos] - before-build = ["pip install abi3audit"] - environment.PACT_LIB_DIR = "/tmp/pact_ffi" - repair-wheel-command = [ - "export DYLD_LIBRARY_PATH=\"$PACT_LIB_DIR:$DYLD_LIBRARY_PATH\"", - "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}", - "abi3audit --strict --report {wheel}", - ] - - [tool.cibuildwheel.windows] - archs = ["auto64"] - before-build = ["pip install abi3audit delvewheel"] - environment.PACT_LIB_DIR = "C:/tmp/pact_ffi" - repair-wheel-command = [ - "delvewheel repair -vv --add-path \"%PACT_LIB_DIR%\" -w {dest_dir} {wheel}", - "abi3audit --strict --report {wheel}", - ] + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" - [[tool.cibuildwheel.overrides]] - environment.MACOSX_DEPLOYMENT_TARGET = "12.0" - inherit.environment = "append" - select = "*-macosx_x86_64" + ## Ruff Configuration + [tool.ruff] + extend = "../pyproject.toml" - [[tool.cibuildwheel.overrides]] - environment.MACOSX_DEPLOYMENT_TARGET = "12.0" - inherit.environment = "append" - select = "*-macosx_arm64" + exclude = [] diff --git a/pact-python-ffi/tests/.ruff.toml b/pact-python-ffi/tests/.ruff.toml index 3558b7bba..ee08a6db7 100644 --- a/pact-python-ffi/tests/.ruff.toml +++ b/pact-python-ffi/tests/.ruff.toml @@ -1,16 +1,17 @@ #:schema https://www.schemastore.org/ruff.json + extend = "../pyproject.toml" # We have a number of helper files which contain assertions/magic values, etc. [lint] ignore = [ - "D102", # Require docstring in public methods - "D103", # Require docstring in public function - "D104", # Require docstring in public package + "D102", # Require docstring in public methods + "D103", # Require docstring in public function + "D104", # Require docstring in public package "INP001", # Forbid implicit namespaces - "PLR2004", # Forbid magic values + "PLR2004", # Forbid magic values "RUF018", # Forbid assignment in assertions - "S101", # Forbid assert statements + "S101", # Forbid assert statements "TID252", # Require absolute imports ] diff --git a/pyproject.toml b/pyproject.toml index 31e55c8e1..59043c67b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,12 @@ #:schema https://www.schemastore.org/pyproject.json + [project] +name = "pact-python" +version = "3.2.1" description = "Tool for creating and verifying consumer-driven contracts using the Pact framework." -name = "pact-python" -version = "3.2.1" +readme = "README.md" +license = { file = "LICENSE" } keywords = ["contract-testing", "pact", "testing"] -license = { file = "LICENSE" } -readme = "README.md" authors = [ { name = "Joshua Ellis", email = "josh@jpellis.me" }, @@ -21,13 +22,13 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", + "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", - "Programming Language :: Python", "Topic :: Software Development :: Testing", ] @@ -41,17 +42,17 @@ requires-python = ">=3.10" dependencies = [ # Pact dependencies "pact-python-ffi~=0.4.0", + "typing-extensions~=4.0 ; python_version < '3.13'", # External dependencies "yarl~=1.0", - "typing-extensions~=4.0 ; python_version < '3.13'", ] [project.urls] - changelog = "https://github.com/pact-foundation/pact-python/blob/main/CHANGELOG.md" + changelog = "https://github.com/pact-foundation/pact-python/blob/main/CHANGELOG.md" documentation = "https://pact-foundation.github.io/pact-python/" - homepage = "https://pact.io" - issues = "https://github.com/pact-foundation/pact-python/issues" - source = "https://github.com/pact-foundation/pact-python" + homepage = "https://pact.io" + issues = "https://github.com/pact-foundation/pact-python/issues" + source = "https://github.com/pact-foundation/pact-python" [project.scripts] pact-verifier = "pact.v2.cli.verify:main" @@ -75,10 +76,10 @@ dev = [ "ruff==0.15.10", { include-group = "docs" }, { include-group = "example" }, - { include-group = "test" }, - { include-group = "types" }, { include-group = "example-v2" }, + { include-group = "test" }, { include-group = "test-v2" }, + { include-group = "types" }, ] docs = [ @@ -86,13 +87,13 @@ docs = [ "griffe-inherited-method-crossrefs==0.0.1.4", "griffe-pydantic==1.3.1", "griffe-warnings-deprecated==1.1.1", + "mkdocs==1.6.1", "mkdocs-gen-files==0.6.1", "mkdocs-github-admonitions-plugin==0.1.1", "mkdocs-literate-nav==0.6.3", "mkdocs-llmstxt==0.5.0", "mkdocs-material[recommended,git,imaging]==9.7.6", "mkdocs-section-index==0.3.11", - "mkdocs==1.6.1", "mkdocstrings[python]==1.0.4", "pathspec==1.0.4", ] @@ -102,9 +103,9 @@ example = [ "grpcio~=1.0", "protobuf~=7.34", "pydantic~=2.0", + "pytest~=9.0", "pytest-cov~=7.0", "pytest-rerunfailures~=16.0", - "pytest~=9.0", "python-multipart~=0.0", "testcontainers~=4.0", "uvicorn[standard]~=0.0", @@ -113,11 +114,11 @@ test = [ "aiohttp~=3.0", "flask~=3.0", "pact-python-cli", + "pytest~=9.0", "pytest-asyncio~=1.0", "pytest-bdd~=8.0", "pytest-cov~=7.0", "pytest-rerunfailures~=16.0", - "pytest~=9.0", "requests~=2.0", "testcontainers~=4.0", ] @@ -126,17 +127,16 @@ types = [ "types-grpcio~=1.0", "types-protobuf~=7.34", "types-requests~=2.0", - # This is required for Python 3.10 support - "typing-extensions~=4.0", + "typing-extensions~=4.0", # For Python 3.10 support ] # Dependencies for v2 example and test environments example-v2 = [ "fastapi~=0.0", "flask[async]~=3.0", + "pytest~=9.0", "pytest-cov~=7.0", "pytest-rerunfailures~=16.0", - "pytest~=9.0", "testcontainers~=4.0", "uvicorn[standard]~=0.0", ] @@ -144,249 +144,195 @@ test-v2 = [ "fastapi~=0.0", "httpx~=0.0", "mock~=5.0", + "pytest~=9.0", "pytest-cov~=7.0", "pytest-rerunfailures~=16.0", - "pytest~=9.0", "uvicorn[standard]~=0.0", ] -################################################################################ -## Hatch Configuration -################################################################################ [build-system] +requires = ["hatchling"] build-backend = "hatchling.build" -requires = ["hatchling"] - -[tool.hatch] - - [tool.hatch.build] - packages = ["src/pact"] - - ######################################## - ## Hatch Environment Configuration - ######################################## - [tool.hatch.envs] - - # Install dev dependencies in the default environment to simplify the developer - # workflow. - [tool.hatch.envs.default] - extra-dependencies = ["hatchling", "packaging"] - installer = "uv" - path = ".venv" - # This is require to get around an incompatibility between hatch and uv - # See: https://github.com/pypa/hatch/issues/1639 - # See: https://github.com/pypa/hatch/issues/1852 - pre-install-commands = [ - "uv pip install --group dev -e .", - "uv pip install --group dev -e ./pact-python-ffi", - "uv pip install --group dev -e ./pact-python-cli", - ] # - - # Update paths to ensure the shared library can be found - # TODO: See if this can be overridden on a per-platform basis - # https://github.com/pypa/hatch/discussions/2024 - # [tool.hatch.envs.default.overrides] - # platform.windows.env-vars = { PATH = "{root}/src/pact_ffi;{env:PATH}" } - - [tool.hatch.envs.default.scripts] - all = ["example", "format", "lint", "test", "typecheck"] - docs = "mkdocs serve {args}" - docs-build = "mkdocs build {args}" - example = "pytest --ignore=examples/v2 examples/ {args}" - format = "ruff format {args}" - lint = "ruff check --output-format=full --show-fixes {args}" - test = "pytest --ignore=tests/v2 tests/ {args}" - typecheck = ["typecheck-examples", "typecheck-src", "typecheck-tests"] - typecheck-examples = "mypy examples/ {args}" - typecheck-src = "mypy src/ {args}" - typecheck-tests = "mypy tests/ {args}" - - # Test environment for running unit tests. - [tool.hatch.envs.test] - installer = "uv" - path = ".venv/test" - pre-install-commands = ["uv pip install --group test -e ."] - - [[tool.hatch.envs.test.matrix]] - python = ["3.10", "3.11", "3.12", "3.13", "3.14"] - - # Test environment for running unit tests. This automatically tests against all - # supported Python versions. - [tool.hatch.envs.example] - installer = "uv" - path = ".venv/example" - pre-install-commands = ["uv pip install --group example -e ."] - - [tool.hatch.envs.example.scripts] - all = ["example"] - - [[tool.hatch.envs.example.matrix]] - python = ["3.10", "3.11", "3.12", "3.13", "3.14"] - - [tool.hatch.envs.v2-test] - features = ["compat-v2"] - installer = "uv" - path = ".venv/v2-test" - pre-install-commands = ["uv pip install --group test-v2 -e ."] - - [tool.hatch.envs.v2-test.scripts] - all = ["test"] - test = "pytest tests/v2 {args}" - - [[tool.hatch.envs.v2-test.matrix]] - python = ["3.10", "3.11", "3.12", "3.13", "3.14"] - - [tool.hatch.envs.v2-example] - features = ["compat-v2"] - installer = "uv" - path = ".venv/v2-example" - pre-install-commands = ["uv pip install --group example-v2 -e ."] - - [tool.hatch.envs.v2-example.scripts] - all = ["example"] - example = "pytest examples/v2 {args}" - - [[tool.hatch.envs.v2-example.matrix]] - python = ["3.10", "3.11", "3.12", "3.13", "3.14"] - -################################################################################ -## UV Workspace -################################################################################ -[tool.uv] - [tool.uv.sources] - pact-python-cli = { workspace = true } - pact-python-ffi = { workspace = true } - - [tool.uv.workspace] - members = ["pact-python-cli", "pact-python-ffi"] - -################################################################################ -## PyTest Configuration -################################################################################ -[tool.pytest] -addopts = [ - "--import-mode=importlib", - # Coverage options - "--cov-config=pyproject.toml", - "--cov-report=xml", - "--cov=pact", - # Reruns - "--reruns=5", -] -asyncio_default_fixture_loop_scope = "session" +[tool] + [tool.coverage] + [tool.coverage.paths] + pact = ["/src/pact"] + tests = ["/examples", "/tests"] + + [tool.coverage.report] + exclude_lines = [ + "@(abc\\.)?abstractmethod", # Ignore abstract methods + "if TYPE_CHECKING:", # Ignore typing + "if __name__ == .__main__.:", # Ignore non-runnable code + "raise NotImplementedError", # Ignore defensive assertions + ] -filterwarnings = [ - "ignore::DeprecationWarning:examples", - "ignore::DeprecationWarning:pact", - "ignore::DeprecationWarning:tests", -] + [tool.hatch] + [tool.hatch.build] + packages = ["src/pact"] + + [tool.hatch.envs] + [tool.hatch.envs.default] + installer = "uv" + path = ".venv" + extra-dependencies = ["hatchling", "packaging"] + dependency-groups = ["dev"] + + [tool.hatch.envs.default.workspace] + parallel = true + members = [ + { path = "pact-python-cli", group = "dev" }, + { path = "pact-python-ffi", group = "dev" }, + ] + + [tool.hatch.envs.default.scripts] + all = ["example", "format", "lint", "test", "typecheck"] + docs = "mkdocs serve {args}" + docs-build = "mkdocs build {args}" + example = "pytest --ignore=examples/v2 examples/ {args}" + format = "ruff format {args}" + lint = "ruff check --output-format=full --show-fixes {args}" + test = "pytest --ignore=tests/v2 tests/ {args}" + typecheck = ["typecheck-examples", "typecheck-src", "typecheck-tests"] + typecheck-examples = "mypy examples/ {args}" + typecheck-src = "mypy src/ {args}" + typecheck-tests = "mypy tests/ {args}" + + # Test environment for running unit tests. + [tool.hatch.envs.test] + installer = "uv" + path = ".venv/test" + dependency-groups = ["test"] + + [[tool.hatch.envs.test.matrix]] + python = ["3.10", "3.11", "3.12", "3.13", "3.14"] + + [tool.hatch.envs.example] + installer = "uv" + path = ".venv/example" + dependency-groups = ["example"] + + [[tool.hatch.envs.example.matrix]] + python = ["3.10", "3.11", "3.12", "3.13", "3.14"] + + [tool.hatch.envs.v2-test] + features = ["compat-v2"] + installer = "uv" + path = ".venv/v2-test" + pre-install-commands = ["uv pip install --group test-v2 -e ."] + + [tool.hatch.envs.v2-test.scripts] + all = ["test"] + test = "pytest tests/v2 {args}" + + [[tool.hatch.envs.v2-test.matrix]] + python = ["3.10", "3.11", "3.12", "3.13", "3.14"] + + [tool.hatch.envs.v2-example] + features = ["compat-v2"] + installer = "uv" + path = ".venv/v2-example" + pre-install-commands = ["uv pip install --group example-v2 -e ."] + + [tool.hatch.envs.v2-example.scripts] + all = ["example"] + example = "pytest examples/v2 {args}" + + [[tool.hatch.envs.v2-example.matrix]] + python = ["3.10", "3.11", "3.12", "3.13", "3.14"] + + [tool.mypy] + exclude = """(?x)^( + (src/pact|tests|examples)/v2/.*\\.pyi? + )$""" + + [tool.pytest] + addopts = [ + # Coverage options + "--cov-config=pyproject.toml", + "--cov-report=xml", + "--cov=pact", + "--import-mode=importlib", + # Reruns + "--reruns=5", + ] -log_date_format = "%H:%M:%S" -log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" -log_level = "NOTSET" + asyncio_default_fixture_loop_scope = "session" -markers = [ - # Marker for tests that require a container - "container", + filterwarnings = [ + "ignore::DeprecationWarning:examples", + "ignore::DeprecationWarning:pact", + "ignore::DeprecationWarning:tests", + ] - # Markers for the compatibility suite - "consumer", - "message", - "provider", -] + log_date_format = "%H:%M:%S" + log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s" + log_level = "NOTSET" -################################################################################ -## Coverage -################################################################################ -[tool.coverage] - - [tool.coverage.paths] - pact = ["/src/pact"] - tests = ["/examples", "/tests"] - - [tool.coverage.report] - exclude_lines = [ - "@(abc\\.)?abstractmethod", # Ignore abstract methods - "if TYPE_CHECKING:", # Ignore typing - "if __name__ == .__main__.:", # Ignore non-runnable code - "raise NotImplementedError", # Ignore defensive assertions - ] + markers = [ + # Marker for tests that require a container + "container", -################################################################################ -## Ruff Configuration -################################################################################ -[tool.ruff] -extend-exclude = ["src/pact/v2/*", "tests/v2/*", "examples/v2/*"] - - [tool.ruff.lint] - select = ["ALL"] - - ignore = [ - "D200", # Require single line docstrings to be on one line. - "D203", # Require blank line before class docstring - "D212", # Multi-line docstring summary must start at the first line - "FIX002", # Forbid TODO in comments - "TD002", # Assign someone to 'TODO' comments - - # The following are disabled for compatibility with the formatter - "COM812", # enforce trailing commas - "ISC001", # require imports to be sorted + # Markers for the compatibility suite + "consumer", + "message", + "provider", ] - [tool.ruff.lint.pyupgrade] - keep-runtime-typing = true - - [tool.ruff.lint.pydocstyle] - convention = "google" + [tool.ruff] + extend-exclude = ["src/pact/v2/*", "tests/v2/*", "examples/v2/*"] - [tool.ruff.lint.isort] - known-first-party = ["pact", "pact_cli", "pact_ffi"] + [tool.ruff.lint] + select = ["ALL"] - [tool.ruff.lint.flake8-tidy-imports] - ban-relative-imports = "all" + ignore = [ + "D200", # Require single line docstrings to be on one line. + "D203", # Require blank line before class docstring + "D212", # Multi-line docstring summary must start at the first line + "FIX002", # Forbid TODO in comments + "TD002", # Assign someone to 'TODO' comments - [tool.ruff.lint.per-file-ignores] - "test_*.py" = [ - "D103", # Require docstring in public function - "D104", # Require docstring in public package - "INP001", # Forbid implicit namespaces - "PLR2004", # Forbid magic values - "RUF018", # Forbid assignment in assertions - "S101", # Forbid assert statements - "TID252", # Require absolute imports + # The following are disabled for compatibility with the formatter + "COM812", # enforce trailing commas + "ISC001", # require imports to be sorted ] + [tool.ruff.lint.pyupgrade] + keep-runtime-typing = true + + [tool.ruff.lint.pydocstyle] + convention = "google" + + [tool.ruff.lint.isort] + known-first-party = ["pact", "pact_cli", "pact_ffi"] + + [tool.ruff.lint.flake8-tidy-imports] + ban-relative-imports = "all" + + [tool.ruff.lint.per-file-ignores] + "test_*.py" = [ + "D103", # Require docstring in public function + "D104", # Require docstring in public package + "INP001", # Forbid implicit namespaces + "PLR2004", # Forbid magic values + "RUF018", # Forbid assignment in assertions + "S101", # Forbid assert statements + "TID252", # Require absolute imports + ] + + [tool.ruff.format] + docstring-code-format = true + preview = true + + [tool.typos] + [tool.typos.default] + extend-ignore-re = [ + # Ignore spelling on a specific line + "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", + # Ignore spelling in a specific block + "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on", + ] - [tool.ruff.format] - docstring-code-format = true - preview = true - -################################################################################ -## Mypy Configuration -################################################################################ -[tool.mypy] -exclude = """(?x)^( - (src/pact|tests|examples)/v2/.*\\.pyi? -)$""" - -################################################################################ -## CI Build Wheel -################################################################################ -[tool.cibuildwheel] - -################################################################################ -## Typos -################################################################################ -[tool.typos] - - [tool.typos.default] - extend-ignore-re = [ - # Ignore spelling on a specific line - "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", - # Ignore spelling in a specific block - "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on", - ] - - [tool.typos.files] - extend-exclude = ["*.svg"] + [tool.typos.files] + extend-exclude = ["*.svg"] diff --git a/tests/.ruff.toml b/tests/.ruff.toml index 3558b7bba..ee08a6db7 100644 --- a/tests/.ruff.toml +++ b/tests/.ruff.toml @@ -1,16 +1,17 @@ #:schema https://www.schemastore.org/ruff.json + extend = "../pyproject.toml" # We have a number of helper files which contain assertions/magic values, etc. [lint] ignore = [ - "D102", # Require docstring in public methods - "D103", # Require docstring in public function - "D104", # Require docstring in public package + "D102", # Require docstring in public methods + "D103", # Require docstring in public function + "D104", # Require docstring in public package "INP001", # Forbid implicit namespaces - "PLR2004", # Forbid magic values + "PLR2004", # Forbid magic values "RUF018", # Forbid assignment in assertions - "S101", # Forbid assert statements + "S101", # Forbid assert statements "TID252", # Require absolute imports ] From 67f3b85ee0e17d4f1d7165fa41e23c83fecf269d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 05:01:10 +0000 Subject: [PATCH 1329/1376] chore(deps): update pre-commit hook tombi-toml/tombi-pre-commit to v0.9.17 (#1558) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a0e865751..a29791495 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -75,7 +75,7 @@ repos: )$ - repo: https://github.com/tombi-toml/tombi-pre-commit - rev: v0.9.3 + rev: v0.9.17 hooks: - id: tombi-format - id: tombi-lint From be29a3a34c1540a80c58a571a0720a95c84b88fb Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 16 Apr 2026 19:01:16 +1000 Subject: [PATCH 1330/1376] fix: bundle both ruby and rust CLIs Until recently, the `pact-standalone` upstream CLI bundled both Ruby and Rust CLIs together. The Ruby CLIs are being deprecated and will be removed, and the `pact-cli` repository will contain the Rust-based CLI which replaces all of the Ruby ones. For now, this bundles both to preserve compatibility, and adds a deprecation warning in preparation for the upcoming removal. Signed-off-by: JP-Ellis --- pact-python-cli/hatch_build.py | 95 ++++++++++++++++++++++-- pact-python-cli/src/pact_cli/__init__.py | 44 +++++++---- pact-python-cli/tests/test_init.py | 39 ++++++++++ 3 files changed, 155 insertions(+), 23 deletions(-) diff --git a/pact-python-cli/hatch_build.py b/pact-python-cli/hatch_build.py index 650b356c0..942177972 100644 --- a/pact-python-cli/hatch_build.py +++ b/pact-python-cli/hatch_build.py @@ -24,8 +24,15 @@ from packaging.tags import sys_tags PKG_DIR = Path(__file__).parent.resolve() / "src" / "pact_cli" + +# Remove when pact-standalone is removed PACT_CLI_URL = "https://github.com/pact-foundation/pact-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" +# Remove fixed version and infer from package metadata when pact-cli versioning +# is adopted. +PACT_RUST_CLI_VERSION = "0.9.5" +PACT_RUST_CLI_URL = "https://github.com/pact-foundation/pact-cli/releases/download/v{version}/pact-{arch}-{os}{ext}" + class UnsupportedPlatformError(RuntimeError): """ @@ -83,6 +90,7 @@ def clean(self, versions: Sequence[str]) -> None: # noqa: ARG002 passed to `hatch build`. """ for subdir in ["bin", "lib", "data"]: + # TODO(epoch-transition): Remove "lib" when standalone dropped # noqa: TD003 shutil.rmtree(PKG_DIR / subdir, ignore_errors=True) def initialize( @@ -116,7 +124,8 @@ def initialize( raise ValueError(msg) try: - build_data["force_include"] = self._install(cli_version) + build_data["force_include"] = self._install_ruby_cli(cli_version) + self._install_rust_cli() except UnsupportedPlatformError as err: msg = f"Pact CLI is not available for {err.platform}." self.app.display_error(msg) @@ -132,9 +141,9 @@ def _sys_tag_platform(self) -> str: """ return next(t.platform for t in sys_tags()) - def _install(self, version: str) -> Mapping[str, str]: + def _install_ruby_cli(self, version: str) -> Mapping[str, str]: """ - Install the Pact standalone binaries. + Install the Pact Ruby standalone binaries. The binaries are installed in `src/pact_cli/bin`, and the relevant version for the current operating system is determined automatically. @@ -148,25 +157,26 @@ def _install(self, version: str) -> Mapping[str, str]: wheel. Each `src` is a full path in the current filesystem, and the `dst` is the corresponding path within the wheel. """ - url = self._pact_bin_url(version) + url = self._pact_ruby_bin_url(version) artefact = self._download(url) self._extract(artefact) return { str(PKG_DIR / "bin"): "pact_cli/bin", + # TODO(epoch-transition): Remove lib/ when standalone dropped # noqa: TD003 str(PKG_DIR / "lib"): "pact_cli/lib", } - def _pact_bin_url(self, version: str) -> str: + def _pact_ruby_bin_url(self, version: str) -> str: """ - Generate the download URL for the Pact binaries. + Generate the download URL for the Pact Ruby binaries. Args: version: The Pact CLI version to download. Returns: - The URL to download the Pact binaries from. If the platform is not - supported, the resulting URL may be invalid. + The URL to download the Pact Ruby binaries from. If the platform is + not supported, the resulting URL may be invalid. """ platform = self._sys_tag_platform() @@ -196,6 +206,75 @@ def _pact_bin_url(self, version: str) -> str: ext=ext, ) + def _pact_rust_bin_url(self, version: str) -> str: + """ + Generate the download URL for the Rust pact binary from pact-cli. + + The pact-cli release assets are plain binaries (not archives) named + ``pact-{arch}-{os}`` (e.g. ``pact-aarch64-macos``), with ``.exe`` + appended on Windows. + + Args: + version: + The pact-cli version to download. + + Returns: + The URL to the pact binary asset. + + Raises: + UnsupportedPlatformError: + If the current platform's OS or architecture is not recognised. + """ + platform = self._sys_tag_platform() + + if platform.startswith("macosx"): + os_name = "macos" + ext = "" + elif "linux" in platform: + # musl-based Linux targets (e.g. Alpine) are not distinguished; + # the linux-gnu binary is used for all Linux targets. + os_name = "linux-gnu" + ext = "" + elif platform.startswith("win"): + os_name = "windows-msvc" + ext = ".exe" + else: + raise UnsupportedPlatformError(platform) + + if platform.endswith(("arm64", "aarch64")): + arch = "aarch64" + elif platform.endswith(("x86_64", "amd64")): + arch = "x86_64" + else: + raise UnsupportedPlatformError(platform) + + return PACT_RUST_CLI_URL.format( + version=version, + arch=arch, + os=os_name, + ext=ext, + ) + + def _install_rust_cli(self) -> None: + """ + Install the Rust pact binary from pact-cli. + + Overwrites the `pact` binary bundled with pact-standalone. + + The binary is downloaded from the pact-cli GitHub release as a plain + executable (not an archive) and placed in `bin/` as `pact` + (or `pact.exe` on Windows). + """ + url = self._pact_rust_bin_url(PACT_RUST_CLI_VERSION) + artefact = self._download(url) + + bin_name = "pact.exe" if sys.platform == "win32" else "pact" + dest = PKG_DIR / "bin" / bin_name + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(artefact, dest) + if sys.platform != "win32": + dest.chmod(0o755) + def _extract(self, artefact: Path) -> None: """ Extract the Pact binaries. diff --git a/pact-python-cli/src/pact_cli/__init__.py b/pact-python-cli/src/pact_cli/__init__.py index 02c0fab1a..0d6c85a03 100644 --- a/pact-python-cli/src/pact_cli/__init__.py +++ b/pact-python-cli/src/pact_cli/__init__.py @@ -43,16 +43,22 @@ ) if TYPE_CHECKING: - from collections.abc import Container, Mapping + from collections.abc import Mapping _USE_SYSTEM_BINS = os.getenv("PACT_USE_SYSTEM_BINS", "").upper() in ("TRUE", "YES") _BIN_DIR = Path(__file__).parent.resolve() / "bin" -_LEGACY_BINS: Container[str] = frozenset(( - "pact-message", - "pact-mock-service", - "pact-provider-verifier", - "pact-stub-service", -)) +_DEPRECATED_COMMANDS: Mapping[str, str | None] = { + "pact-broker": "pact broker", + "pact-message": None, # being removed; no Rust equivalent + "pact-mock-service": "pact mock", + "pact-plugin-cli": "pact plugin", + "pact-provider-verifier": "pact verifier", + "pact-stub-server": "pact stub", + "pact-stub-service": "pact stub", + "pact_mock_server_cli": "pact mock", + "pact_verifier_cli": "pact verifier", + "pactflow": "pact pactflow", +} def _telemetry_env() -> Mapping[str, str]: @@ -108,14 +114,22 @@ def _exec() -> None: print("Unknown command:", command, file=sys.stderr) # noqa: T201 sys.exit(1) - if command in _LEGACY_BINS: - warnings.warn( - f"The '{command}' executable is deprecated and will be removed in " - "a future release. Please migrate to the new Pact CLI tools. " - "See: ", - DeprecationWarning, - stacklevel=2, - ) + if command in _DEPRECATED_COMMANDS: + replacement = _DEPRECATED_COMMANDS[command] + if replacement: + print( # noqa: T201 + f"WARNING: '{command}' is deprecated and will be removed in a " + f"future release. Use '{replacement}' instead.\n", + file=sys.stderr, + flush=True, + ) + else: + print( # noqa: T201 + f"WARNING: '{command}' is deprecated and will be removed in a " + "future release.\n", + file=sys.stderr, + flush=True, + ) if not _USE_SYSTEM_BINS: executable = _find_executable(command) diff --git a/pact-python-cli/tests/test_init.py b/pact-python-cli/tests/test_init.py index 004ef8f35..904e2e62a 100644 --- a/pact-python-cli/tests/test_init.py +++ b/pact-python-cli/tests/test_init.py @@ -198,3 +198,42 @@ def test_exec_directly(executable: str) -> None: assert (os.sep + executable) in cmd assert args == [cmd] assert env + + +def test_deprecated_command_warns_with_replacement( + capsys: pytest.CaptureFixture[str], +) -> None: + """Ruby commands with a Rust equivalent print a hint to stderr.""" + with ( + patch.object(sys, "argv", new=["pact-broker", "--help"]), + patch("os.execve"), + ): + pact_cli._exec() # noqa: SLF001 + captured = capsys.readouterr() + assert "deprecated" in captured.err + assert "pact broker" in captured.err + + +def test_deprecated_command_warns_without_replacement( + capsys: pytest.CaptureFixture[str], +) -> None: + """Ruby commands without a Rust equivalent print a generic warning.""" + with ( + patch.object(sys, "argv", new=["pact-message", "--help"]), + patch("os.execve"), + ): + pact_cli._exec() # noqa: SLF001 + captured = capsys.readouterr() + assert "deprecated" in captured.err + assert "Use " not in captured.err # no replacement hint + + +def test_pact_command_does_not_warn(capsys: pytest.CaptureFixture[str]) -> None: + """The Rust pact binary does not trigger any deprecation warning.""" + with ( + patch.object(sys, "argv", new=["pact", "--help"]), + patch("os.execve"), + ): + pact_cli._exec() # noqa: SLF001 + captured = capsys.readouterr() + assert "deprecated" not in captured.err From 87814fc768a6ca33e3cd46a7603ecad85604c895 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 16 Apr 2026 19:39:56 +1000 Subject: [PATCH 1331/1376] chore: allow windows arm cli builds to proceed Signed-off-by: JP-Ellis --- pact-python-cli/hatch_build.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/pact-python-cli/hatch_build.py b/pact-python-cli/hatch_build.py index 942177972..a9ebf6a19 100644 --- a/pact-python-cli/hatch_build.py +++ b/pact-python-cli/hatch_build.py @@ -124,13 +124,24 @@ def initialize( raise ValueError(msg) try: - build_data["force_include"] = self._install_ruby_cli(cli_version) - self._install_rust_cli() + force_include = self._install_ruby_cli(cli_version) + except UnsupportedPlatformError as err: + # The Ruby standalone is deprecated and not available on all + # platforms (e.g. Windows ARM). Warn and continue rather than + # failing the build. + self.app.display_warning( + f"Pact Ruby standalone CLI not available for {err.platform}; skipping." + ) + force_include = {} + + try: + force_include = {**force_include, **self._install_rust_cli()} except UnsupportedPlatformError as err: msg = f"Pact CLI is not available for {err.platform}." self.app.display_error(msg) raise + build_data["force_include"] = force_include build_data["tag"] = self._infer_tag() def _sys_tag_platform(self) -> str: @@ -162,7 +173,6 @@ def _install_ruby_cli(self, version: str) -> Mapping[str, str]: self._extract(artefact) return { str(PKG_DIR / "bin"): "pact_cli/bin", - # TODO(epoch-transition): Remove lib/ when standalone dropped # noqa: TD003 str(PKG_DIR / "lib"): "pact_cli/lib", } @@ -187,6 +197,9 @@ def _pact_ruby_bin_url(self, version: str) -> str: os_name = "linux" ext = "tar.gz" elif platform.startswith("win"): + if platform.endswith(("arm64", "aarch64")): + # The Ruby standalone has no Windows ARM release. + raise UnsupportedPlatformError(platform) os_name = "windows" ext = "zip" else: @@ -255,7 +268,7 @@ def _pact_rust_bin_url(self, version: str) -> str: ext=ext, ) - def _install_rust_cli(self) -> None: + def _install_rust_cli(self) -> Mapping[str, str]: """ Install the Rust pact binary from pact-cli. @@ -275,6 +288,8 @@ def _install_rust_cli(self) -> None: if sys.platform != "win32": dest.chmod(0o755) + return {str(PKG_DIR / "bin"): "pact_cli/bin"} + def _extract(self, artefact: Path) -> None: """ Extract the Pact binaries. From a1d53ab45cc22f21faf147f3e3325a4128758a3e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 16 Apr 2026 20:20:16 +1000 Subject: [PATCH 1332/1376] chore(ci): have wheel target 310 While the release script requires 3.11, we need 3.10 for the wheels. Once 3.10 is deprecated, we can switch back to using `STABLE_PYTHON_VERSION`. Signed-off-by: JP-Ellis --- .github/workflows/release-cli.yml | 9 +++++---- .github/workflows/release-ffi.yml | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 0d4486891..c04904b45 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -53,8 +53,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' && github.event.action != 'closed' }} env: - # Bump to 3.11 to access tomllib, otherwise, we would use the oldest supported Python version - STABLE_PYTHON_VERSION: '311' + STABLE_PYTHON_VERSION: '310' HATCH_VERBOSE: '1' FORCE_COLOR: '1' CIBW_BUILD_FRONTEND: build @@ -100,7 +99,8 @@ jobs: uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Install Python - run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + # Bump to 3.11 to access tomllib + run: uv python install 311 - name: Update release PR run: uv run python scripts/release.py prepare cli @@ -126,7 +126,8 @@ jobs: uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Install Python - run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + # Bump to 3.11 to access tomllib + run: uv python install 311 - name: Create and push release tag run: uv run python scripts/release.py tag cli diff --git a/.github/workflows/release-ffi.yml b/.github/workflows/release-ffi.yml index fb64dfe06..29c8e5fde 100644 --- a/.github/workflows/release-ffi.yml +++ b/.github/workflows/release-ffi.yml @@ -54,8 +54,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' && github.event.action != 'closed' }} env: - # Bump to 3.11 to access tomllib, otherwise, we would use the oldest supported Python version - STABLE_PYTHON_VERSION: '311' + STABLE_PYTHON_VERSION: '310' HATCH_VERBOSE: '1' FORCE_COLOR: '1' CIBW_BUILD_FRONTEND: build @@ -101,7 +100,8 @@ jobs: uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Install Python - run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + # Bump to 3.11 to access tomllib + run: uv python install 311 - name: Update release PR run: uv run python scripts/release.py prepare ffi @@ -127,7 +127,8 @@ jobs: uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Install Python - run: uv python install ${{ env.STABLE_PYTHON_VERSION }} + # Bump to 3.11 to access tomllib + run: uv python install 311 - name: Create and push release tag run: uv run python scripts/release.py tag ffi From 9dd2a6410e30ce6fc7a22728272ffb2bd1a1f67e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 16 Apr 2026 21:29:03 +1000 Subject: [PATCH 1333/1376] fix: occasional infinite recursion Under some circumstances, there was an infinite recursion whereby the Python shim would try and call the Python shim. The fix is to always strip the Python executable's directory from PATH before searching for the real executable. Signed-off-by: JP-Ellis --- pact-python-cli/src/pact_cli/__init__.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pact-python-cli/src/pact_cli/__init__.py b/pact-python-cli/src/pact_cli/__init__.py index 0d6c85a03..403ba09f5 100644 --- a/pact-python-cli/src/pact_cli/__init__.py +++ b/pact-python-cli/src/pact_cli/__init__.py @@ -131,18 +131,15 @@ def _exec() -> None: flush=True, ) - if not _USE_SYSTEM_BINS: - executable = _find_executable(command) - else: - # To avoid finding the same executable, we have to process the PATH - # variable and remove the current executable's directory. - script_dir = Path(sys.argv[0]).parent.resolve() - os.environ["PATH"] = os.pathsep.join( - p - for p in os.getenv("PATH", "").split(os.pathsep) - if Path(p).resolve() != script_dir - ) - executable = _find_executable(command) + # To avoid finding the same executable, remove the current entry point's + # directory from PATH before searching. + script_dir = Path(sys.argv[0]).parent.resolve() + os.environ["PATH"] = os.pathsep.join( + p + for p in os.getenv("PATH", "").split(os.pathsep) + if Path(p).resolve() != script_dir + ) + executable = _find_executable(command) if not executable: print(f"Command '{command}' not found.", file=sys.stderr) # noqa: T201 From e8ac06041e3a51d05f6a8bea682d487f7c5933f6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Apr 2026 10:11:10 +0000 Subject: [PATCH 1334/1376] chore(release): pact-python-cli v2.6.0.0 --- pact-python-cli/CHANGELOG.md | 21 +++++++++++++++++++++ pact-python-cli/pyproject.toml | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pact-python-cli/CHANGELOG.md b/pact-python-cli/CHANGELOG.md index f21b80009..a5e13b758 100644 --- a/pact-python-cli/CHANGELOG.md +++ b/pact-python-cli/CHANGELOG.md @@ -8,6 +8,27 @@ Note that this _only_ includes changes to the Python re-packaging of the Pact CL +## [pact-python-cli/2.6.0.0] _2026-04-16_ + +### 🐛 Bug Fixes + +- Bundle both ruby and rust CLIs + +### 📚 Documentation + +- Update changelog for pact-python-cli/2.5.7.0 + +### ⚙️ Miscellaneous Tasks + +- Remove versioningit, switch to static version in pyproject.toml +- Minor update to cliff config +- Replace taplo with tombi +- Allow windows arm cli builds to proceed + +### Contributors + +- @JP-Ellis + ## [pact-python-cli/2.5.7.0] _2025-12-10_ ### 🚀 Features diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 5e15d93d4..eff71c201 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -2,7 +2,7 @@ [project] name = "pact-python-cli" -version = "2.5.7.0" +version = "2.6.0.0" description = "Pact CLI bundle for Python" readme = "README.md" license = "MIT" From be5b7d4cf3a8e0d4043aba1864fcf5c10a011fdd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:12:57 +1000 Subject: [PATCH 1335/1376] chore(deps): update dependency mkdocs-section-index to v0.3.12 (#1562) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 59043c67b..73399a5e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ docs = [ "mkdocs-literate-nav==0.6.3", "mkdocs-llmstxt==0.5.0", "mkdocs-material[recommended,git,imaging]==9.7.6", - "mkdocs-section-index==0.3.11", + "mkdocs-section-index==0.3.12", "mkdocstrings[python]==1.0.4", "pathspec==1.0.4", ] From 9ad0ef84a452c98621a359c290654897a588aafb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:13:16 +1000 Subject: [PATCH 1336/1376] chore(deps): update ruff to v0.15.11 (#1563) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a29791495..af9cf9416 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - id: biome-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.10 + rev: v0.15.11 hooks: - id: ruff-check exclude: | diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index eff71c201..885019013 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.15.10", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.11", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] types = ["mypy==1.20.1"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index dc0e64993..1033a9412 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.15.10", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.11", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] types = ["mypy==1.20.1", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 73399a5e5..a05550902 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.15.10", + "ruff==0.15.11", { include-group = "docs" }, { include-group = "example" }, { include-group = "example-v2" }, From 37100d63a32b50631c4ff3a5701113455fd55a44 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 23:24:59 +0000 Subject: [PATCH 1337/1376] chore(deps): update pre-commit hook tombi-toml/tombi-pre-commit to v0.9.18 (#1561) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af9cf9416..5ff9b0312 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -75,7 +75,7 @@ repos: )$ - repo: https://github.com/tombi-toml/tombi-pre-commit - rev: v0.9.17 + rev: v0.9.18 hooks: - id: tombi-format - id: tombi-lint From 5953f96ce5efa857f4508f571a10dfef80651ff4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 23:35:28 +0000 Subject: [PATCH 1338/1376] chore(deps): update astral-sh/setup-uv action to v8.1.0 (#1564) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- .github/workflows/release-cli.yml | 6 +++--- .github/workflows/release-core.yml | 6 +++--- .github/workflows/release-ffi.yml | 6 +++--- .github/workflows/test.yml | 12 ++++++------ 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 346ae3868..c7186e9ba 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index c04904b45..b0d650f2d 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -96,7 +96,7 @@ jobs: tool: git-cliff,typos - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Install Python # Bump to 3.11 to access tomllib @@ -123,7 +123,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Install Python # Bump to 3.11 to access tomllib @@ -155,7 +155,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Install Python run: uv python install ${{ env.STABLE_PYTHON_VERSION }} diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index c47f706e0..c478710f5 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -92,7 +92,7 @@ jobs: tool: git-cliff,typos - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Install Python run: uv python install ${{ env.STABLE_PYTHON_VERSION }} @@ -120,7 +120,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Install Python run: uv python install ${{ env.STABLE_PYTHON_VERSION }} @@ -151,7 +151,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Install Python run: uv python install ${{ env.STABLE_PYTHON_VERSION }} diff --git a/.github/workflows/release-ffi.yml b/.github/workflows/release-ffi.yml index 29c8e5fde..83c7bf5a5 100644 --- a/.github/workflows/release-ffi.yml +++ b/.github/workflows/release-ffi.yml @@ -97,7 +97,7 @@ jobs: tool: git-cliff,typos - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Install Python # Bump to 3.11 to access tomllib @@ -124,7 +124,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Install Python # Bump to 3.11 to access tomllib @@ -156,7 +156,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Install Python run: uv python install ${{ env.STABLE_PYTHON_VERSION }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b17eadda2..dbefdd005 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,7 +73,7 @@ jobs: submodules: true - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true @@ -145,7 +145,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true @@ -190,7 +190,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true @@ -222,7 +222,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true @@ -255,7 +255,7 @@ jobs: fetch-depth: 0 - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true @@ -295,7 +295,7 @@ jobs: ${{ runner.os }}-prek- - name: Set up uv - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true cache-suffix: prek From 0795e4965ddccb554b9a726422111b32fdfaa918 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 17 Apr 2026 09:42:55 +1000 Subject: [PATCH 1339/1376] chore(ci): avoid most of CI on draft PRs CI usage has significantly increased recently due to three release PRs being constantly kept up to date. As these PRs are kept as draft, CI doesn't need to run. Signed-off-by: JP-Ellis --- .github/workflows/docs.yml | 8 ++++++-- .github/workflows/test.yml | 16 +++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c7186e9ba..41114fd5e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,6 +8,11 @@ on: pull_request: branches: - main + types: + - opened + - synchronize + - reopened + - ready_for_review env: STABLE_PYTHON_VERSION: '3.10' @@ -17,7 +22,7 @@ env: jobs: build: name: Build docs - + if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ubuntu-latest steps: @@ -51,7 +56,6 @@ jobs: publish: name: Publish docs - if: github.ref == 'refs/heads/main' needs: build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dbefdd005..5155b4149 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,11 @@ on: pull_request: branches: - main + types: + - opened + - synchronize + - reopened + - ready_for_review concurrency: group: ${{ github.workflow }}-${{ github.ref || github.run_id }} @@ -45,13 +50,13 @@ jobs: if: | contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') - || contains(needs.*.result, 'skipped') test: name: >- Test Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} + if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ${{ matrix.os }} strategy: @@ -125,6 +130,7 @@ jobs: Test Python Example ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} + if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ${{ matrix.os }} strategy: @@ -180,7 +186,7 @@ jobs: format: name: Format - + if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ubuntu-latest steps: @@ -212,7 +218,7 @@ jobs: run: hatch run format --check --output-format github lint: name: Lint - + if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ubuntu-latest steps: @@ -245,7 +251,7 @@ jobs: typecheck: name: Typecheck - + if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ubuntu-latest steps: @@ -274,7 +280,7 @@ jobs: prek: name: Prek (pre-commit) - + if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ubuntu-latest steps: From 23f5d3d6c10a40a9523c743f2c83ade77a3b5776 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 17 Apr 2026 12:00:03 +1000 Subject: [PATCH 1340/1376] chore: prepare for 0.5.3 Signed-off-by: JP-Ellis --- pact-python-ffi/src/pact_ffi/__init__.py | 764 ++++++++++------------- 1 file changed, 347 insertions(+), 417 deletions(-) diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index 04ae931b5..732444034 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -419,7 +419,7 @@ class InteractionHandle: Handle to a HTTP Interaction. [Rust - `InteractionHandle`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/mock_server/handles/struct.InteractionHandle.html) + `InteractionHandle`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/mock_server/handles/struct.InteractionHandle.html) """ def __init__(self, ref: int) -> None: @@ -919,7 +919,7 @@ class PactHandle: Handle to a Pact. [Rust - `PactHandle`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/mock_server/handles/struct.PactHandle.html) + `PactHandle`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/mock_server/handles/struct.PactHandle.html) """ def __init__(self, ref: int) -> None: @@ -1702,7 +1702,7 @@ class VerifierHandle: """ Handle to a Verifier. - [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/verifier/handle/struct.VerifierHandle.html) + [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/verifier/handle/struct.VerifierHandle.html) """ def __init__(self, ref: cffi.FFI.CData) -> None: @@ -1738,7 +1738,7 @@ class ExpressionValueType(Enum): """ Expression Value Type. - [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/models/expressions/enum.ExpressionValueType.html) + [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/models/expressions/enum.ExpressionValueType.html) """ UNKNOWN = lib.ExpressionValueType_Unknown @@ -1765,7 +1765,7 @@ class GeneratorCategory(Enum): """ Generator Category. - [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/models/generators/enum.GeneratorCategory.html) + [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/models/generators/enum.GeneratorCategory.html) """ METHOD = lib.GeneratorCategory_METHOD @@ -1793,7 +1793,7 @@ class InteractionPart(Enum): """ Interaction Part. - [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/mock_server/handles/enum.InteractionPart.html) + [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/mock_server/handles/enum.InteractionPart.html) """ REQUEST = lib.InteractionPart_Request @@ -1839,7 +1839,7 @@ class MatchingRuleCategory(Enum): """ Matching Rule Category. - [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) + [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) """ METHOD = lib.MatchingRuleCategory_METHOD @@ -1868,7 +1868,7 @@ class PactSpecification(Enum): """ Pact Specification. - [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/models/pact_specification/enum.PactSpecification.html) + [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/models/pact_specification/enum.PactSpecification.html) """ UNKNOWN = lib.PactSpecification_Unknown @@ -1921,7 +1921,7 @@ class _StringResult(Enum): """ Internal enum from Pact FFI. - [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/mock_server/enum.StringResult.html) + [Rust `StringResult`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/mock_server/enum.StringResult.html) """ FAILED = lib.StringResult_Failed @@ -2086,7 +2086,7 @@ def version() -> str: """ Return the version of the pact_ffi library. - [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_version) + [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_version) Returns: The version of the pact_ffi library as a string, in the form of `x.y.z`. @@ -2106,7 +2106,7 @@ def init(log_env_var: str) -> None: tracing subscriber. [Rust - `pactffi_init`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_init) + `pactffi_init`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_init) Args: log_env_var: @@ -2127,7 +2127,7 @@ def init_with_log_level(level: str = "INFO") -> None: tracing subscriber. [Rust - `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_init_with_log_level) + `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_init_with_log_level) Args: level: @@ -2147,7 +2147,7 @@ def enable_ansi_support() -> None: On non-Windows platforms, this function is a no-op. [Rust - `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_enable_ansi_support) + `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_enable_ansi_support) # Safety @@ -2165,7 +2165,7 @@ def log_message( Log using the shared core logging facility. [Rust - `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_log_message) + `pactffi_log_message`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_log_message) This is useful for callers to have a single set of logs. @@ -2195,7 +2195,7 @@ def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: Get an iterator over mismatches. [Rust - `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatches_get_iter) + `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatches_get_iter) """ raise NotImplementedError @@ -2204,7 +2204,7 @@ def mismatches_delete(mismatches: Mismatches) -> None: """ Delete mismatches. - [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatches_delete) + [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatches_delete) """ raise NotImplementedError @@ -2213,7 +2213,7 @@ def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: """ Get the next mismatch from a mismatches iterator. - [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatches_iter_next) + [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatches_iter_next) Returns a null pointer if no mismatches remain. """ @@ -2224,7 +2224,7 @@ def mismatches_iter_delete(iter: MismatchesIterator) -> None: """ Delete a mismatches iterator when you're done with it. - [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatches_iter_delete) + [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatches_iter_delete) """ raise NotImplementedError @@ -2233,7 +2233,7 @@ def mismatch_to_json(mismatch: Mismatch) -> str: """ Get a JSON representation of the mismatch. - [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatch_to_json) + [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatch_to_json) """ raise NotImplementedError @@ -2242,7 +2242,7 @@ def mismatch_type(mismatch: Mismatch) -> str: """ Get the type of a mismatch. - [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatch_type) + [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatch_type) """ raise NotImplementedError @@ -2251,7 +2251,7 @@ def mismatch_summary(mismatch: Mismatch) -> str: """ Get a summary of a mismatch. - [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatch_summary) + [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatch_summary) """ raise NotImplementedError @@ -2260,7 +2260,7 @@ def mismatch_description(mismatch: Mismatch) -> str: """ Get a description of a mismatch. - [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatch_description) + [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatch_description) """ raise NotImplementedError @@ -2269,7 +2269,7 @@ def mismatch_ansi_description(mismatch: Mismatch) -> str: """ Get an ANSI-compatible description of a mismatch. - [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mismatch_ansi_description) + [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatch_ansi_description) """ raise NotImplementedError @@ -2279,7 +2279,7 @@ def get_error_message(length: int = 1024) -> str | None: Provide the error message from `LAST_ERROR` to the calling C code. [Rust - `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_get_error_message) + `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_get_error_message) This function should be called after any other function in the pact_matching FFI indicates a failure with its own error message, if the caller wants to @@ -2333,7 +2333,11 @@ def log_to_stdout(level_filter: LevelFilter) -> int: """ Convenience function to direct all logging to stdout. - [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_log_to_stdout) + This function is equivalent to using [`logger_init`] followed by the + [`logger_attach_sink`] with the appropriate sink specifier, and then + [`logger_apply`]. + + [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_log_to_stdout) """ raise NotImplementedError @@ -2342,8 +2346,12 @@ def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: """ Convenience function to direct all logging to stderr. + This function is equivalent to using [`logger_init`] followed by the + [`logger_attach_sink`] with the appropriate sink specifier, and then + [`logger_apply`]. + [Rust - `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_log_to_stderr) + `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_log_to_stderr) Args: level_filter: @@ -2367,8 +2375,12 @@ def log_to_file(file_name: str, level_filter: LevelFilter) -> int: """ Convenience function to direct all logging to a file. + This function is equivalent to using [`logger_init`] followed by the + [`logger_attach_sink`] with the appropriate sink specifier, and then + [`logger_apply`]. + [Rust - `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_log_to_file) + `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_log_to_file) # Safety @@ -2382,7 +2394,11 @@ def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: """ Convenience function to direct all logging to a task local memory buffer. - [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_log_to_buffer) + This function is equivalent to using [`logger_init`] followed by the + [`logger_attach_sink`] with the appropriate sink specifier, and then + [`logger_apply`]. + + [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_log_to_buffer) Raises: RuntimeError: @@ -2400,7 +2416,7 @@ def logger_init() -> None: """ Initialize the FFI logger with no sinks. - [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_logger_init) + [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_logger_init) This initialized logger does nothing until `pactffi_logger_apply` has been called. @@ -2422,7 +2438,7 @@ def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: Attach an additional sink to the thread-local logger. [Rust - `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_logger_attach_sink) + `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_logger_attach_sink) This logger does nothing until `pactffi_logger_apply` has been called. @@ -2469,7 +2485,7 @@ def logger_apply() -> int: Apply the previously configured sinks and levels to the program. [Rust - `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_logger_apply) + `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_logger_apply) If no sinks have been setup, will set the log level to info and the target to standard out. @@ -2485,7 +2501,7 @@ def fetch_log_buffer(log_id: str) -> str: Fetch the in-memory logger buffer contents. [Rust - `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_fetch_log_buffer) + `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_fetch_log_buffer) This will only have any contents if the `buffer` sink has been configured to log to. The contents will be allocated on the heap and will need to be freed @@ -2511,7 +2527,7 @@ def parse_pact_json(json: str) -> Pact: Parses the provided JSON into a Pact model. [Rust - `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_parse_pact_json) + `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_parse_pact_json) The returned Pact model must be freed with the `pactffi_pact_model_delete` function when no longer needed. @@ -2528,7 +2544,7 @@ def pact_model_delete(pact: Pact) -> None: """ Frees the memory used by the Pact model. - [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_model_delete) + [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_model_delete) """ lib.pactffi_pact_model_delete(pact._ptr) @@ -2538,7 +2554,7 @@ def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: Returns an iterator over all the interactions in the Pact. [Rust - `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_model_interaction_iterator) + `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_model_interaction_iterator) The iterator will contain a copy of the interactions, so it will not be affected but mutations to the Pact model and will still function if the Pact @@ -2560,7 +2576,7 @@ def pact_spec_version(pact: Pact) -> PactSpecification: """ Returns the Pact specification enum that the Pact is for. - [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_spec_version) + [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_spec_version) """ raise NotImplementedError @@ -2569,7 +2585,7 @@ def pact_interaction_delete(interaction: PactInteraction) -> None: """ Frees the memory used by the Pact interaction model. - [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_delete) + [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_interaction_delete) """ lib.pactffi_pact_interaction_delete(interaction._ptr) @@ -2578,7 +2594,7 @@ def async_message_new() -> AsynchronousMessage: """ Get a mutable pointer to a newly-created default message on the heap. - [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_new) + [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_new) # Safety @@ -2595,7 +2611,7 @@ def async_message_delete(message: AsynchronousMessage) -> None: """ Destroy the `AsynchronousMessage` being pointed to. - [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_delete) + [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_delete) """ lib.pactffi_async_message_delete(message._ptr) @@ -2605,7 +2621,7 @@ def async_message_get_contents(message: AsynchronousMessage) -> MessageContents Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. [Rust - `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_contents) + `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_contents) If the message contents are missing, this function will return `None`. """ @@ -2624,7 +2640,7 @@ def async_message_generate_contents( contents as would be received by the consumer. [Rust - `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_generate_contents) + `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_generate_contents) If the message contents are missing, this function will return `None`. """ @@ -2638,7 +2654,7 @@ def async_message_get_contents_str(message: AsynchronousMessage) -> str: """ Get the message contents of an `AsynchronousMessage` in string form. - [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_contents_str) + [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_contents_str) # Safety @@ -2665,7 +2681,7 @@ def async_message_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_set_contents_str) + `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_set_contents_str) - `message` - the message to set the contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -2693,7 +2709,7 @@ def async_message_get_contents_length(message: AsynchronousMessage) -> int: Get the length of the contents of a `AsynchronousMessage`. [Rust - `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_contents_length) + `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_contents_length) # Safety @@ -2712,7 +2728,7 @@ def async_message_get_contents_bin(message: AsynchronousMessage) -> str: Get the contents of an `AsynchronousMessage` as bytes. [Rust - `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_contents_bin) + `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_contents_bin) # Safety @@ -2739,7 +2755,7 @@ def async_message_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_set_contents_bin) + `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_set_contents_bin) * `message` - the message to set the contents for * `contents` - pointer to contents to copy from @@ -2766,7 +2782,7 @@ def async_message_get_description(message: AsynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_description) + `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_description) Raises: RuntimeError: @@ -2786,7 +2802,7 @@ def async_message_set_description( """ Write the `description` field on the `AsynchronousMessage`. - [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_set_description) + [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_set_description) # Safety @@ -2811,7 +2827,7 @@ def async_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_provider_state) + `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_provider_state) Raises: RuntimeError: @@ -2830,7 +2846,7 @@ def async_message_get_provider_state_iter( """ Get an iterator over provider states. - [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) + [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) # Safety @@ -2845,7 +2861,7 @@ def consumer_get_name(consumer: Consumer) -> str: r""" Get a copy of this consumer's name. - [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_consumer_get_name) + [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_consumer_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -2891,7 +2907,7 @@ def pact_get_consumer(pact: Pact) -> Consumer: `pactffi_pact_consumer_delete` when no longer required. [Rust - `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_get_consumer) + `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_get_consumer) # Errors @@ -2905,7 +2921,7 @@ def pact_consumer_delete(consumer: Consumer) -> None: """ Frees the memory used by the Pact consumer. - [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_consumer_delete) + [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_consumer_delete) """ raise NotImplementedError @@ -2921,7 +2937,7 @@ def message_contents_delete(contents: MessageContents) -> None: Deleting a message content which is associated with an interaction will result in undefined behaviour. - [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_delete) + [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_delete) """ lib.pactffi_message_contents_delete(contents._ptr) @@ -2930,7 +2946,7 @@ def message_contents_get_contents_str(contents: MessageContents) -> str | None: """ Get the message contents in string form. - [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_get_contents_str) + [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_get_contents_str) If the message has no contents or contain invalid UTF-8 characters, this function will return `None`. @@ -2950,7 +2966,7 @@ def message_contents_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_set_contents_str) + `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_set_contents_str) * `contents` - the message contents to set the contents for * `contents_str` - pointer to contents to copy from. Must be a valid @@ -2977,7 +2993,7 @@ def message_contents_get_contents_length(contents: MessageContents) -> int: """ Get the length of the message contents. - [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_get_contents_length) + [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_get_contents_length) If the message has not contents, this function will return 0. """ @@ -2989,7 +3005,7 @@ def message_contents_get_contents_bin(contents: MessageContents) -> bytes | None Get the contents of a message as a pointer to an array of bytes. [Rust - `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_get_contents_bin) + `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_get_contents_bin) If the message has no contents, this function will return `None`. """ @@ -3012,7 +3028,7 @@ def message_contents_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_set_contents_bin) + `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_set_contents_bin) * `message` - the message contents to set the contents for * `contents_bin` - pointer to contents to copy from @@ -3041,7 +3057,7 @@ def message_contents_get_metadata_iter( Get an iterator over the metadata of a message. [Rust - `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) + `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) # Safety @@ -3070,7 +3086,7 @@ def message_contents_get_matching_rule_iter( Get an iterator over the matching rules for a category of a message. [Rust - `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) + `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -3112,7 +3128,7 @@ def request_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP request. - [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) + [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -3149,7 +3165,7 @@ def response_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP response. - [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) + [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -3187,7 +3203,7 @@ def message_contents_get_generators_iter( Get an iterator over the generators for a category of a message. [Rust - `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_contents_get_generators_iter) + `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_get_generators_iter) # Safety @@ -3213,7 +3229,7 @@ def request_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP request. [Rust - `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_request_contents_get_generators_iter) + `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_request_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -3238,7 +3254,7 @@ def response_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP response. [Rust - `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_response_contents_get_generators_iter) + `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_response_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -3263,7 +3279,7 @@ def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: any generator. [Rust - `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_parse_matcher_definition) + `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_parse_matcher_definition) The following are examples of matching rule definitions: @@ -3299,7 +3315,7 @@ def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: using the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matcher_definition_error) + `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matcher_definition_error) """ raise NotImplementedError @@ -3313,7 +3329,7 @@ def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matcher_definition_value) + `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matcher_definition_value) Note that different expressions values can have types other than a string. Use `pactffi_matcher_definition_value_type` to get the actual type of the @@ -3327,7 +3343,7 @@ def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: """ Frees the memory used by the result of parsing the matching definition expression. - [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matcher_definition_delete) + [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matcher_definition_delete) """ raise NotImplementedError @@ -3340,7 +3356,7 @@ def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Ge NULL pointer, otherwise returns the generator as a pointer. [Rust - `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matcher_definition_generator) + `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matcher_definition_generator) The generator pointer will be a valid pointer as long as `pactffi_matcher_definition_delete` has not been called on the definition. @@ -3359,7 +3375,7 @@ def matcher_definition_value_type( If there was an error parsing the expression, it will return Unknown. [Rust - `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matcher_definition_value_type) + `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matcher_definition_value_type) """ raise NotImplementedError @@ -3368,7 +3384,7 @@ def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: """ Free the iterator when you're done using it. - [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_iter_delete) + [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_iter_delete) """ raise NotImplementedError @@ -3383,7 +3399,7 @@ def matcher_definition_iter( `pactffi_matching_rule_iter_delete` function once done with it. [Rust - `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matcher_definition_iter) + `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matcher_definition_iter) If there was an error parsing the expression, this function will return a NULL pointer. @@ -3399,7 +3415,7 @@ def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: deleted but will be cleaned up when the iterator is deleted. [Rust - `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_iter_next) + `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_iter_next) Will return a NULL pointer when the iterator has advanced past the end of the list. @@ -3421,7 +3437,7 @@ def matching_rule_id(rule_result: MatchingRuleResult) -> int: Return the ID of the matching rule. [Rust - `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_id) + `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_id) The ID corresponds to the following rules: @@ -3467,7 +3483,7 @@ def matching_rule_value(rule_result: MatchingRuleResult) -> str: pointer. [Rust - `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_value) + `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_value) The associated values for the rules are: @@ -3515,7 +3531,7 @@ def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: Will return a NULL pointer if the matching rule result was a reference. [Rust - `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_pointer) + `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_pointer) # Safety @@ -3533,7 +3549,7 @@ def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: structure. I.e., [Rust - `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_reference_name) + `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_reference_name) ```json { @@ -3560,7 +3576,7 @@ def validate_datetime(value: str, format: str) -> None: Validates the date/time value against the date/time format string. [Rust - `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_validate_datetime) + `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_validate_datetime) Raises: ValueError: @@ -3587,7 +3603,7 @@ def generator_to_json(generator: Generator) -> str: Get the JSON form of the generator. [Rust - `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generator_to_json) + `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generator_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -3610,7 +3626,7 @@ def generator_generate_string(generator: Generator, context_json: str) -> str: function). [Rust - `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generator_generate_string) + `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generator_generate_string) If anything goes wrong, it will return a NULL pointer. """ @@ -3633,7 +3649,7 @@ def generator_generate_integer(generator: Generator, context_json: str) -> int: should be the values returned from the Provider State callback function). [Rust - `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generator_generate_integer) + `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generator_generate_integer) If anything goes wrong or the generator is not a type that can generate an integer value, it will return a zero value. @@ -3649,7 +3665,7 @@ def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generators_iter_delete) + `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generators_iter_delete) """ lib.pactffi_generators_iter_delete(iter._ptr) @@ -3659,7 +3675,7 @@ def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePa Get the next path and generator out of the iterator, if possible. [Rust - `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generators_iter_next) + `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generators_iter_next) The returned pointer must be deleted with `pactffi_generator_iter_pair_delete`. @@ -3679,7 +3695,7 @@ def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: Free a pair of key and value returned from `pactffi_generators_iter_next`. [Rust - `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generators_iter_pair_delete) + `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generators_iter_pair_delete) """ lib.pactffi_generators_iter_pair_delete(pair._ptr) @@ -3688,7 +3704,7 @@ def sync_http_new() -> SynchronousHttp: """ Get a mutable pointer to a newly-created default interaction on the heap. - [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_new) + [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_new) # Safety @@ -3706,7 +3722,7 @@ def sync_http_delete(interaction: SynchronousHttp) -> None: Destroy the `SynchronousHttp` interaction being pointed to. [Rust - `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_delete) + `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_delete) """ lib.pactffi_sync_http_delete(interaction._ptr) @@ -3716,7 +3732,7 @@ def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: Get the request of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_request) + `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_request) # Safety @@ -3736,7 +3752,7 @@ def sync_http_get_request_contents(interaction: SynchronousHttp) -> str | None: Get the request contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_request_contents) + `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_request_contents) Note that this function will return `None` if either the body is missing or is `null`. @@ -3756,7 +3772,7 @@ def sync_http_set_request_contents( Sets the request contents of the interaction. [Rust - `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_set_request_contents) + `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_set_request_contents) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -3784,7 +3800,7 @@ def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: Get the length of the request contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) + `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) This function will return 0 if the body is missing. """ @@ -3796,7 +3812,7 @@ def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes | Get the request contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) + `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) Note that this function will return `None` if either the body is missing or is `null`. @@ -3820,7 +3836,7 @@ def sync_http_set_request_contents_bin( Sets the request contents of the interaction as an array of bytes. [Rust - `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) + `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from @@ -3847,7 +3863,7 @@ def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: Get the response of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_response) + `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_response) # Safety @@ -3867,7 +3883,7 @@ def sync_http_get_response_contents(interaction: SynchronousHttp) -> str | None: Get the response contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_response_contents) + `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_response_contents) Note that this function will return `None` if either the body is missing or is `null`. @@ -3887,7 +3903,7 @@ def sync_http_set_response_contents( Sets the response contents of the interaction. [Rust - `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_set_response_contents) + `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_set_response_contents) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -3915,7 +3931,7 @@ def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: Get the length of the response contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) + `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) This function will return 0 if the body is missing. """ @@ -3927,7 +3943,7 @@ def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes | Get the response contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) + `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) Note that this function will return `None` if either the body is missing or is `null`. @@ -3951,7 +3967,7 @@ def sync_http_set_response_contents_bin( Sets the response contents of the `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) + `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from @@ -3978,7 +3994,7 @@ def sync_http_get_description(interaction: SynchronousHttp) -> str: Get a copy of the description. [Rust - `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_description) + `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_description) Raises: RuntimeError: @@ -3996,7 +4012,7 @@ def sync_http_set_description(interaction: SynchronousHttp, description: str) -> Write the `description` field on the `SynchronousHttp`. [Rust - `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_set_description) + `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_set_description) # Safety @@ -4021,7 +4037,7 @@ def sync_http_get_provider_state( Get a copy of the provider state at the given index from this interaction. [Rust - `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_provider_state) + `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_provider_state) # Safety @@ -4047,7 +4063,7 @@ def sync_http_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) + `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) # Safety @@ -4070,7 +4086,7 @@ def pact_interaction_as_synchronous_http( """ Cast this interaction to a `SynchronousHttp` interaction. - [Rust `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) + [Rust `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) Args: interaction: @@ -4100,7 +4116,7 @@ def pact_interaction_as_asynchronous_message( Note that if the interaction is a V3 `Message`, it will be converted to a V4 `AsynchronousMessage` before being returned. - [Rust `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) + [Rust `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) Args: interaction: @@ -4127,7 +4143,7 @@ def pact_interaction_as_synchronous_message( """ Cast this interaction to a `SynchronousMessage` interaction. - [Rust `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) + [Rust `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) Args: interaction: @@ -4153,7 +4169,7 @@ def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> Asynchronous Get the next asynchronous message from the iterator. [Rust - `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_async_message_iter_next) + `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_async_message_iter_next) Raises: StopIteration: @@ -4170,7 +4186,7 @@ def pact_async_message_iter_delete(iter: PactAsyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_async_message_iter_delete) + `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_async_message_iter_delete) """ lib.pactffi_pact_async_message_iter_delete(iter._ptr) @@ -4180,7 +4196,7 @@ def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMes Get the next synchronous request/response message from the V4 pact. [Rust - `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_sync_message_iter_next) + `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_sync_message_iter_next) Raises: StopIteration: @@ -4197,7 +4213,7 @@ def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) + `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) """ lib.pactffi_pact_sync_message_iter_delete(iter._ptr) @@ -4207,7 +4223,7 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: Get the next synchronous HTTP request/response interaction from the V4 pact. [Rust - `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_sync_http_iter_next) + `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_sync_http_iter_next) Raises: StopIteration: @@ -4224,7 +4240,7 @@ def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) + `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) """ lib.pactffi_pact_sync_http_iter_delete(iter._ptr) @@ -4234,7 +4250,7 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction Get the next interaction from the pact. [Rust - `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_iter_next) + `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_interaction_iter_next) Raises: StopIteration: @@ -4251,7 +4267,7 @@ def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_interaction_iter_delete) + `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_interaction_iter_delete) """ lib.pactffi_pact_interaction_iter_delete(iter._ptr) @@ -4261,7 +4277,7 @@ def pact_message_iter_next(iter: PactMessageIterator) -> PactInteraction: Get the next interaction from the pact. [Rust - `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_message_iter_next) + `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_message_iter_next) Raises: StopIteration: @@ -4278,7 +4294,7 @@ def pact_message_iter_delete(iter: PactMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_message_iter_delete) + `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_message_iter_delete) """ lib.pactffi_pact_message_iter_delete(iter._ptr) @@ -4288,7 +4304,7 @@ def matching_rule_to_json(rule: MatchingRule) -> str: Get the JSON form of the matching rule. [Rust - `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rule_to_json) + `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -4305,7 +4321,7 @@ def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rules_iter_delete) + `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rules_iter_delete) """ lib.pactffi_matching_rules_iter_delete(iter._ptr) @@ -4317,7 +4333,7 @@ def matching_rules_iter_next( Get the next path and matching rule out of the iterator, if possible. [Rust - `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rules_iter_next) + `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rules_iter_next) The returned pointer must be deleted with `pactffi_matching_rules_iter_pair_delete`. @@ -4339,7 +4355,7 @@ def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) + `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) """ lib.pactffi_matching_rules_iter_pair_delete(pair._ptr) @@ -4349,7 +4365,7 @@ def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: Get the next value from the iterator. [Rust - `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_iter_next) + `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_iter_next) # Safety @@ -4370,7 +4386,7 @@ def provider_state_iter_delete(iter: ProviderStateIterator) -> None: Delete the iterator. [Rust - `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_iter_delete) + `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_iter_delete) """ lib.pactffi_provider_state_iter_delete(iter._ptr) @@ -4380,7 +4396,7 @@ def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadata Get the next key and value out of the iterator, if possible. [Rust - `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_metadata_iter_next) + `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_metadata_iter_next) The returned pointer must be deleted with `pactffi_message_metadata_pair_delete`. @@ -4406,7 +4422,7 @@ def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: Free the metadata iterator when you're done using it. [Rust - `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_metadata_iter_delete) + `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_metadata_iter_delete) """ lib.pactffi_message_metadata_iter_delete(iter._ptr) @@ -4416,7 +4432,7 @@ def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_message_metadata_pair_delete) + `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_metadata_pair_delete) """ lib.pactffi_message_metadata_pair_delete(pair._ptr) @@ -4426,7 +4442,7 @@ def provider_get_name(provider: Provider) -> str: Get a copy of this provider's name. [Rust - `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_get_name) + `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -4472,7 +4488,7 @@ def pact_get_provider(pact: Pact) -> Provider: `pactffi_pact_provider_delete` when no longer required. [Rust - `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_get_provider) + `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_get_provider) # Errors @@ -4487,7 +4503,7 @@ def pact_provider_delete(provider: Provider) -> None: Frees the memory used by the Pact provider. [Rust - `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_provider_delete) + `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_provider_delete) """ raise NotImplementedError @@ -4497,7 +4513,7 @@ def provider_state_get_name(provider_state: ProviderState) -> str | None: Get the name of the provider state as a string. [Rust - `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_get_name) + `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_get_name) Raises: RuntimeError: @@ -4517,7 +4533,7 @@ def provider_state_get_param_iter( Get an iterator over the params of a provider state. [Rust - `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_get_param_iter) + `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_get_param_iter) # Safety @@ -4545,7 +4561,7 @@ def provider_state_param_iter_next( Get the next key and value out of the iterator, if possible. [Rust - `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_param_iter_next) + `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_param_iter_next) # Safety @@ -4566,7 +4582,7 @@ def provider_state_delete(provider_state: ProviderState) -> None: Free the provider state when you're done using it. [Rust - `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_delete) + `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_delete) """ raise NotImplementedError @@ -4576,7 +4592,7 @@ def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: Free the provider state param iterator when you're done using it. [Rust - `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_param_iter_delete) + `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_param_iter_delete) """ lib.pactffi_provider_state_param_iter_delete(iter._ptr) @@ -4586,7 +4602,7 @@ def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: Free a pair of key and value. [Rust - `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_provider_state_param_pair_delete) + `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_param_pair_delete) """ lib.pactffi_provider_state_param_pair_delete(pair._ptr) @@ -4596,7 +4612,7 @@ def sync_message_new() -> SynchronousMessage: Get a mutable pointer to a newly-created default message on the heap. [Rust - `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_new) + `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_new) # Safety @@ -4614,7 +4630,7 @@ def sync_message_delete(message: SynchronousMessage) -> None: Destroy the `Message` being pointed to. [Rust - `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_delete) + `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_delete) """ lib.pactffi_sync_message_delete(message._ptr) @@ -4624,7 +4640,7 @@ def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: Get the request contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) + `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) # Safety @@ -4651,7 +4667,7 @@ def sync_message_set_request_contents_str( Sets the request contents of the message. [Rust - `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) + `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) - `message` - the message to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -4679,7 +4695,7 @@ def sync_message_get_request_contents_length(message: SynchronousMessage) -> int Get the length of the request contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) + `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) # Safety @@ -4698,7 +4714,7 @@ def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes: Get the request contents of a `SynchronousMessage` as a bytes. [Rust - `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) + `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) # Safety @@ -4725,7 +4741,7 @@ def sync_message_set_request_contents_bin( Sets the request contents of the message as an array of bytes. [Rust - `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) + `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) * `message` - the message to set the request contents for * `contents` - pointer to contents to copy from @@ -4752,7 +4768,7 @@ def sync_message_get_request_contents(message: SynchronousMessage) -> MessageCon Get the request contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_request_contents) + `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_request_contents) # Safety @@ -4779,7 +4795,7 @@ def sync_message_generate_request_contents( contents as would be received by the consumer. [Rust - `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_generate_request_contents) + `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_generate_request_contents) Raises: RuntimeError: @@ -4797,7 +4813,7 @@ def sync_message_get_number_responses(message: SynchronousMessage) -> int: Get the number of response messages in the `SynchronousMessage`. [Rust - `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_number_responses) + `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_number_responses) If the message is null, this function will return 0. """ @@ -4812,7 +4828,7 @@ def sync_message_get_response_contents_str( Get the response contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) + `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) # Safety @@ -4845,7 +4861,7 @@ def sync_message_set_response_contents_str( with default values. [Rust - `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) + `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response. @@ -4877,7 +4893,7 @@ def sync_message_get_response_contents_length( Get the length of the response contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) + `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) # Safety @@ -4899,7 +4915,7 @@ def sync_message_get_response_contents_bin( Get the response contents of a `SynchronousMessage` as bytes. [Rust - `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) + `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) # Safety @@ -4930,7 +4946,7 @@ def sync_message_set_response_contents_bin( responses will be padded with default values. [Rust - `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) + `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response @@ -4961,7 +4977,7 @@ def sync_message_get_response_contents( Get the response contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_response_contents) + `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_response_contents) # Safety @@ -4990,7 +5006,7 @@ def sync_message_generate_response_contents( received by the consumer. [Rust - `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_generate_response_contents) + `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_generate_response_contents) Raises: RuntimeError: @@ -5008,7 +5024,7 @@ def sync_message_get_description(message: SynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_description) + `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_description) Raises: RuntimeError: @@ -5026,7 +5042,7 @@ def sync_message_set_description(message: SynchronousMessage, description: str) Write the `description` field on the `SynchronousMessage`. [Rust - `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_set_description) + `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_set_description) # Safety @@ -5051,7 +5067,7 @@ def sync_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_provider_state) + `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_provider_state) # Safety @@ -5077,7 +5093,7 @@ def sync_message_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) + `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) # Safety @@ -5099,61 +5115,20 @@ def string_delete(string: OwnedString) -> None: Delete a string previously returned by this FFI. [Rust - `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_string_delete) + `pactffi_string_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_string_delete) """ lib.pactffi_string_delete(string._ptr) -def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: - """ - [DEPRECATED] External interface to create a HTTP mock server. - - A pointer to the pact JSON as a NULL-terminated C string is passed in, as - well as the port for the mock server to run on. A value of 0 for the port - will result in a port being allocated by the operating system. The port of - the mock server is returned. - - [Rust - `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_create_mock_server) - - * `pact_str` - Pact JSON - * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:80) - * `tls` - boolean flag to indicate of the mock server should use TLS (using - a self-signed certificate) - - This function is deprecated and replaced with - `pactffi_create_mock_server_for_transport`. - - # Errors - - Errors are returned as negative values. - - | Error | Description | - |-------|-------------| - | -1 | A null pointer was received | - | -2 | The pact JSON could not be parsed | - | -3 | The mock server could not be started | - | -4 | The method panicked | - | -5 | The address is not valid | - | -6 | Could not create the TLS configuration with the self-signed certificate | - """ - warnings.warn( - "This function is deprecated, use create_mock_server_for_transport instead", - DeprecationWarning, - stacklevel=2, - ) - raise NotImplementedError - - def get_tls_ca_certificate() -> OwnedString: """ Fetch the CA Certificate used to generate the self-signed certificate. [Rust - `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_get_tls_ca_certificate) + `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_get_tls_ca_certificate) **NOTE:** The string for the result is allocated on the heap, and will have - to be freed by the caller using pactffi_string_delete. + to be freed by the caller using [`string_delete`][pact_ffi.string_delete]. # Errors @@ -5162,47 +5137,6 @@ def get_tls_ca_certificate() -> OwnedString: return OwnedString(lib.pactffi_get_tls_ca_certificate()) -def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) -> int: - """ - [DEPRECATED] External interface to create a HTTP mock server. - - A Pact handle is passed in, as well as the port for the mock server to run - on. A value of 0 for the port will result in a port being allocated by the - operating system. The port of the mock server is returned. - - [Rust - `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_create_mock_server_for_pact) - - * `pact` - Handle to a Pact model created with created with - `pactffi_new_pact`. - * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:0). - Must be a valid UTF-8 NULL-terminated string. - * `tls` - boolean flag to indicate of the mock server should use TLS (using - a self-signed certificate) - - This function is deprecated and replaced with - `pactffi_create_mock_server_for_transport`. - - # Errors - - Errors are returned as negative values. - - | Error | Description | - |-------|-------------| - | -1 | An invalid handle was received. Handles should be created with `pactffi_new_pact` | - | -3 | The mock server could not be started | - | -4 | The method panicked | - | -5 | The address is not valid | - | -6 | Could not create the TLS configuration with the self-signed certificate | - """ # noqa: E501 - warnings.warn( - "This function is deprecated, use create_mock_server_for_transport instead", - DeprecationWarning, - stacklevel=2, - ) - raise NotImplementedError - - def create_mock_server_for_transport( pact: PactHandle, addr: str, @@ -5214,7 +5148,7 @@ def create_mock_server_for_transport( Create a mock server for the provided Pact handle and transport. [Rust - `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_create_mock_server_for_transport) + `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_create_mock_server_for_transport) Args: pact: @@ -5276,7 +5210,7 @@ def mock_server_matched(mock_server_handle: PactServerHandle) -> bool: if any request has not been successfully matched, or the method panics. [Rust - `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mock_server_matched) + `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mock_server_matched) """ return lib.pactffi_mock_server_matched(mock_server_handle._ref) @@ -5288,7 +5222,7 @@ def mock_server_mismatches( External interface to get all the mismatches from a mock server. [Rust - `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mock_server_mismatches) + `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mock_server_mismatches) # Errors @@ -5315,7 +5249,7 @@ def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: and cleanup any memory allocated for it. [Rust - `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_cleanup_mock_server) + `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_cleanup_mock_server) Args: mock_server_handle: @@ -5344,7 +5278,7 @@ def write_pact_file( directory to write the file to is passed as the second parameter. [Rust - `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_write_pact_file) + `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_write_pact_file) Args: mock_server_handle: @@ -5396,7 +5330,7 @@ def mock_server_logs(mock_server_handle: PactServerHandle) -> str: started. [Rust - `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_mock_server_logs) + `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mock_server_logs) Raises: RuntimeError: @@ -5420,7 +5354,7 @@ def generate_datetime_string(format: str) -> StringResult: string needs to be freed with the `pactffi_string_delete` function [Rust - `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generate_datetime_string) + `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generate_datetime_string) # Safety @@ -5437,7 +5371,7 @@ def check_regex(regex: str, example: str) -> bool: Checks that the example string matches the given regex. [Rust - `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_check_regex) + `pactffi_check_regex`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_check_regex) # Safety @@ -5456,7 +5390,7 @@ def generate_regex_value(regex: str) -> StringResult: `pactffi_string_delete` function. [Rust - `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_generate_regex_value) + `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generate_regex_value) # Safety @@ -5471,7 +5405,7 @@ def free_string(s: str) -> None: [DEPRECATED] Frees the memory allocated to a string by another function. [Rust - `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_free_string) + `pactffi_free_string`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_free_string) This function is deprecated. Use `pactffi_string_delete` instead. @@ -5493,7 +5427,7 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle: Creates a new Pact model and returns a handle to it. [Rust - `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_new_pact) + `pactffi_new_pact`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_new_pact) Args: consumer_name: @@ -5546,7 +5480,7 @@ def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: will result in that interaction being replaced with the new one. [Rust - `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_new_interaction) + `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_new_interaction) Args: pact: @@ -5574,7 +5508,7 @@ def new_message_interaction(pact: PactHandle, description: str) -> InteractionHa will result in that interaction being replaced with the new one. [Rust - `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_new_message_interaction) + `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_new_message_interaction) Args: pact: @@ -5605,7 +5539,7 @@ def new_sync_message_interaction( will result in that interaction being replaced with the new one. [Rust - `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_new_sync_message_interaction) + `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_new_sync_message_interaction) Args: pact: @@ -5630,7 +5564,7 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> None: Sets the description for the Interaction. [Rust - `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_upon_receiving) + `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_upon_receiving) This function @@ -5671,7 +5605,7 @@ def given(interaction: InteractionHandle, description: str) -> None: Adds a provider state to the Interaction. [Rust - `pactffi_given`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_given) + `pactffi_given`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_given) Args: interaction: @@ -5698,7 +5632,7 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> Non used with V4 interactions. [Rust - `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_interaction_test_name) + `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_interaction_test_name) Args: interaction: @@ -5745,7 +5679,7 @@ def given_with_param( be parsed as JSON. [Rust - `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_given_with_param) + `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_given_with_param) Args: interaction: @@ -5787,7 +5721,7 @@ def given_with_params( with a `value` key. [Rust - `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_given_with_params) + `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_given_with_params) Args: interaction: @@ -5826,7 +5760,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None Configures the request for the Interaction. [Rust - `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_request) + `pactffi_with_request`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_request) Args: interaction: @@ -5840,7 +5774,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md) + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md) which allows regex patterns. For examples: ```json @@ -5875,7 +5809,7 @@ def with_query_parameter_v2( Configures a query parameter for the Interaction. [Rust - `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_query_parameter_v2) + `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_query_parameter_v2) To setup a query parameter with multiple values, you can either call this function multiple times with a different index value: @@ -5912,7 +5846,7 @@ def with_query_parameter_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md) If you want the matching rules to apply to all values (and not just the one with the given index), make sure to set the value to be an array. @@ -5964,7 +5898,7 @@ def with_query_parameter_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -5986,7 +5920,7 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None: Sets the specification version for a given Pact model. [Rust - `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_specification) + `pactffi_with_specification`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_specification) Args: pact: @@ -6010,7 +5944,7 @@ def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: Fetches the Pact specification version for the given Pact model. [Rust - `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_handle_get_pact_spec_version) + `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_handle_get_pact_spec_version) Args: handle: @@ -6036,7 +5970,7 @@ def with_pact_metadata( the mock server for it has already started) or the namespace is readonly. [Rust - `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_pact_metadata) + `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_pact_metadata) Args: pact: @@ -6102,7 +6036,7 @@ def with_metadata( ``` See - [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md) + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md) # Note @@ -6151,7 +6085,7 @@ def with_header_v2( r""" Configures a header for the Interaction. - [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_header_v2) + [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_header_v2) To setup a header with multiple values, you can either call this function multiple times with a different index value: @@ -6189,7 +6123,7 @@ def with_header_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -6211,7 +6145,7 @@ def with_header_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -6243,7 +6177,7 @@ def set_header( and generators can not be configured with it. [Rust - `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_set_header) + `pactffi_set_header`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_set_header) If matching rules are required to be set, use `pactffi_with_header_v2`. @@ -6281,7 +6215,7 @@ def response_status(interaction: InteractionHandle, status: int) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_response_status) + `pactffi_response_status`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_response_status) Args: interaction: @@ -6305,7 +6239,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_response_status_v2) + `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_response_status_v2) To include matching rules for the status (only statusCode or integer really makes sense to use), include the matching rule JSON format with the value as @@ -6324,7 +6258,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -6335,7 +6269,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -6359,7 +6293,7 @@ def with_body( Adds the body for the interaction. [Rust - `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_body) + `pactffi_with_body`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_body) Returns false if the interaction or Pact can't be modified (i.e. the mock server for it has already started) @@ -6394,7 +6328,7 @@ def with_body( body: The body contents. For JSON payloads, matching rules can be embedded in the body. See - [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.28/rust/pact_ffi/IntegrationJson.md). + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -6421,7 +6355,7 @@ def with_binary_body( Adds the body for the interaction. [Rust - `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_binary_body) + `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_binary_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -6481,7 +6415,7 @@ def with_binary_file( already started) [Rust - `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_binary_file) + `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_binary_file) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -6528,7 +6462,7 @@ def with_matching_rules( Add matching rules to the interaction. [Rust - `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_matching_rules) + `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_matching_rules) This function can be called multiple times, in which case the matching rules will be merged. @@ -6566,7 +6500,7 @@ def with_generators( Add generators to the interaction. [Rust - `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_generators) + `pactffi_with_generators`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_generators) This function can be called multiple times, in which case the generators will be combined (provide they don't clash). @@ -6614,7 +6548,7 @@ def with_multipart_file_v2( # noqa: PLR0913 already started) or an error occurs. [Rust - `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_multipart_file_v2) + `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_multipart_file_v2) This function can be called multiple times. In that case, each subsequent call will be appended to the existing multipart body as a new part. @@ -6668,7 +6602,7 @@ def with_multipart_file( already started) or an error occurs. [Rust - `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_with_multipart_file) + `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_multipart_file) * `interaction` - Interaction handle to set the body for. * `part` - Request or response part. @@ -6705,7 +6639,7 @@ def set_key(interaction: InteractionHandle, key: str | None) -> None: Sets the key attribute for the interaction. [Rust - `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_set_key) + `pactffi_set_key`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_set_key) Args: interaction: @@ -6733,7 +6667,7 @@ def set_pending(interaction: InteractionHandle, *, pending: bool) -> None: Mark the interaction as pending. [Rust - `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_set_pending) + `pactffi_set_pending`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_set_pending) Args: interaction: @@ -6757,7 +6691,7 @@ def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> Add a comment to the interaction. [Rust - `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_set_comment) + `pactffi_set_comment`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_set_comment) Args: interaction: @@ -6791,7 +6725,7 @@ def add_text_comment(interaction: InteractionHandle, comment: str) -> None: Add a text comment to the interaction. [Rust - `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_add_text_comment) + `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_add_text_comment) Args: interaction: @@ -6821,7 +6755,7 @@ def pact_handle_get_async_message_iter(pact: PactHandle) -> PactAsyncMessageIter `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6847,7 +6781,7 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6873,7 +6807,7 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: `pactffi_pact_sync_http_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) + `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) # Safety @@ -6897,7 +6831,7 @@ def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: `pactffi_pact_message_iter_delete`. [Rust - `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_handle_get_message_iter) + `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_handle_get_message_iter) # Safety @@ -6925,7 +6859,7 @@ def pact_handle_write_file( External interface to write out the pact file. [Rust - `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_pact_handle_write_file) + `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_handle_write_file) This function should be called if all the consumer tests have passed. @@ -6969,7 +6903,7 @@ def free_pact_handle(pact: PactHandle) -> None: Delete a Pact handle and free the resources used by it. [Rust - `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_free_pact_handle) + `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_free_pact_handle) Raises: RuntimeError: @@ -6985,32 +6919,6 @@ def free_pact_handle(pact: PactHandle) -> None: raise RuntimeError(msg) -def verify(args: str) -> int: - """ - External interface to verifier a provider. - - [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verify) - - * `args` - the same as the CLI interface, except newline delimited - - # Errors - - Errors are returned as non-zero numeric values. - - | Error | Description | - |-------|-------------| - | 1 | The verification process failed, see output for errors | - | 2 | A null pointer was received | - | 3 | The method panicked | - | 4 | Invalid arguments were provided to the verification process | - - # Safety - - Exported functions are inherently unsafe. Deal. - """ - raise NotImplementedError - - def verifier_new_for_application() -> VerifierHandle: """ Get a Handle to a newly created verifier. @@ -7021,7 +6929,7 @@ def verifier_new_for_application() -> VerifierHandle: to set the required values and enable it. [Rust - `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_new_for_application) + `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_new_for_application) """ result: cffi.FFI.CData = lib.pactffi_verifier_new_for_application( b"pact-python", @@ -7034,7 +6942,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None: """ Shutdown the verifier and release all resources. - [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_shutdown) + [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_shutdown) """ lib.pactffi_verifier_shutdown(handle._ref) @@ -7051,7 +6959,7 @@ def verifier_set_provider_info( # noqa: PLR0913 Set the provider details for the Pact verifier. [Rust - `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_provider_info) + `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_provider_info) Args: handle: @@ -7097,7 +7005,7 @@ def verifier_add_provider_transport( Adds a new transport for the given provider. [Rust - `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_add_provider_transport) + `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_add_provider_transport) Args: handle: @@ -7138,7 +7046,7 @@ def verifier_set_filter_info( Set the filters for the Pact verifier. [Rust - `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_filter_info) + `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_filter_info) Set filters to narrow down the interactions to verify. @@ -7174,7 +7082,7 @@ def verifier_set_provider_state( Set the provider state URL for the Pact verifier. [Rust - `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_provider_state) + `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_provider_state) Args: handle: @@ -7209,7 +7117,7 @@ def verifier_set_verification_options( Set the options used by the verifier when calling the provider. [Rust - `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_verification_options) + `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_verification_options) Args: handle: @@ -7244,7 +7152,7 @@ def verifier_set_coloured_output( Enables or disables coloured output using ANSI escape codes. [Rust - `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_coloured_output) + `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_coloured_output) By default, coloured output is enabled. @@ -7273,7 +7181,7 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> Enables or disables if no pacts are found to verify results in an error. [Rust - `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) + `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) Args: handle: @@ -7306,7 +7214,7 @@ def verifier_set_publish_options( Set the options used when publishing verification results to the Broker. [Rust - `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_publish_options) + `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_publish_options) Args: handle: @@ -7349,7 +7257,7 @@ def verifier_set_consumer_filters( Set the consumer filters for the Pact verifier. [Rust - `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_set_consumer_filters) + `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_consumer_filters) """ lib.pactffi_verifier_set_consumer_filters( handle._ref, @@ -7367,7 +7275,7 @@ def verifier_add_custom_header( Adds a custom header to be added to the requests made to the provider. [Rust - `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_add_custom_header) + `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_add_custom_header) """ lib.pactffi_verifier_add_custom_header( handle._ref, @@ -7376,12 +7284,37 @@ def verifier_add_custom_header( ) +def verifier_set_follow_redirects( + handle: VerifierHandle, + *, + follow: bool, +) -> None: + """ + Sets whether redirects should be automatically followed. + + [Rust + `pactffi_verifier_set_follow_redirects`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_follow_redirects) + + Args: + handle: + The verifier handle to update. + + follow: + If `True`, redirects will be automatically followed when making + requests to the provider. + """ + lib.pactffi_verifier_set_follow_redirects( + handle._ref, + 1 if follow else 0, + ) + + def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: """ Adds a Pact file as a source to verify. [Rust - `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_add_file_source) + `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_add_file_source) """ lib.pactffi_verifier_add_file_source(handle._ref, file.encode("utf-8")) @@ -7393,7 +7326,7 @@ def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> Non All pacts from the directory that match the provider name will be verified. [Rust - `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_add_directory_source) + `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_add_directory_source) # Safety @@ -7415,7 +7348,7 @@ def verifier_url_source( Adds a URL as a source to verify. [Rust - `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_url_source) + `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_url_source) Args: handle: @@ -7455,7 +7388,7 @@ def verifier_broker_source( Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_broker_source) + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_broker_source) This will fetch all the pact files from the broker that match the provider name. @@ -7503,7 +7436,7 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) + `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) This will fetch all the pact files from the broker that match the provider name and the consumer version selectors (See [Consumer Version @@ -7582,7 +7515,8 @@ def verifier_execute(handle: VerifierHandle) -> None: """ Runs the verification. - (https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_execute) + [Rust + `pactffi_verifier_execute`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_execute) Raises: RuntimeError: @@ -7594,71 +7528,12 @@ def verifier_execute(handle: VerifierHandle) -> None: raise RuntimeError(msg) -def verifier_cli_args() -> str: - """ - External interface to retrieve the CLI options and arguments. - - This available when calling the CLI interface, returning them as a JSON - string. - - [Rust - `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_cli_args) - - The purpose is to then be able to use in other languages which wrap the FFI - library, to implement the same CLI functionality automatically without - manual maintenance of arguments, help descriptions etc. - - # Example structure - - ```json - { - "options": [ - { - "long": "scheme", - "help": "Provider URI scheme (defaults to http)", - "possible_values": [ - "http", - "https" - ], - "default_value": "http" - "multiple": false, - }, - { - "long": "file", - "short": "f", - "help": "Pact file to verify (can be repeated)", - "multiple": true - }, - { - "long": "user", - "help": "Username to use when fetching pacts from URLS", - "multiple": false, - "env": "PACT_BROKER_USERNAME" - } - ], - "flags": [ - { - "long": "disable-ssl-verification", - "help": "Disables validation of SSL certificates", - "multiple": false - } - ] - } - ``` - - # Safety - - Exported functions are inherently unsafe. - """ - raise NotImplementedError - - def verifier_logs(handle: VerifierHandle) -> OwnedString: """ Extracts the logs for the verification run. [Rust - `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_logs) + `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_logs) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` @@ -7680,7 +7555,7 @@ def verifier_logs_for_provider(provider_name: str) -> OwnedString: Extracts the logs for the verification run for the provider name. [Rust - `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_logs_for_provider) + `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_logs_for_provider) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` @@ -7702,7 +7577,7 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: Extracts the standard output for the verification run. [Rust - `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_output) + `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_output) Args: handle: @@ -7729,7 +7604,7 @@ def verifier_json(handle: VerifierHandle) -> OwnedString: Extracts the verification result as a JSON document. [Rust - `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_verifier_json) + `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_json) Raises: RuntimeError: @@ -7742,6 +7617,61 @@ def verifier_json(handle: VerifierHandle) -> OwnedString: return OwnedString(ptr) +def using_plugin_with_delay( + pact: PactHandle, + plugin_name: str, + plugin_version: str | None, + completion_delay: int, +) -> None: + """ + Add a plugin to be used by the test. + + The plugin needs to be installed correctly for this function to work. + + Note that plugins run as separate processes, so will need to be cleaned up + afterwards by calling [`cleanup_plugins`][pact_ffi.cleanup_plugins] + otherwise you will have plugin processes left running. + + [Rust `pactffi_using_plugin_with_delay`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_using_plugin_with_delay) + + Args: + pact: + Handle to a Pact model. + + plugin_name: + Name of the plugin to use. + + plugin_version: + Version of the plugin to use. If `None`, the latest version will be + used. + + completion_delay: + An arbitrary delay specified in milliseconds to add before the + function returns to allow asynchronous tasks to complete. + + Raises: + RuntimeError: + If the plugin could not be loaded. + """ + ret = lib.pactffi_using_plugin_with_delay( + pact._ref, + plugin_name.encode("utf-8"), + plugin_version.encode("utf-8") if plugin_version else ffi.NULL, + completion_delay, + ) + if ret == 0: + return + if ret == 1: + msg = f"A general panic was caught: {get_error_message()}" + elif ret == 2: # noqa: PLR2004 + msg = f"Failed to load the plugin {plugin_name}." + elif ret == 3: # noqa: PLR2004 + msg = f"The Pact handle {pact} is invalid." + else: + msg = f"There was an unknown error loading the plugin {plugin_name}." + raise RuntimeError(msg) + + def using_plugin( pact: PactHandle, plugin_name: str, @@ -7757,7 +7687,7 @@ def using_plugin( otherwise you will have plugin processes left running. [Rust - `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_using_plugin) + `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_using_plugin) Args: pact: @@ -7800,7 +7730,7 @@ def cleanup_plugins(pact: PactHandle) -> None: zero). [Rust - `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_cleanup_plugins) + `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_cleanup_plugins) """ lib.pactffi_cleanup_plugins(pact._ref) @@ -7819,7 +7749,7 @@ def interaction_contents( format of the JSON contents. [Rust - `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_interaction_contents) + `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_interaction_contents) Args: interaction: @@ -7849,15 +7779,15 @@ def interaction_contents( return if ret == 1: msg = f"A general panic was caught: {get_error_message()}" - if ret == 2: # noqa: PLR2004 + elif ret == 2: # noqa: PLR2004 msg = "The mock server has already been started." - if ret == 3: # noqa: PLR2004 + elif ret == 3: # noqa: PLR2004 msg = f"The interaction handle {interaction} is invalid." - if ret == 4: # noqa: PLR2004 + elif ret == 4: # noqa: PLR2004 msg = f"The content type {content_type} is not valid." - if ret == 5: # noqa: PLR2004 + elif ret == 5: # noqa: PLR2004 msg = "The content is not valid JSON." - if ret == 6: # noqa: PLR2004 + elif ret == 6: # noqa: PLR2004 msg = f"The plugin returned an error: {get_error_message()}" else: msg = f"There was an unknown error configuring the interaction: {ret}" @@ -7879,7 +7809,7 @@ def matches_string_value( function once it is no longer required. [Rust - `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_string_value) + `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_string_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string @@ -7910,7 +7840,7 @@ def matches_u64_value( function once it is no longer required. [Rust - `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_u64_value) + `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_u64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7940,7 +7870,7 @@ def matches_i64_value( function once it is no longer required. [Rust - `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_i64_value) + `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_i64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7970,7 +7900,7 @@ def matches_f64_value( function once it is no longer required. [Rust - `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_f64_value) + `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_f64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -8000,7 +7930,7 @@ def matches_bool_value( function once it is no longer required. [Rust - `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_bool_value) + `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_bool_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get, 0 == false and 1 == true @@ -8032,7 +7962,7 @@ def matches_binary_value( # noqa: PLR0913 function once it is no longer required. [Rust - `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_binary_value) + `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_binary_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -8067,7 +7997,7 @@ def matches_json_value( function once it is no longer required. [Rust - `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.28/pact_ffi/?search=pactffi_matches_json_value) + `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_json_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string From 6f586a41f155fe42f405921a32de6156148bc169 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 17 Apr 2026 12:00:32 +1000 Subject: [PATCH 1341/1376] feat: can toggle follow redirects By default, the Pact verifier follows redirects and validates the final response. This can behaviour can now be disabled. Signed-off-by: JP-Ellis --- pyproject.toml | 2 +- src/pact/verifier.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a05550902..e35858e0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ requires-python = ">=3.10" # - A minor version address vulnerability which directly impacts Pact Python dependencies = [ # Pact dependencies - "pact-python-ffi~=0.4.0", + "pact-python-ffi~=0.5.0", "typing-extensions~=4.0 ; python_version < '3.13'", # External dependencies "yarl~=1.0", diff --git a/src/pact/verifier.py b/src/pact/verifier.py index 529202444..3a53a476a 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -943,6 +943,20 @@ def add_custom_headers( self.add_custom_header(name, value) return self + def follow_redirects(self, follow: bool) -> Self: # noqa: FBT001 + """ + Set whether redirects should be followed. + + By default, the Pact verifier does follow redirects, testing the final + non-redirect response (mimicking the default behaviour of most HTTP + clients). + + In some cases, it may be desirable to test the redirect response itself, + in which case this method can be used to disable following redirects. + """ + pact_ffi.verifier_set_follow_redirects(self._handle, follow=follow) + return self + @overload def add_source( self, From 2331e6b152b03ed98ea6f78bd40e9cca11b23add Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 17 Apr 2026 12:01:16 +1000 Subject: [PATCH 1342/1376] feat: allow plugin loading delay The loading of plugins can take time, and as it is dispatched asynchronously, there are some rare circumstances where Pact will proceed with waiting for the plugin to be fully loaded. In such cases, an artificial delay is added to accommodate for this. Signed-off-by: JP-Ellis --- src/pact/pact.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/pact/pact.py b/src/pact/pact.py index 7e5afcdd6..3c584f1d6 100644 --- a/src/pact/pact.py +++ b/src/pact/pact.py @@ -222,7 +222,12 @@ def with_specification( pact_ffi.with_specification(self._handle, version) return self - def using_plugin(self, name: str, version: str | None = None) -> Self: + def using_plugin( + self, + name: str, + version: str | None = None, + delay: int | None = None, + ) -> Self: """ Add a plugin to be used by the test. @@ -234,8 +239,15 @@ def using_plugin(self, name: str, version: str | None = None) -> Self: version: Version of the plugin. This is optional and can be `None`. + + delay: + An arbitrary delay in milliseconds to add before the function + returns to allow asynchronous tasks to complete. """ - pact_ffi.using_plugin(self._handle, name, version) + if delay is not None: + pact_ffi.using_plugin_with_delay(self._handle, name, version, delay) + else: + pact_ffi.using_plugin(self._handle, name, version) return self def with_metadata( From b17ae35a378de7e67d3896cec52e28184cf4f558 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 17 Apr 2026 12:09:38 +1000 Subject: [PATCH 1343/1376] chore: fix hatch env workspaces Signed-off-by: JP-Ellis --- pyproject.toml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e35858e0d..a9387eb7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -179,13 +179,6 @@ build-backend = "hatchling.build" extra-dependencies = ["hatchling", "packaging"] dependency-groups = ["dev"] - [tool.hatch.envs.default.workspace] - parallel = true - members = [ - { path = "pact-python-cli", group = "dev" }, - { path = "pact-python-ffi", group = "dev" }, - ] - [tool.hatch.envs.default.scripts] all = ["example", "format", "lint", "test", "typecheck"] docs = "mkdocs serve {args}" @@ -336,3 +329,11 @@ build-backend = "hatchling.build" [tool.typos.files] extend-exclude = ["*.svg"] + + [tool.uv] + [tool.uv.sources] + pact-python-cli = { workspace = true } + pact-python-ffi = { workspace = true } + + [tool.uv.workspace] + members = ["pact-python-cli", "pact-python-ffi"] From 940bef5ca9a06f9c8511cc6a2bc6e831d1a75027 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 17 Apr 2026 14:36:36 +1000 Subject: [PATCH 1344/1376] chore: remove connect test Connect doesn't really make sense to be tested within the context of a Pact test. It was only incidentally working before. Signed-off-by: JP-Ellis --- tests/interaction/test_http_interaction.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/interaction/test_http_interaction.py b/tests/interaction/test_http_interaction.py index 6a29a67cc..589e447c3 100644 --- a/tests/interaction/test_http_interaction.py +++ b/tests/interaction/test_http_interaction.py @@ -28,7 +28,6 @@ "HEAD", "OPTIONS", "TRACE", - "CONNECT", ] From bef92840cbb003cd88064682b504c88809c38c05 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 17 Apr 2026 15:02:42 +1000 Subject: [PATCH 1345/1376] chore: exclude virtual DLLs Signed-off-by: JP-Ellis --- pact-python-ffi/pyproject.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 1033a9412..24a20bbb2 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -76,7 +76,13 @@ build-backend = "hatchling.build" before-build = ["pip install abi3audit delvewheel"] environment.PACT_LIB_DIR = "C:/tmp/pact_ffi" repair-wheel-command = [ - "delvewheel repair -vv --add-path \"%PACT_LIB_DIR%\" -w {dest_dir} {wheel}", + # ext-ms-* DLLs are no-export forwarding stubs present in System32 on all + # modern Windows. delvewheel 1.12.0 removed them from its no-bundle list + # (they have no exports so the author assumed nothing links to them), but + # they do appear in DLL import tables and cannot be bundled. Exclude the + # whole ext-ms-* family until upstream fixes ignore_regexes. + # See: https://github.com/adang1345/delvewheel/issues/ + "delvewheel repair -vv --add-path \"%PACT_LIB_DIR%\" --exclude \"ext-ms-*\" -w {dest_dir} {wheel}", "abi3audit --strict --report {wheel}", ] From dfc1e6e869c3d37c5bc9f764d7b42b5317b8a8a3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 16 Apr 2026 23:50:34 +0000 Subject: [PATCH 1346/1376] chore(release): pact-python-ffi v0.5.3.0 --- pact-python-ffi/CHANGELOG.md | 45 ++++++++++++++++++++++++++++++++++ pact-python-ffi/pyproject.toml | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/pact-python-ffi/CHANGELOG.md b/pact-python-ffi/CHANGELOG.md index af61c39e8..b5487a27a 100644 --- a/pact-python-ffi/CHANGELOG.md +++ b/pact-python-ffi/CHANGELOG.md @@ -8,6 +8,51 @@ Note that this _only_ includes changes to the Python FFI interface. For changes +## [pact-python-ffi/0.5.3.0] _2026-04-16_ + +### 🚀 Features + +- Removed: + - `create_mock_server` (use `create_mock_server_for_transport` instead) + - `create_mock_server_for_pact` (use `create_mock_server_for_pact_and_transport` instead) + - `verify` + - `verifier_cli_args` +- Added: + - `verifier_set_follow_redirects` + - `using_plugin_with_delay` +- Allow iteration over all interactions +- Implement the Pact class +- Add handle to pointer conversion +- Add casting interaction to subtypes +- Add iterator over all interactions + +### 🐛 Bug Fixes + +- Incorrect sync http deletion + +### 📚 Documentation + +- Update changelogs + +### ⚙️ Miscellaneous Tasks + +- Update non-compliant docstrings and types +- Upgrade pymdownx extensions +- Fix json schema url +- Remove ruff sub-configs +- Switch to versioningit +- Ensure pact interactions get deleted +- Add ruff ignores for tests +- Refactor ffi tests +- Remove versioningit, switch to static version in pyproject.toml +- Minor update to cliff config +- Replace taplo with tombi + +### Contributors + +- @JP-Ellis +- @Nikhil172913832 + ## [pact-python-ffi/0.4.28.2] _2025-10-06_ ### 📚 Documentation diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 24a20bbb2..8b631a556 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -2,7 +2,7 @@ [project] name = "pact-python-ffi" -version = "0.4.28.2" +version = "0.5.3.0" description = "Python bindings for the Pact FFI library" readme = "README.md" license = "MIT" From c15032c3bc9de46e852389bd29848701b679ad59 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 17 Apr 2026 05:44:19 +0000 Subject: [PATCH 1347/1376] chore(release): pact-python v3.3.0 --- CHANGELOG.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2e2a140d..869a038d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,70 @@ All notable changes to this project will be documented in this file. +## [pact-python/3.3.0] _2026-04-17_ + +### 🚀 Features + +- Add xml matching + + A new `pact.xml` module provides builder functions for constructing XML request and response bodies with embedded Pact matchers. Use `xml.element()` to describe the XML structure and attach matchers where needed, then wrap the result with `xml.body()` before passing it to `with_body(..., content_type="application/xml")`: + + ```python + from pact import match, xml + + response = xml.body( + xml.element( + "user", + xml.element("id", match.int(123)), + xml.element("name", match.str("Alice")), + ) + ) + interaction.with_body(response, content_type="application/xml") + ``` + + Repeating elements are supported via `.each(min=1, examples=2)` on any `XmlElement`. Attributes (including namespace declarations) can be passed via the `attrs` keyword argument. +- Allow iteration over all interactions +- Use common `PactInteraction` type +- Can toggle follow redirects +- Allow plugin loading delay + +### 📚 Documentation + +- Update changelog for pact-python/3.2.1 +- _(examples)_ Add http+xml example +- Update xml example to use new matcher +- _(examples)_ Add service consumer/provider HTTP example + +### ⚙️ Miscellaneous Tasks + +- _(ci)_ Re-enable 3.14 tests +- Upgrade stable python version +- Add .worktrees to .gitignore +- _(ci)_ Reduce ci usage +- _(ci)_ Downgrade stable python version +- _(ci)_ Remove unused workflows +- Remove versioningit, switch to static version in pyproject.toml +- Add release script +- Minor update to cliff config +- Authenticate gh api calls +- Remove release label +- Replace taplo with tombi +- _(ci)_ Have wheel target 310 +- _(ci)_ Avoid most of CI on draft PRs +- Fix hatch env workspaces +- Remove connect test + +### � Other + +- Fix coverage upload overwrite and add example coverage + +### Contributors + +- @JP-Ellis +- @adityagiri3600 +- @benaduo +- @Nikhil172913832 + ## [pact-python/3.2.1] _2025-12-10_ ### 📚 Documentation diff --git a/pyproject.toml b/pyproject.toml index a9387eb7d..f71f4d82a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ [project] name = "pact-python" -version = "3.2.1" +version = "3.3.0" description = "Tool for creating and verifying consumer-driven contracts using the Pact framework." readme = "README.md" license = { file = "LICENSE" } From 39af1d21d2d371c2efed3a05ac04b1908c745858 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:12:22 +1000 Subject: [PATCH 1348/1376] chore(deps): update pre-commit hook tombi-toml/tombi-pre-commit to v0.9.19 (#1569) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ff9b0312..abd819dfb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -75,7 +75,7 @@ repos: )$ - repo: https://github.com/tombi-toml/tombi-pre-commit - rev: v0.9.18 + rev: v0.9.19 hooks: - id: tombi-format - id: tombi-lint From 3a2486c1b1ecae103281ce08d7f19d15d6b6ab95 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:38:09 +1000 Subject: [PATCH 1349/1376] chore(deps): update taiki-e/install-action action to v2.75.18 (#1570) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release-cli.yml | 2 +- .github/workflows/release-core.yml | 2 +- .github/workflows/release-ffi.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index b0d650f2d..d636a9f1d 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -91,7 +91,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 with: tool: git-cliff,typos diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index c478710f5..5e802e3ee 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -87,7 +87,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 with: tool: git-cliff,typos diff --git a/.github/workflows/release-ffi.yml b/.github/workflows/release-ffi.yml index 83c7bf5a5..432765b95 100644 --- a/.github/workflows/release-ffi.yml +++ b/.github/workflows/release-ffi.yml @@ -92,7 +92,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 with: tool: git-cliff,typos From 9211adc1cf895308a9791d1f31e4bdca5e0b9913 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:32:05 +1000 Subject: [PATCH 1350/1376] chore(deps): update pre-commit hook tombi-toml/tombi-pre-commit to v0.9.20 (#1571) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index abd819dfb..14d37d6ba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -75,7 +75,7 @@ repos: )$ - repo: https://github.com/tombi-toml/tombi-pre-commit - rev: v0.9.19 + rev: v0.9.20 hooks: - id: tombi-format - id: tombi-lint From 61d10c97a004433e4160dd55e4d187b769f4a668 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:32:07 +1000 Subject: [PATCH 1351/1376] chore(deps): update dependency mypy to v1.20.2 (#1573) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 885019013..192f07fa0 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -56,7 +56,7 @@ requires-python = ">=3.10" # developper consistency. All other dependencies are as above. dev = ["ruff==0.15.11", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] -types = ["mypy==1.20.1"] +types = ["mypy==1.20.2"] ## Build System [build-system] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 8b631a556..c8d4536a2 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -43,7 +43,7 @@ dependencies = ["cffi~=2.0"] [dependency-groups] dev = ["ruff==0.15.11", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] -types = ["mypy==1.20.1", "typing-extensions~=4.0"] +types = ["mypy==1.20.2", "typing-extensions~=4.0"] [build-system] requires = ["cffi", "hatchling", "packaging", "setuptools"] diff --git a/pyproject.toml b/pyproject.toml index f71f4d82a..4bb97133b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ test = [ "testcontainers~=4.0", ] types = [ - "mypy==1.20.1", + "mypy==1.20.2", "types-grpcio~=1.0", "types-protobuf~=7.34", "types-requests~=2.0", From 2c180310e66e05cd25429ddf47bf60f77cbcae6e Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 22 Apr 2026 16:17:56 +1000 Subject: [PATCH 1352/1376] chore: simplify find_free_port Since Python 3.2, the socket object implements the context protocol, making the use of `contextlib.closing` redundant. Additionally, `setsockopt` should be called _before_ the port is bound, and therefore its invocation was likely a no-op (if my understanding is correct). Signed-off-by: JP-Ellis --- src/pact/_util.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pact/_util.py b/src/pact/_util.py index ba7dcff73..46df624f1 100644 --- a/src/pact/_util.py +++ b/src/pact/_util.py @@ -13,7 +13,6 @@ import logging import socket import warnings -from contextlib import closing from functools import partial from inspect import Parameter, _ParameterKind from typing import TYPE_CHECKING, TypeVar @@ -173,9 +172,8 @@ def find_free_port() -> int: Returns: The port number. """ - with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("", 0)) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) return s.getsockname()[1] From bb407d3e75f9901ac13b6f0160bd75a5aa120cb3 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 22 Apr 2026 16:12:19 +1000 Subject: [PATCH 1353/1376] fix: avoid rare port clash The FFI has built-in support for finding a free port, and it is exposed in Python through the `PactServer.port` method. This should avoid the rare race condition whereby the same port might be used by two concurrent tests. This was likely caused by the brief time window between Python releasing the port (used to find the free port), and the FFI binding to said port. Fixes: #1541 Signed-off-by: JP-Ellis --- src/pact/pact.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pact/pact.py b/src/pact/pact.py index 3c584f1d6..18c975ec2 100644 --- a/src/pact/pact.py +++ b/src/pact/pact.py @@ -76,7 +76,6 @@ from yarl import URL import pact_ffi -from pact._util import find_free_port from pact.error import ( InteractionVerificationError, Mismatch, @@ -667,7 +666,7 @@ def __init__( # noqa: PLR0913 independently of `raises`. """ self._host = host - self._port = port or find_free_port() + self._port = port or 0 self._transport = transport self._transport_config = transport_config self._pact_handle = pact_handle From 37a0f0a250b14c7d0dd66fc88a74d17f1462640c Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Wed, 22 Apr 2026 14:48:24 +1000 Subject: [PATCH 1354/1376] chore: replace pre-commit with prek Signed-off-by: JP-Ellis --- .github/workflows/test.yml | 35 +++----------- .pre-commit-config.yaml | 98 -------------------------------------- .yamlfmt.yml | 13 ----- prek.toml | 91 +++++++++++++++++++++++++++++++++++ pyproject.toml | 6 +++ 5 files changed, 103 insertions(+), 140 deletions(-) delete mode 100644 .pre-commit-config.yaml delete mode 100644 .yamlfmt.yml create mode 100644 prek.toml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5155b4149..da7af78af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,8 +53,8 @@ jobs: test: name: >- - Test Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, - 'windows-') && 'Windows' || 'Linux' }} + Test Python ${{ matrix.python-version }} + on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ${{ matrix.os }} @@ -127,8 +127,8 @@ jobs: example: name: >- - Test Python Example ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' - || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} + Test Python Example ${{ matrix.python-version }} + on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ${{ matrix.os }} @@ -279,40 +279,17 @@ jobs: run: hatch run typecheck prek: - name: Prek (pre-commit) + name: Prek if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Cache prek - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: |- - ~/.cache/prek - key: >- - ${{ runner.os }}-prek-${{ - hashFiles( - '**/.pre-commit-config.yaml', - '**/.pre-commit-config.yml' - ) }} - restore-keys: |- - ${{ runner.os }}-prek- - - name: Set up uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - with: - enable-cache: true - cache-suffix: prek - cache-dependency-glob: |- - **/.pre-commit-config.yaml - **/.pre-commit-config.yml - - name: Install prek - run: uv tool install prek - name: Install hatch run: uv tool install hatch - - name: Run prek - run: prek run --show-diff-on-failure --color=always --all-files + - uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 14d37d6ba..000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,98 +0,0 @@ ---- -default_install_hook_types: - - pre-commit - - pre-push - -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 - hooks: - - id: check-added-large-files - - id: check-case-conflict - - id: check-executables-have-shebangs - - id: check-illegal-windows-names - - id: check-merge-conflict - - id: check-shebang-scripts-are-executable - - id: check-symlinks - - id: check-vcs-permalinks - - id: destroyed-symlinks - - id: end-of-file-fixer - - id: fix-byte-order-marker - - id: mixed-line-ending - - id: trailing-whitespace - - - repo: https://github.com/google/yamlfmt - rev: v0.21.0 - hooks: - - id: yamlfmt - - - repo: https://gitlab.com/bmares/check-json5 - rev: v1.0.1 - hooks: - # As above, this only checks for valid JSON files. This implementation - # allows for comments within JSON files. - - id: check-json5 - - - repo: https://github.com/biomejs/pre-commit - rev: v2.4.12 - hooks: - - id: biome-check - - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.11 - hooks: - - id: ruff-check - exclude: | - (?x)^( - (src/pact|tests|examples)/v2/.*\\.pyi? - )$ - args: - - --fix - - --exit-non-zero-on-fix - - id: ruff-format - exclude: | - (?x)^( - (src/pact|tests|examples)/v2/.*\\.pyi? - )$ - - - repo: https://github.com/crate-ci/committed - rev: v1.1.11 - hooks: - - id: committed - - - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.22.0 - hooks: - - id: markdownlint-cli2 - - - repo: https://github.com/crate-ci/typos - rev: v1.45.1 - hooks: - - id: typos - exclude: | - (?x)^( - mascot\.svg - )$ - - - repo: https://github.com/tombi-toml/tombi-pre-commit - rev: v0.9.20 - hooks: - - id: tombi-format - - id: tombi-lint - - - repo: local - hooks: - # Mypy is difficult to run pre-commit's isolated environment as it needs - # to be able to find dependencies. - - id: mypy - name: mypy - entry: hatch run mypy - language: system - types: - - python - exclude: | - (?x)^( - (src/pact|tests|examples)/v2/.*\\.pyi? - )$ - stages: - - pre-push diff --git a/.yamlfmt.yml b/.yamlfmt.yml deleted file mode 100644 index 02062fbbd..000000000 --- a/.yamlfmt.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- -line_ending: lf - -formatter: - include_document_start: true - line_ending: lf - retain_line_breaks_single: true - max_line_length: 100 - drop_merge_tag: true - pad_line_comments: 2 - trim_trailing_whitespace: true - eof_newline: true - force_array_style: block diff --git a/prek.toml b/prek.toml new file mode 100644 index 000000000..9f93ffdf4 --- /dev/null +++ b/prek.toml @@ -0,0 +1,91 @@ +#:schema https://raw.githubusercontent.com/j178/prek/refs/heads/master/prek.schema.json +#:tombi toml-version = "v1.1.0" + +[[repos]] +repo = "https://github.com/pre-commit/pre-commit-hooks" +rev = "v6.0.0" +hooks = [ + { id = "check-added-large-files" }, + { id = "check-case-conflict" }, + { id = "check-executables-have-shebangs" }, + { id = "check-illegal-windows-names" }, + { id = "check-merge-conflict" }, + { id = "check-shebang-scripts-are-executable" }, + { id = "check-symlinks" }, + { id = "check-vcs-permalinks" }, + { id = "destroyed-symlinks" }, + { id = "end-of-file-fixer" }, + { id = "fix-byte-order-marker" }, + { id = "mixed-line-ending" }, + { id = "trailing-whitespace" }, +] + +# YAML formatting +[[repos]] +repo = "https://github.com/lyz-code/yamlfix/" +rev = "1.19.1" +hooks = [{ id = "yamlfix" }] + +[[repos]] +repo = "https://gitlab.com/bmares/check-json5" +rev = "v1.0.1" +hooks = [ + # As above, this only checks for valid JSON files. This implementation + # allows for comments within JSON files. + { id = "check-json5" }, +] + +[[repos]] +repo = "https://github.com/biomejs/pre-commit" +rev = "v2.4.12" +hooks = [{ id = "biome-check" }] + +[[repos]] +repo = "https://github.com/astral-sh/ruff-pre-commit" +rev = "v0.15.11" + + [[repos.hooks]] + id = "ruff-check" + exclude = '(src/pact|tests|examples)/v2/.*\.pyi?' + args = ["--fix", "--exit-non-zero-on-fix"] + + [[repos.hooks]] + id = "ruff-format" + exclude = '(src/pact|tests|examples)/v2/.*\.pyi?' + +[[repos]] +repo = "https://github.com/crate-ci/committed" +rev = "v1.1.11" +hooks = [{ id = "committed" }] + +[[repos]] +repo = "https://github.com/DavidAnson/markdownlint-cli2" +rev = "v0.22.0" +hooks = [{ id = "markdownlint-cli2" }] + +[[repos]] +repo = "https://github.com/crate-ci/typos" +rev = "v1.45.1" + + [[repos.hooks]] + id = "typos" + exclude = 'mascot\.svg' + +[[repos]] +repo = "https://github.com/tombi-toml/tombi-pre-commit" +rev = "v0.9.20" +hooks = [{ id = "tombi-format" }, { id = "tombi-lint" }] + +[[repos]] +repo = "local" + + [[repos.hooks]] + # Mypy is difficult to run pre-commit's isolated environment as it needs + # to be able to find dependencies. + id = "mypy" + name = "mypy" + entry = "hatch run mypy" + language = "system" + types = ["python"] + exclude = '(src/pact|tests|examples)/v2/.*\.pyi?' + stages = ["pre-push"] diff --git a/pyproject.toml b/pyproject.toml index 4bb97133b..605862e89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -337,3 +337,9 @@ build-backend = "hatchling.build" [tool.uv.workspace] members = ["pact-python-cli", "pact-python-ffi"] + + [tool.yamlfix] + line_length = 100 + section_whitelines = 1 + sequence_style = "block_style" + whitelines = 1 From 442916ede1be0d178af069036254bb0fbc15e1b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 06:48:16 +0000 Subject: [PATCH 1355/1376] chore(deps): update python:3.14-slim docker digest to 9b9a75b (#1578) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 0b48c700b..ba314b76c 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:bc389f7dfcb21413e72a28f491985326994795e34d2b86c8ae2f417b4e7818aa +FROM python:3.14-slim@sha256:9b9a75b908891c42b7af174dcf3f6534ebcedfc28c874c6281eb452e86470e3e ARG USERNAME=vscode ARG USER_UID=1000 From 325e228f98c202a9482ddf00444a393e401fe642 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Apr 2026 06:50:02 +0000 Subject: [PATCH 1356/1376] chore(release): pact-python v3.3.1 --- CHANGELOG.md | 15 +++++++++++++++ pyproject.toml | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 869a038d2..c8d4dfd37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ All notable changes to this project will be documented in this file. +## [pact-python/3.3.1] _2026-04-22_ + +### 🐛 Bug Fixes + +- Avoid rare port clash + +### ⚙️ Miscellaneous Tasks + +- Simplify find_free_port +- Replace pre-commit with prek + +### Contributors + +- @JP-Ellis + ## [pact-python/3.3.0] _2026-04-17_ ### 🚀 Features diff --git a/pyproject.toml b/pyproject.toml index 605862e89..eb0d3ebed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ [project] name = "pact-python" -version = "3.3.0" +version = "3.3.1" description = "Tool for creating and verifying consumer-driven contracts using the Pact framework." readme = "README.md" license = { file = "LICENSE" } From 4a9f595d07ab3e2a74e3887dcdc450112cfb57ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:20:00 +1000 Subject: [PATCH 1357/1376] chore(deps): update dependency pathspec to v1.1.0 (#1581) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index eb0d3ebed..1a84951b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,7 @@ docs = [ "mkdocs-material[recommended,git,imaging]==9.7.6", "mkdocs-section-index==0.3.12", "mkdocstrings[python]==1.0.4", - "pathspec==1.0.4", + "pathspec==1.1.0", ] example = [ "fastapi~=0.0", From 14ce0ca91760ed1fa682721a12bb25d1a705c245 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:20:02 +1000 Subject: [PATCH 1358/1376] chore(deps): update python:3.14-slim docker digest to 3989a23 (#1580) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index ba314b76c..9b151455d 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:9b9a75b908891c42b7af174dcf3f6534ebcedfc28c874c6281eb452e86470e3e +FROM python:3.14-slim@sha256:3989a23fd2c28a34c7be819e488b958a10601d421ac25bea1e7a5d757365e2d5 ARG USERNAME=vscode ARG USER_UID=1000 From f1afa28d7034aa49a7d5b3d81249a44b43f0d6be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 13:43:01 +1000 Subject: [PATCH 1359/1376] chore(deps): update dependency ruff to v0.15.12 (#1583) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 192f07fa0..d62f9ebd3 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.15.11", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.12", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] types = ["mypy==1.20.2"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index c8d4536a2..37cf5c820 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.15.11", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.12", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] types = ["mypy==1.20.2", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 1a84951b7..afecafb9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.15.11", + "ruff==0.15.12", { include-group = "docs" }, { include-group = "example" }, { include-group = "example-v2" }, From 58dcb8d818580084ea7f103d1ac9a72eb389b03c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 03:49:36 +0000 Subject: [PATCH 1360/1376] chore(deps): update python:3.14-slim docker digest to 5b3879b (#1582) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 9b151455d..cb1664690 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:3989a23fd2c28a34c7be819e488b958a10601d421ac25bea1e7a5d757365e2d5 +FROM python:3.14-slim@sha256:5b3879b6f3cb77e712644d50262d05a7c146b7312d784a18eff7ff5462e77033 ARG USERNAME=vscode ARG USER_UID=1000 From d2934b7024781fdf2d10284e08af2d5515a01a25 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:56:22 +1000 Subject: [PATCH 1361/1376] chore(deps): update dependency pathspec to v1.1.1 (#1586) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index afecafb9b..db9d08a7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,7 @@ docs = [ "mkdocs-material[recommended,git,imaging]==9.7.6", "mkdocs-section-index==0.3.12", "mkdocstrings[python]==1.0.4", - "pathspec==1.1.0", + "pathspec==1.1.1", ] example = [ "fastapi~=0.0", From 084af65e8f9b5951f6cf4ba1eac7055fbc62a622 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:02:29 +0000 Subject: [PATCH 1362/1376] chore(deps): update taiki-e/install-action action to v2.75.23 (#1585) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release-cli.yml | 2 +- .github/workflows/release-core.yml | 2 +- .github/workflows/release-ffi.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index d636a9f1d..de36fd389 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -91,7 +91,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 + uses: taiki-e/install-action@481c34c1cf3a84c68b5e46f4eccfc82af798415a # v2.75.23 with: tool: git-cliff,typos diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index 5e802e3ee..17e7bd927 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -87,7 +87,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 + uses: taiki-e/install-action@481c34c1cf3a84c68b5e46f4eccfc82af798415a # v2.75.23 with: tool: git-cliff,typos diff --git a/.github/workflows/release-ffi.yml b/.github/workflows/release-ffi.yml index 432765b95..bc07b888f 100644 --- a/.github/workflows/release-ffi.yml +++ b/.github/workflows/release-ffi.yml @@ -92,7 +92,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 + uses: taiki-e/install-action@481c34c1cf3a84c68b5e46f4eccfc82af798415a # v2.75.23 with: tool: git-cliff,typos From 43b8dc95aa4704ac148df7a731724d9bb9250fcf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:34:08 +1000 Subject: [PATCH 1363/1376] chore(deps): update j178/prek-action action to v2.0.3 (#1587) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da7af78af..72b3ac7b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -292,4 +292,4 @@ jobs: - name: Install hatch run: uv tool install hatch - - uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2 + - uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3 From 329466431f00250e0fd86693fd275caa53bc7152 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 15:56:32 +1000 Subject: [PATCH 1364/1376] chore(deps): update taiki-e/install-action action to v2.75.30 (#1588) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release-cli.yml | 2 +- .github/workflows/release-core.yml | 2 +- .github/workflows/release-ffi.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index de36fd389..40b966329 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -91,7 +91,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@481c34c1cf3a84c68b5e46f4eccfc82af798415a # v2.75.23 + uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 with: tool: git-cliff,typos diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index 17e7bd927..41b14068c 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -87,7 +87,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@481c34c1cf3a84c68b5e46f4eccfc82af798415a # v2.75.23 + uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 with: tool: git-cliff,typos diff --git a/.github/workflows/release-ffi.yml b/.github/workflows/release-ffi.yml index bc07b888f..4b5262cf7 100644 --- a/.github/workflows/release-ffi.yml +++ b/.github/workflows/release-ffi.yml @@ -92,7 +92,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@481c34c1cf3a84c68b5e46f4eccfc82af798415a # v2.75.23 + uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 with: tool: git-cliff,typos From 440de3e8a88f75a935fdbcf7d28eb5e67d34537b Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 4 May 2026 05:57:01 +0000 Subject: [PATCH 1365/1376] chore(release): pact-python-ffi v0.5.4.0 --- pact-python-ffi/CHANGELOG.md | 4 + pact-python-ffi/pyproject.toml | 2 +- pact-python-ffi/src/pact_ffi/__init__.py | 555 +++++++++++++---------- 3 files changed, 314 insertions(+), 247 deletions(-) diff --git a/pact-python-ffi/CHANGELOG.md b/pact-python-ffi/CHANGELOG.md index b5487a27a..e49d92e53 100644 --- a/pact-python-ffi/CHANGELOG.md +++ b/pact-python-ffi/CHANGELOG.md @@ -8,6 +8,10 @@ Note that this _only_ includes changes to the Python FFI interface. For changes +## [pact-python-ffi/0.5.4.0] _2026-05-04_ + +- Added `add_interaction_reference` + ## [pact-python-ffi/0.5.3.0] _2026-04-16_ ### 🚀 Features diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index 37cf5c820..ce3c06b2a 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -2,7 +2,7 @@ [project] name = "pact-python-ffi" -version = "0.5.3.0" +version = "0.5.4.0" description = "Python bindings for the Pact FFI library" readme = "README.md" license = "MIT" diff --git a/pact-python-ffi/src/pact_ffi/__init__.py b/pact-python-ffi/src/pact_ffi/__init__.py index 732444034..228d6e4a7 100644 --- a/pact-python-ffi/src/pact_ffi/__init__.py +++ b/pact-python-ffi/src/pact_ffi/__init__.py @@ -419,7 +419,7 @@ class InteractionHandle: Handle to a HTTP Interaction. [Rust - `InteractionHandle`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/mock_server/handles/struct.InteractionHandle.html) + `InteractionHandle`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/mock_server/handles/struct.InteractionHandle.html) """ def __init__(self, ref: int) -> None: @@ -919,7 +919,7 @@ class PactHandle: Handle to a Pact. [Rust - `PactHandle`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/mock_server/handles/struct.PactHandle.html) + `PactHandle`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/mock_server/handles/struct.PactHandle.html) """ def __init__(self, ref: int) -> None: @@ -1702,7 +1702,7 @@ class VerifierHandle: """ Handle to a Verifier. - [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/verifier/handle/struct.VerifierHandle.html) + [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/verifier/handle/struct.VerifierHandle.html) """ def __init__(self, ref: cffi.FFI.CData) -> None: @@ -1738,7 +1738,7 @@ class ExpressionValueType(Enum): """ Expression Value Type. - [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/models/expressions/enum.ExpressionValueType.html) + [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/models/expressions/enum.ExpressionValueType.html) """ UNKNOWN = lib.ExpressionValueType_Unknown @@ -1765,7 +1765,7 @@ class GeneratorCategory(Enum): """ Generator Category. - [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/models/generators/enum.GeneratorCategory.html) + [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/models/generators/enum.GeneratorCategory.html) """ METHOD = lib.GeneratorCategory_METHOD @@ -1793,7 +1793,7 @@ class InteractionPart(Enum): """ Interaction Part. - [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/mock_server/handles/enum.InteractionPart.html) + [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/mock_server/handles/enum.InteractionPart.html) """ REQUEST = lib.InteractionPart_Request @@ -1839,7 +1839,7 @@ class MatchingRuleCategory(Enum): """ Matching Rule Category. - [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) + [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) """ METHOD = lib.MatchingRuleCategory_METHOD @@ -1868,7 +1868,7 @@ class PactSpecification(Enum): """ Pact Specification. - [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/models/pact_specification/enum.PactSpecification.html) + [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/models/pact_specification/enum.PactSpecification.html) """ UNKNOWN = lib.PactSpecification_Unknown @@ -1921,7 +1921,7 @@ class _StringResult(Enum): """ Internal enum from Pact FFI. - [Rust `StringResult`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/mock_server/enum.StringResult.html) + [Rust `StringResult`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/mock_server/enum.StringResult.html) """ FAILED = lib.StringResult_Failed @@ -2086,7 +2086,7 @@ def version() -> str: """ Return the version of the pact_ffi library. - [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_version) + [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_version) Returns: The version of the pact_ffi library as a string, in the form of `x.y.z`. @@ -2106,7 +2106,7 @@ def init(log_env_var: str) -> None: tracing subscriber. [Rust - `pactffi_init`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_init) + `pactffi_init`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_init) Args: log_env_var: @@ -2127,7 +2127,7 @@ def init_with_log_level(level: str = "INFO") -> None: tracing subscriber. [Rust - `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_init_with_log_level) + `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_init_with_log_level) Args: level: @@ -2147,7 +2147,7 @@ def enable_ansi_support() -> None: On non-Windows platforms, this function is a no-op. [Rust - `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_enable_ansi_support) + `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_enable_ansi_support) # Safety @@ -2165,7 +2165,7 @@ def log_message( Log using the shared core logging facility. [Rust - `pactffi_log_message`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_log_message) + `pactffi_log_message`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_log_message) This is useful for callers to have a single set of logs. @@ -2195,7 +2195,7 @@ def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: Get an iterator over mismatches. [Rust - `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatches_get_iter) + `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_mismatches_get_iter) """ raise NotImplementedError @@ -2204,7 +2204,7 @@ def mismatches_delete(mismatches: Mismatches) -> None: """ Delete mismatches. - [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatches_delete) + [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_mismatches_delete) """ raise NotImplementedError @@ -2213,7 +2213,7 @@ def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: """ Get the next mismatch from a mismatches iterator. - [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatches_iter_next) + [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_mismatches_iter_next) Returns a null pointer if no mismatches remain. """ @@ -2224,7 +2224,7 @@ def mismatches_iter_delete(iter: MismatchesIterator) -> None: """ Delete a mismatches iterator when you're done with it. - [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatches_iter_delete) + [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_mismatches_iter_delete) """ raise NotImplementedError @@ -2233,7 +2233,7 @@ def mismatch_to_json(mismatch: Mismatch) -> str: """ Get a JSON representation of the mismatch. - [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatch_to_json) + [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_mismatch_to_json) """ raise NotImplementedError @@ -2242,7 +2242,7 @@ def mismatch_type(mismatch: Mismatch) -> str: """ Get the type of a mismatch. - [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatch_type) + [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_mismatch_type) """ raise NotImplementedError @@ -2251,7 +2251,7 @@ def mismatch_summary(mismatch: Mismatch) -> str: """ Get a summary of a mismatch. - [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatch_summary) + [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_mismatch_summary) """ raise NotImplementedError @@ -2260,7 +2260,7 @@ def mismatch_description(mismatch: Mismatch) -> str: """ Get a description of a mismatch. - [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatch_description) + [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_mismatch_description) """ raise NotImplementedError @@ -2269,7 +2269,7 @@ def mismatch_ansi_description(mismatch: Mismatch) -> str: """ Get an ANSI-compatible description of a mismatch. - [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mismatch_ansi_description) + [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_mismatch_ansi_description) """ raise NotImplementedError @@ -2279,7 +2279,7 @@ def get_error_message(length: int = 1024) -> str | None: Provide the error message from `LAST_ERROR` to the calling C code. [Rust - `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_get_error_message) + `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_get_error_message) This function should be called after any other function in the pact_matching FFI indicates a failure with its own error message, if the caller wants to @@ -2337,7 +2337,7 @@ def log_to_stdout(level_filter: LevelFilter) -> int: [`logger_attach_sink`] with the appropriate sink specifier, and then [`logger_apply`]. - [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_log_to_stdout) + [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_log_to_stdout) """ raise NotImplementedError @@ -2351,7 +2351,7 @@ def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: [`logger_apply`]. [Rust - `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_log_to_stderr) + `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_log_to_stderr) Args: level_filter: @@ -2380,7 +2380,7 @@ def log_to_file(file_name: str, level_filter: LevelFilter) -> int: [`logger_apply`]. [Rust - `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_log_to_file) + `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_log_to_file) # Safety @@ -2398,7 +2398,7 @@ def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: [`logger_attach_sink`] with the appropriate sink specifier, and then [`logger_apply`]. - [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_log_to_buffer) + [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_log_to_buffer) Raises: RuntimeError: @@ -2416,7 +2416,7 @@ def logger_init() -> None: """ Initialize the FFI logger with no sinks. - [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_logger_init) + [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_logger_init) This initialized logger does nothing until `pactffi_logger_apply` has been called. @@ -2438,7 +2438,7 @@ def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: Attach an additional sink to the thread-local logger. [Rust - `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_logger_attach_sink) + `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_logger_attach_sink) This logger does nothing until `pactffi_logger_apply` has been called. @@ -2485,7 +2485,7 @@ def logger_apply() -> int: Apply the previously configured sinks and levels to the program. [Rust - `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_logger_apply) + `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_logger_apply) If no sinks have been setup, will set the log level to info and the target to standard out. @@ -2501,7 +2501,7 @@ def fetch_log_buffer(log_id: str) -> str: Fetch the in-memory logger buffer contents. [Rust - `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_fetch_log_buffer) + `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_fetch_log_buffer) This will only have any contents if the `buffer` sink has been configured to log to. The contents will be allocated on the heap and will need to be freed @@ -2527,7 +2527,7 @@ def parse_pact_json(json: str) -> Pact: Parses the provided JSON into a Pact model. [Rust - `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_parse_pact_json) + `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_parse_pact_json) The returned Pact model must be freed with the `pactffi_pact_model_delete` function when no longer needed. @@ -2544,7 +2544,7 @@ def pact_model_delete(pact: Pact) -> None: """ Frees the memory used by the Pact model. - [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_model_delete) + [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_model_delete) """ lib.pactffi_pact_model_delete(pact._ptr) @@ -2554,7 +2554,7 @@ def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: Returns an iterator over all the interactions in the Pact. [Rust - `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_model_interaction_iterator) + `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_model_interaction_iterator) The iterator will contain a copy of the interactions, so it will not be affected but mutations to the Pact model and will still function if the Pact @@ -2576,7 +2576,7 @@ def pact_spec_version(pact: Pact) -> PactSpecification: """ Returns the Pact specification enum that the Pact is for. - [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_spec_version) + [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_spec_version) """ raise NotImplementedError @@ -2585,7 +2585,7 @@ def pact_interaction_delete(interaction: PactInteraction) -> None: """ Frees the memory used by the Pact interaction model. - [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_interaction_delete) + [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_interaction_delete) """ lib.pactffi_pact_interaction_delete(interaction._ptr) @@ -2594,7 +2594,7 @@ def async_message_new() -> AsynchronousMessage: """ Get a mutable pointer to a newly-created default message on the heap. - [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_new) + [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_new) # Safety @@ -2611,7 +2611,7 @@ def async_message_delete(message: AsynchronousMessage) -> None: """ Destroy the `AsynchronousMessage` being pointed to. - [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_delete) + [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_delete) """ lib.pactffi_async_message_delete(message._ptr) @@ -2621,7 +2621,7 @@ def async_message_get_contents(message: AsynchronousMessage) -> MessageContents Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. [Rust - `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_contents) + `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_get_contents) If the message contents are missing, this function will return `None`. """ @@ -2640,7 +2640,7 @@ def async_message_generate_contents( contents as would be received by the consumer. [Rust - `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_generate_contents) + `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_generate_contents) If the message contents are missing, this function will return `None`. """ @@ -2654,7 +2654,7 @@ def async_message_get_contents_str(message: AsynchronousMessage) -> str: """ Get the message contents of an `AsynchronousMessage` in string form. - [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_contents_str) + [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_get_contents_str) # Safety @@ -2681,7 +2681,7 @@ def async_message_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_set_contents_str) + `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_set_contents_str) - `message` - the message to set the contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -2709,7 +2709,7 @@ def async_message_get_contents_length(message: AsynchronousMessage) -> int: Get the length of the contents of a `AsynchronousMessage`. [Rust - `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_contents_length) + `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_get_contents_length) # Safety @@ -2728,7 +2728,7 @@ def async_message_get_contents_bin(message: AsynchronousMessage) -> str: Get the contents of an `AsynchronousMessage` as bytes. [Rust - `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_contents_bin) + `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_get_contents_bin) # Safety @@ -2755,7 +2755,7 @@ def async_message_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_set_contents_bin) + `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_set_contents_bin) * `message` - the message to set the contents for * `contents` - pointer to contents to copy from @@ -2782,7 +2782,7 @@ def async_message_get_description(message: AsynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_description) + `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_get_description) Raises: RuntimeError: @@ -2802,7 +2802,7 @@ def async_message_set_description( """ Write the `description` field on the `AsynchronousMessage`. - [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_set_description) + [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_set_description) # Safety @@ -2827,7 +2827,7 @@ def async_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_provider_state) + `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_get_provider_state) Raises: RuntimeError: @@ -2846,7 +2846,7 @@ def async_message_get_provider_state_iter( """ Get an iterator over provider states. - [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) + [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) # Safety @@ -2861,7 +2861,7 @@ def consumer_get_name(consumer: Consumer) -> str: r""" Get a copy of this consumer's name. - [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_consumer_get_name) + [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_consumer_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -2907,7 +2907,7 @@ def pact_get_consumer(pact: Pact) -> Consumer: `pactffi_pact_consumer_delete` when no longer required. [Rust - `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_get_consumer) + `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_get_consumer) # Errors @@ -2921,7 +2921,7 @@ def pact_consumer_delete(consumer: Consumer) -> None: """ Frees the memory used by the Pact consumer. - [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_consumer_delete) + [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_consumer_delete) """ raise NotImplementedError @@ -2937,7 +2937,7 @@ def message_contents_delete(contents: MessageContents) -> None: Deleting a message content which is associated with an interaction will result in undefined behaviour. - [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_delete) + [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_message_contents_delete) """ lib.pactffi_message_contents_delete(contents._ptr) @@ -2946,7 +2946,7 @@ def message_contents_get_contents_str(contents: MessageContents) -> str | None: """ Get the message contents in string form. - [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_get_contents_str) + [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_message_contents_get_contents_str) If the message has no contents or contain invalid UTF-8 characters, this function will return `None`. @@ -2966,7 +2966,7 @@ def message_contents_set_contents_str( Sets the contents of the message as a string. [Rust - `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_set_contents_str) + `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_message_contents_set_contents_str) * `contents` - the message contents to set the contents for * `contents_str` - pointer to contents to copy from. Must be a valid @@ -2993,7 +2993,7 @@ def message_contents_get_contents_length(contents: MessageContents) -> int: """ Get the length of the message contents. - [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_get_contents_length) + [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_message_contents_get_contents_length) If the message has not contents, this function will return 0. """ @@ -3005,7 +3005,7 @@ def message_contents_get_contents_bin(contents: MessageContents) -> bytes | None Get the contents of a message as a pointer to an array of bytes. [Rust - `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_get_contents_bin) + `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_message_contents_get_contents_bin) If the message has no contents, this function will return `None`. """ @@ -3028,7 +3028,7 @@ def message_contents_set_contents_bin( Sets the contents of the message as an array of bytes. [Rust - `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_set_contents_bin) + `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_message_contents_set_contents_bin) * `message` - the message contents to set the contents for * `contents_bin` - pointer to contents to copy from @@ -3057,7 +3057,7 @@ def message_contents_get_metadata_iter( Get an iterator over the metadata of a message. [Rust - `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) + `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) # Safety @@ -3086,7 +3086,7 @@ def message_contents_get_matching_rule_iter( Get an iterator over the matching rules for a category of a message. [Rust - `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) + `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -3128,7 +3128,7 @@ def request_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP request. - [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) + [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -3165,7 +3165,7 @@ def response_contents_get_matching_rule_iter( r""" Get an iterator over the matching rules for a category of an HTTP response. - [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) + [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) The returned pointer must be deleted with `pactffi_matching_rules_iter_delete` when done with it. @@ -3203,7 +3203,7 @@ def message_contents_get_generators_iter( Get an iterator over the generators for a category of a message. [Rust - `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_contents_get_generators_iter) + `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_message_contents_get_generators_iter) # Safety @@ -3229,7 +3229,7 @@ def request_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP request. [Rust - `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_request_contents_get_generators_iter) + `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_request_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -3254,7 +3254,7 @@ def response_contents_get_generators_iter( Get an iterator over the generators for a category of an HTTP response. [Rust - `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_response_contents_get_generators_iter) + `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_response_contents_get_generators_iter) The returned pointer must be deleted with `pactffi_generators_iter_delete` when done with it. @@ -3279,7 +3279,7 @@ def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: any generator. [Rust - `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_parse_matcher_definition) + `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_parse_matcher_definition) The following are examples of matching rule definitions: @@ -3315,7 +3315,7 @@ def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: using the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matcher_definition_error) + `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matcher_definition_error) """ raise NotImplementedError @@ -3329,7 +3329,7 @@ def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: the `pactffi_string_delete` function once done with it. [Rust - `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matcher_definition_value) + `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matcher_definition_value) Note that different expressions values can have types other than a string. Use `pactffi_matcher_definition_value_type` to get the actual type of the @@ -3343,7 +3343,7 @@ def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: """ Frees the memory used by the result of parsing the matching definition expression. - [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matcher_definition_delete) + [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matcher_definition_delete) """ raise NotImplementedError @@ -3356,7 +3356,7 @@ def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Ge NULL pointer, otherwise returns the generator as a pointer. [Rust - `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matcher_definition_generator) + `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matcher_definition_generator) The generator pointer will be a valid pointer as long as `pactffi_matcher_definition_delete` has not been called on the definition. @@ -3375,7 +3375,7 @@ def matcher_definition_value_type( If there was an error parsing the expression, it will return Unknown. [Rust - `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matcher_definition_value_type) + `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matcher_definition_value_type) """ raise NotImplementedError @@ -3384,7 +3384,7 @@ def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: """ Free the iterator when you're done using it. - [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_iter_delete) + [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matching_rule_iter_delete) """ raise NotImplementedError @@ -3399,7 +3399,7 @@ def matcher_definition_iter( `pactffi_matching_rule_iter_delete` function once done with it. [Rust - `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matcher_definition_iter) + `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matcher_definition_iter) If there was an error parsing the expression, this function will return a NULL pointer. @@ -3415,7 +3415,7 @@ def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: deleted but will be cleaned up when the iterator is deleted. [Rust - `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_iter_next) + `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matching_rule_iter_next) Will return a NULL pointer when the iterator has advanced past the end of the list. @@ -3437,7 +3437,7 @@ def matching_rule_id(rule_result: MatchingRuleResult) -> int: Return the ID of the matching rule. [Rust - `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_id) + `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matching_rule_id) The ID corresponds to the following rules: @@ -3483,7 +3483,7 @@ def matching_rule_value(rule_result: MatchingRuleResult) -> str: pointer. [Rust - `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_value) + `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matching_rule_value) The associated values for the rules are: @@ -3531,7 +3531,7 @@ def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: Will return a NULL pointer if the matching rule result was a reference. [Rust - `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_pointer) + `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matching_rule_pointer) # Safety @@ -3549,7 +3549,7 @@ def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: structure. I.e., [Rust - `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_reference_name) + `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matching_rule_reference_name) ```json { @@ -3576,7 +3576,7 @@ def validate_datetime(value: str, format: str) -> None: Validates the date/time value against the date/time format string. [Rust - `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_validate_datetime) + `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_validate_datetime) Raises: ValueError: @@ -3603,7 +3603,7 @@ def generator_to_json(generator: Generator) -> str: Get the JSON form of the generator. [Rust - `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generator_to_json) + `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_generator_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -3626,7 +3626,7 @@ def generator_generate_string(generator: Generator, context_json: str) -> str: function). [Rust - `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generator_generate_string) + `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_generator_generate_string) If anything goes wrong, it will return a NULL pointer. """ @@ -3649,7 +3649,7 @@ def generator_generate_integer(generator: Generator, context_json: str) -> int: should be the values returned from the Provider State callback function). [Rust - `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generator_generate_integer) + `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_generator_generate_integer) If anything goes wrong or the generator is not a type that can generate an integer value, it will return a zero value. @@ -3665,7 +3665,7 @@ def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generators_iter_delete) + `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_generators_iter_delete) """ lib.pactffi_generators_iter_delete(iter._ptr) @@ -3675,7 +3675,7 @@ def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePa Get the next path and generator out of the iterator, if possible. [Rust - `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generators_iter_next) + `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_generators_iter_next) The returned pointer must be deleted with `pactffi_generator_iter_pair_delete`. @@ -3695,7 +3695,7 @@ def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: Free a pair of key and value returned from `pactffi_generators_iter_next`. [Rust - `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generators_iter_pair_delete) + `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_generators_iter_pair_delete) """ lib.pactffi_generators_iter_pair_delete(pair._ptr) @@ -3704,7 +3704,7 @@ def sync_http_new() -> SynchronousHttp: """ Get a mutable pointer to a newly-created default interaction on the heap. - [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_new) + [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_new) # Safety @@ -3722,7 +3722,7 @@ def sync_http_delete(interaction: SynchronousHttp) -> None: Destroy the `SynchronousHttp` interaction being pointed to. [Rust - `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_delete) + `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_delete) """ lib.pactffi_sync_http_delete(interaction._ptr) @@ -3732,7 +3732,7 @@ def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: Get the request of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_request) + `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_get_request) # Safety @@ -3752,7 +3752,7 @@ def sync_http_get_request_contents(interaction: SynchronousHttp) -> str | None: Get the request contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_request_contents) + `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_get_request_contents) Note that this function will return `None` if either the body is missing or is `null`. @@ -3772,7 +3772,7 @@ def sync_http_set_request_contents( Sets the request contents of the interaction. [Rust - `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_set_request_contents) + `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_set_request_contents) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -3800,7 +3800,7 @@ def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: Get the length of the request contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) + `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) This function will return 0 if the body is missing. """ @@ -3812,7 +3812,7 @@ def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes | Get the request contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) + `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) Note that this function will return `None` if either the body is missing or is `null`. @@ -3836,7 +3836,7 @@ def sync_http_set_request_contents_bin( Sets the request contents of the interaction as an array of bytes. [Rust - `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) + `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) - `interaction` - the interaction to set the request contents for - `contents` - pointer to contents to copy from @@ -3863,7 +3863,7 @@ def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: Get the response of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_response) + `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_get_response) # Safety @@ -3883,7 +3883,7 @@ def sync_http_get_response_contents(interaction: SynchronousHttp) -> str | None: Get the response contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_response_contents) + `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_get_response_contents) Note that this function will return `None` if either the body is missing or is `null`. @@ -3903,7 +3903,7 @@ def sync_http_set_response_contents( Sets the response contents of the interaction. [Rust - `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_set_response_contents) + `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_set_response_contents) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -3931,7 +3931,7 @@ def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: Get the length of the response contents of a `SynchronousHttp` interaction. [Rust - `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) + `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) This function will return 0 if the body is missing. """ @@ -3943,7 +3943,7 @@ def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes | Get the response contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) + `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) Note that this function will return `None` if either the body is missing or is `null`. @@ -3967,7 +3967,7 @@ def sync_http_set_response_contents_bin( Sets the response contents of the `SynchronousHttp` interaction as bytes. [Rust - `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) + `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) - `interaction` - the interaction to set the response contents for - `contents` - pointer to contents to copy from @@ -3994,7 +3994,7 @@ def sync_http_get_description(interaction: SynchronousHttp) -> str: Get a copy of the description. [Rust - `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_description) + `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_get_description) Raises: RuntimeError: @@ -4012,7 +4012,7 @@ def sync_http_set_description(interaction: SynchronousHttp, description: str) -> Write the `description` field on the `SynchronousHttp`. [Rust - `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_set_description) + `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_set_description) # Safety @@ -4037,7 +4037,7 @@ def sync_http_get_provider_state( Get a copy of the provider state at the given index from this interaction. [Rust - `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_provider_state) + `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_get_provider_state) # Safety @@ -4063,7 +4063,7 @@ def sync_http_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) + `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) # Safety @@ -4086,7 +4086,7 @@ def pact_interaction_as_synchronous_http( """ Cast this interaction to a `SynchronousHttp` interaction. - [Rust `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) + [Rust `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) Args: interaction: @@ -4116,7 +4116,7 @@ def pact_interaction_as_asynchronous_message( Note that if the interaction is a V3 `Message`, it will be converted to a V4 `AsynchronousMessage` before being returned. - [Rust `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) + [Rust `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) Args: interaction: @@ -4143,7 +4143,7 @@ def pact_interaction_as_synchronous_message( """ Cast this interaction to a `SynchronousMessage` interaction. - [Rust `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) + [Rust `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) Args: interaction: @@ -4169,7 +4169,7 @@ def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> Asynchronous Get the next asynchronous message from the iterator. [Rust - `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_async_message_iter_next) + `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_async_message_iter_next) Raises: StopIteration: @@ -4186,7 +4186,7 @@ def pact_async_message_iter_delete(iter: PactAsyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_async_message_iter_delete) + `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_async_message_iter_delete) """ lib.pactffi_pact_async_message_iter_delete(iter._ptr) @@ -4196,7 +4196,7 @@ def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMes Get the next synchronous request/response message from the V4 pact. [Rust - `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_sync_message_iter_next) + `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_sync_message_iter_next) Raises: StopIteration: @@ -4213,7 +4213,7 @@ def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) + `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) """ lib.pactffi_pact_sync_message_iter_delete(iter._ptr) @@ -4223,7 +4223,7 @@ def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: Get the next synchronous HTTP request/response interaction from the V4 pact. [Rust - `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_sync_http_iter_next) + `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_sync_http_iter_next) Raises: StopIteration: @@ -4240,7 +4240,7 @@ def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) + `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) """ lib.pactffi_pact_sync_http_iter_delete(iter._ptr) @@ -4250,7 +4250,7 @@ def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction Get the next interaction from the pact. [Rust - `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_interaction_iter_next) + `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_interaction_iter_next) Raises: StopIteration: @@ -4267,7 +4267,7 @@ def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_interaction_iter_delete) + `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_interaction_iter_delete) """ lib.pactffi_pact_interaction_iter_delete(iter._ptr) @@ -4277,7 +4277,7 @@ def pact_message_iter_next(iter: PactMessageIterator) -> PactInteraction: Get the next interaction from the pact. [Rust - `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_message_iter_next) + `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_message_iter_next) Raises: StopIteration: @@ -4294,7 +4294,7 @@ def pact_message_iter_delete(iter: PactMessageIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_message_iter_delete) + `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_message_iter_delete) """ lib.pactffi_pact_message_iter_delete(iter._ptr) @@ -4304,7 +4304,7 @@ def matching_rule_to_json(rule: MatchingRule) -> str: Get the JSON form of the matching rule. [Rust - `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rule_to_json) + `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matching_rule_to_json) The returned string must be deleted with `pactffi_string_delete`. @@ -4321,7 +4321,7 @@ def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: Free the iterator when you're done using it. [Rust - `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rules_iter_delete) + `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matching_rules_iter_delete) """ lib.pactffi_matching_rules_iter_delete(iter._ptr) @@ -4333,7 +4333,7 @@ def matching_rules_iter_next( Get the next path and matching rule out of the iterator, if possible. [Rust - `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rules_iter_next) + `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matching_rules_iter_next) The returned pointer must be deleted with `pactffi_matching_rules_iter_pair_delete`. @@ -4355,7 +4355,7 @@ def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) + `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) """ lib.pactffi_matching_rules_iter_pair_delete(pair._ptr) @@ -4365,7 +4365,7 @@ def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: Get the next value from the iterator. [Rust - `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_iter_next) + `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_provider_state_iter_next) # Safety @@ -4386,7 +4386,7 @@ def provider_state_iter_delete(iter: ProviderStateIterator) -> None: Delete the iterator. [Rust - `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_iter_delete) + `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_provider_state_iter_delete) """ lib.pactffi_provider_state_iter_delete(iter._ptr) @@ -4396,7 +4396,7 @@ def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadata Get the next key and value out of the iterator, if possible. [Rust - `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_metadata_iter_next) + `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_message_metadata_iter_next) The returned pointer must be deleted with `pactffi_message_metadata_pair_delete`. @@ -4422,7 +4422,7 @@ def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: Free the metadata iterator when you're done using it. [Rust - `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_metadata_iter_delete) + `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_message_metadata_iter_delete) """ lib.pactffi_message_metadata_iter_delete(iter._ptr) @@ -4432,7 +4432,7 @@ def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: Free a pair of key and value returned from `message_metadata_iter_next`. [Rust - `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_message_metadata_pair_delete) + `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_message_metadata_pair_delete) """ lib.pactffi_message_metadata_pair_delete(pair._ptr) @@ -4442,7 +4442,7 @@ def provider_get_name(provider: Provider) -> str: Get a copy of this provider's name. [Rust - `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_get_name) + `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_provider_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -4488,7 +4488,7 @@ def pact_get_provider(pact: Pact) -> Provider: `pactffi_pact_provider_delete` when no longer required. [Rust - `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_get_provider) + `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_get_provider) # Errors @@ -4503,7 +4503,7 @@ def pact_provider_delete(provider: Provider) -> None: Frees the memory used by the Pact provider. [Rust - `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_provider_delete) + `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_provider_delete) """ raise NotImplementedError @@ -4513,7 +4513,7 @@ def provider_state_get_name(provider_state: ProviderState) -> str | None: Get the name of the provider state as a string. [Rust - `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_get_name) + `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_provider_state_get_name) Raises: RuntimeError: @@ -4533,7 +4533,7 @@ def provider_state_get_param_iter( Get an iterator over the params of a provider state. [Rust - `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_get_param_iter) + `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_provider_state_get_param_iter) # Safety @@ -4561,7 +4561,7 @@ def provider_state_param_iter_next( Get the next key and value out of the iterator, if possible. [Rust - `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_param_iter_next) + `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_provider_state_param_iter_next) # Safety @@ -4582,7 +4582,7 @@ def provider_state_delete(provider_state: ProviderState) -> None: Free the provider state when you're done using it. [Rust - `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_delete) + `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_provider_state_delete) """ raise NotImplementedError @@ -4592,7 +4592,7 @@ def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: Free the provider state param iterator when you're done using it. [Rust - `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_param_iter_delete) + `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_provider_state_param_iter_delete) """ lib.pactffi_provider_state_param_iter_delete(iter._ptr) @@ -4602,7 +4602,7 @@ def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: Free a pair of key and value. [Rust - `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_provider_state_param_pair_delete) + `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_provider_state_param_pair_delete) """ lib.pactffi_provider_state_param_pair_delete(pair._ptr) @@ -4612,7 +4612,7 @@ def sync_message_new() -> SynchronousMessage: Get a mutable pointer to a newly-created default message on the heap. [Rust - `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_new) + `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_new) # Safety @@ -4630,7 +4630,7 @@ def sync_message_delete(message: SynchronousMessage) -> None: Destroy the `Message` being pointed to. [Rust - `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_delete) + `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_delete) """ lib.pactffi_sync_message_delete(message._ptr) @@ -4640,7 +4640,7 @@ def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: Get the request contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) + `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) # Safety @@ -4667,7 +4667,7 @@ def sync_message_set_request_contents_str( Sets the request contents of the message. [Rust - `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) + `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) - `message` - the message to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -4695,7 +4695,7 @@ def sync_message_get_request_contents_length(message: SynchronousMessage) -> int Get the length of the request contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) + `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) # Safety @@ -4714,7 +4714,7 @@ def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes: Get the request contents of a `SynchronousMessage` as a bytes. [Rust - `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) + `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) # Safety @@ -4741,7 +4741,7 @@ def sync_message_set_request_contents_bin( Sets the request contents of the message as an array of bytes. [Rust - `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) + `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) * `message` - the message to set the request contents for * `contents` - pointer to contents to copy from @@ -4768,7 +4768,7 @@ def sync_message_get_request_contents(message: SynchronousMessage) -> MessageCon Get the request contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_request_contents) + `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_get_request_contents) # Safety @@ -4795,7 +4795,7 @@ def sync_message_generate_request_contents( contents as would be received by the consumer. [Rust - `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_generate_request_contents) + `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_generate_request_contents) Raises: RuntimeError: @@ -4813,7 +4813,7 @@ def sync_message_get_number_responses(message: SynchronousMessage) -> int: Get the number of response messages in the `SynchronousMessage`. [Rust - `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_number_responses) + `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_get_number_responses) If the message is null, this function will return 0. """ @@ -4828,7 +4828,7 @@ def sync_message_get_response_contents_str( Get the response contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) + `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) # Safety @@ -4861,7 +4861,7 @@ def sync_message_set_response_contents_str( with default values. [Rust - `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) + `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response. @@ -4893,7 +4893,7 @@ def sync_message_get_response_contents_length( Get the length of the response contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) + `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) # Safety @@ -4915,7 +4915,7 @@ def sync_message_get_response_contents_bin( Get the response contents of a `SynchronousMessage` as bytes. [Rust - `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) + `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) # Safety @@ -4946,7 +4946,7 @@ def sync_message_set_response_contents_bin( responses will be padded with default values. [Rust - `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) + `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response @@ -4977,7 +4977,7 @@ def sync_message_get_response_contents( Get the response contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_response_contents) + `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_get_response_contents) # Safety @@ -5006,7 +5006,7 @@ def sync_message_generate_response_contents( received by the consumer. [Rust - `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_generate_response_contents) + `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_generate_response_contents) Raises: RuntimeError: @@ -5024,7 +5024,7 @@ def sync_message_get_description(message: SynchronousMessage) -> str: Get a copy of the description. [Rust - `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_description) + `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_get_description) Raises: RuntimeError: @@ -5042,7 +5042,7 @@ def sync_message_set_description(message: SynchronousMessage, description: str) Write the `description` field on the `SynchronousMessage`. [Rust - `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_set_description) + `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_set_description) # Safety @@ -5067,7 +5067,7 @@ def sync_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_provider_state) + `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_get_provider_state) # Safety @@ -5093,7 +5093,7 @@ def sync_message_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) + `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) # Safety @@ -5115,7 +5115,7 @@ def string_delete(string: OwnedString) -> None: Delete a string previously returned by this FFI. [Rust - `pactffi_string_delete`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_string_delete) + `pactffi_string_delete`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_string_delete) """ lib.pactffi_string_delete(string._ptr) @@ -5125,7 +5125,7 @@ def get_tls_ca_certificate() -> OwnedString: Fetch the CA Certificate used to generate the self-signed certificate. [Rust - `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_get_tls_ca_certificate) + `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_get_tls_ca_certificate) **NOTE:** The string for the result is allocated on the heap, and will have to be freed by the caller using [`string_delete`][pact_ffi.string_delete]. @@ -5148,7 +5148,7 @@ def create_mock_server_for_transport( Create a mock server for the provided Pact handle and transport. [Rust - `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_create_mock_server_for_transport) + `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_create_mock_server_for_transport) Args: pact: @@ -5210,7 +5210,7 @@ def mock_server_matched(mock_server_handle: PactServerHandle) -> bool: if any request has not been successfully matched, or the method panics. [Rust - `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mock_server_matched) + `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_mock_server_matched) """ return lib.pactffi_mock_server_matched(mock_server_handle._ref) @@ -5222,7 +5222,7 @@ def mock_server_mismatches( External interface to get all the mismatches from a mock server. [Rust - `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mock_server_mismatches) + `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_mock_server_mismatches) # Errors @@ -5249,7 +5249,7 @@ def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: and cleanup any memory allocated for it. [Rust - `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_cleanup_mock_server) + `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_cleanup_mock_server) Args: mock_server_handle: @@ -5278,7 +5278,7 @@ def write_pact_file( directory to write the file to is passed as the second parameter. [Rust - `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_write_pact_file) + `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_write_pact_file) Args: mock_server_handle: @@ -5330,7 +5330,7 @@ def mock_server_logs(mock_server_handle: PactServerHandle) -> str: started. [Rust - `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_mock_server_logs) + `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_mock_server_logs) Raises: RuntimeError: @@ -5354,7 +5354,7 @@ def generate_datetime_string(format: str) -> StringResult: string needs to be freed with the `pactffi_string_delete` function [Rust - `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generate_datetime_string) + `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_generate_datetime_string) # Safety @@ -5371,7 +5371,7 @@ def check_regex(regex: str, example: str) -> bool: Checks that the example string matches the given regex. [Rust - `pactffi_check_regex`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_check_regex) + `pactffi_check_regex`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_check_regex) # Safety @@ -5390,7 +5390,7 @@ def generate_regex_value(regex: str) -> StringResult: `pactffi_string_delete` function. [Rust - `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_generate_regex_value) + `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_generate_regex_value) # Safety @@ -5405,7 +5405,7 @@ def free_string(s: str) -> None: [DEPRECATED] Frees the memory allocated to a string by another function. [Rust - `pactffi_free_string`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_free_string) + `pactffi_free_string`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_free_string) This function is deprecated. Use `pactffi_string_delete` instead. @@ -5427,7 +5427,7 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle: Creates a new Pact model and returns a handle to it. [Rust - `pactffi_new_pact`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_new_pact) + `pactffi_new_pact`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_new_pact) Args: consumer_name: @@ -5480,7 +5480,7 @@ def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: will result in that interaction being replaced with the new one. [Rust - `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_new_interaction) + `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_new_interaction) Args: pact: @@ -5508,7 +5508,7 @@ def new_message_interaction(pact: PactHandle, description: str) -> InteractionHa will result in that interaction being replaced with the new one. [Rust - `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_new_message_interaction) + `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_new_message_interaction) Args: pact: @@ -5539,7 +5539,7 @@ def new_sync_message_interaction( will result in that interaction being replaced with the new one. [Rust - `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_new_sync_message_interaction) + `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_new_sync_message_interaction) Args: pact: @@ -5564,7 +5564,7 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> None: Sets the description for the Interaction. [Rust - `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_upon_receiving) + `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_upon_receiving) This function @@ -5605,7 +5605,7 @@ def given(interaction: InteractionHandle, description: str) -> None: Adds a provider state to the Interaction. [Rust - `pactffi_given`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_given) + `pactffi_given`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_given) Args: interaction: @@ -5632,7 +5632,7 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> Non used with V4 interactions. [Rust - `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_interaction_test_name) + `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_interaction_test_name) Args: interaction: @@ -5679,7 +5679,7 @@ def given_with_param( be parsed as JSON. [Rust - `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_given_with_param) + `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_given_with_param) Args: interaction: @@ -5721,7 +5721,7 @@ def given_with_params( with a `value` key. [Rust - `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_given_with_params) + `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_given_with_params) Args: interaction: @@ -5760,7 +5760,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None Configures the request for the Interaction. [Rust - `pactffi_with_request`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_request) + `pactffi_with_request`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_with_request) Args: interaction: @@ -5774,7 +5774,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md) + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.4/rust/pact_ffi/IntegrationJson.md) which allows regex patterns. For examples: ```json @@ -5809,7 +5809,7 @@ def with_query_parameter_v2( Configures a query parameter for the Interaction. [Rust - `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_query_parameter_v2) + `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_with_query_parameter_v2) To setup a query parameter with multiple values, you can either call this function multiple times with a different index value: @@ -5846,7 +5846,7 @@ def with_query_parameter_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.4/rust/pact_ffi/IntegrationJson.md) If you want the matching rules to apply to all values (and not just the one with the given index), make sure to set the value to be an array. @@ -5898,7 +5898,7 @@ def with_query_parameter_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.4/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -5920,7 +5920,7 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None: Sets the specification version for a given Pact model. [Rust - `pactffi_with_specification`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_specification) + `pactffi_with_specification`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_with_specification) Args: pact: @@ -5944,7 +5944,7 @@ def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: Fetches the Pact specification version for the given Pact model. [Rust - `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_handle_get_pact_spec_version) + `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_handle_get_pact_spec_version) Args: handle: @@ -5970,7 +5970,7 @@ def with_pact_metadata( the mock server for it has already started) or the namespace is readonly. [Rust - `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_pact_metadata) + `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_with_pact_metadata) Args: pact: @@ -6036,7 +6036,7 @@ def with_metadata( ``` See - [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md) + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.4/rust/pact_ffi/IntegrationJson.md) # Note @@ -6085,7 +6085,7 @@ def with_header_v2( r""" Configures a header for the Interaction. - [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_header_v2) + [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_with_header_v2) To setup a header with multiple values, you can either call this function multiple times with a different index value: @@ -6123,7 +6123,7 @@ def with_header_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.4/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -6145,7 +6145,7 @@ def with_header_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.4/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -6177,7 +6177,7 @@ def set_header( and generators can not be configured with it. [Rust - `pactffi_set_header`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_set_header) + `pactffi_set_header`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_set_header) If matching rules are required to be set, use `pactffi_with_header_v2`. @@ -6215,7 +6215,7 @@ def response_status(interaction: InteractionHandle, status: int) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_response_status) + `pactffi_response_status`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_response_status) Args: interaction: @@ -6239,7 +6239,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_response_status_v2) + `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_response_status_v2) To include matching rules for the status (only statusCode or integer really makes sense to use), include the matching rule JSON format with the value as @@ -6258,7 +6258,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.4/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -6269,7 +6269,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.4/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -6293,7 +6293,7 @@ def with_body( Adds the body for the interaction. [Rust - `pactffi_with_body`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_body) + `pactffi_with_body`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_with_body) Returns false if the interaction or Pact can't be modified (i.e. the mock server for it has already started) @@ -6328,7 +6328,7 @@ def with_body( body: The body contents. For JSON payloads, matching rules can be embedded in the body. See - [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.3/rust/pact_ffi/IntegrationJson.md). + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.5.4/rust/pact_ffi/IntegrationJson.md). Raises: RuntimeError: @@ -6355,7 +6355,7 @@ def with_binary_body( Adds the body for the interaction. [Rust - `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_binary_body) + `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_with_binary_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -6415,7 +6415,7 @@ def with_binary_file( already started) [Rust - `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_binary_file) + `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_with_binary_file) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -6462,7 +6462,7 @@ def with_matching_rules( Add matching rules to the interaction. [Rust - `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_matching_rules) + `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_with_matching_rules) This function can be called multiple times, in which case the matching rules will be merged. @@ -6500,7 +6500,7 @@ def with_generators( Add generators to the interaction. [Rust - `pactffi_with_generators`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_generators) + `pactffi_with_generators`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_with_generators) This function can be called multiple times, in which case the generators will be combined (provide they don't clash). @@ -6548,7 +6548,7 @@ def with_multipart_file_v2( # noqa: PLR0913 already started) or an error occurs. [Rust - `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_multipart_file_v2) + `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_with_multipart_file_v2) This function can be called multiple times. In that case, each subsequent call will be appended to the existing multipart body as a new part. @@ -6602,7 +6602,7 @@ def with_multipart_file( already started) or an error occurs. [Rust - `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_with_multipart_file) + `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_with_multipart_file) * `interaction` - Interaction handle to set the body for. * `part` - Request or response part. @@ -6639,7 +6639,7 @@ def set_key(interaction: InteractionHandle, key: str | None) -> None: Sets the key attribute for the interaction. [Rust - `pactffi_set_key`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_set_key) + `pactffi_set_key`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_set_key) Args: interaction: @@ -6667,7 +6667,7 @@ def set_pending(interaction: InteractionHandle, *, pending: bool) -> None: Mark the interaction as pending. [Rust - `pactffi_set_pending`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_set_pending) + `pactffi_set_pending`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_set_pending) Args: interaction: @@ -6691,7 +6691,7 @@ def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> Add a comment to the interaction. [Rust - `pactffi_set_comment`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_set_comment) + `pactffi_set_comment`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_set_comment) Args: interaction: @@ -6725,7 +6725,7 @@ def add_text_comment(interaction: InteractionHandle, comment: str) -> None: Add a text comment to the interaction. [Rust - `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_add_text_comment) + `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_add_text_comment) Args: interaction: @@ -6747,6 +6747,69 @@ def add_text_comment(interaction: InteractionHandle, comment: str) -> None: raise RuntimeError(msg) +def add_interaction_reference( + interaction: InteractionHandle, + group: str, + name: str, + value: str, +) -> None: + """ + Add an external reference to the interaction. + + The reference will be stored in the Pact file comments under the + `references` key, grouped by `group`. For instance, you could store the + AsyncAPI operation ID that the interaction corresponds to as an external + reference, or a pull request reference. + + [Rust + `pactffi_add_interaction_reference`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_add_interaction_reference) + + Args: + interaction: + Interaction handle to modify. + + group: + Group or system the reference belongs to (e.g. `"Jira"`, + `"OpenAPI"`, `"GitHub"`). + + name: + Name or identifier of the reference (e.g. `"TICKET"`, + `"OperationID"`, `"PullRequest"`). + + value: + Value of the reference, typically an ID (e.g., `"TICKET-123"`, + `"getUserById"`, `"#123"`). + + + + Args: + interaction: + Interaction handle to modify. + + group: + Group or system the reference belongs to (e.g. ``"Jira"``). + + name: + Name or identifier of the reference (e.g. ``"TICKET-123"``). + + value: + Value of the reference, typically an ID. + + Raises: + RuntimeError: + If the reference could not be added. + """ + success: bool = lib.pactffi_add_interaction_reference( + interaction._ref, + group.encode("utf-8"), + name.encode("utf-8"), + value.encode("utf-8"), + ) + if not success: + msg = f"Failed to add interaction reference for {interaction}." + raise RuntimeError(msg) + + def pact_handle_get_async_message_iter(pact: PactHandle) -> PactAsyncMessageIterator: r""" Get an iterator over all the asynchronous messages of the Pact. @@ -6755,7 +6818,7 @@ def pact_handle_get_async_message_iter(pact: PactHandle) -> PactAsyncMessageIter `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6781,7 +6844,7 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6807,7 +6870,7 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: `pactffi_pact_sync_http_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) + `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) # Safety @@ -6831,7 +6894,7 @@ def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: `pactffi_pact_message_iter_delete`. [Rust - `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_handle_get_message_iter) + `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_handle_get_message_iter) # Safety @@ -6859,7 +6922,7 @@ def pact_handle_write_file( External interface to write out the pact file. [Rust - `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_pact_handle_write_file) + `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_pact_handle_write_file) This function should be called if all the consumer tests have passed. @@ -6903,7 +6966,7 @@ def free_pact_handle(pact: PactHandle) -> None: Delete a Pact handle and free the resources used by it. [Rust - `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_free_pact_handle) + `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_free_pact_handle) Raises: RuntimeError: @@ -6929,7 +6992,7 @@ def verifier_new_for_application() -> VerifierHandle: to set the required values and enable it. [Rust - `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_new_for_application) + `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_new_for_application) """ result: cffi.FFI.CData = lib.pactffi_verifier_new_for_application( b"pact-python", @@ -6942,7 +7005,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None: """ Shutdown the verifier and release all resources. - [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_shutdown) + [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_shutdown) """ lib.pactffi_verifier_shutdown(handle._ref) @@ -6959,7 +7022,7 @@ def verifier_set_provider_info( # noqa: PLR0913 Set the provider details for the Pact verifier. [Rust - `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_provider_info) + `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_set_provider_info) Args: handle: @@ -7005,7 +7068,7 @@ def verifier_add_provider_transport( Adds a new transport for the given provider. [Rust - `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_add_provider_transport) + `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_add_provider_transport) Args: handle: @@ -7046,7 +7109,7 @@ def verifier_set_filter_info( Set the filters for the Pact verifier. [Rust - `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_filter_info) + `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_set_filter_info) Set filters to narrow down the interactions to verify. @@ -7082,7 +7145,7 @@ def verifier_set_provider_state( Set the provider state URL for the Pact verifier. [Rust - `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_provider_state) + `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_set_provider_state) Args: handle: @@ -7117,7 +7180,7 @@ def verifier_set_verification_options( Set the options used by the verifier when calling the provider. [Rust - `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_verification_options) + `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_set_verification_options) Args: handle: @@ -7152,7 +7215,7 @@ def verifier_set_coloured_output( Enables or disables coloured output using ANSI escape codes. [Rust - `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_coloured_output) + `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_set_coloured_output) By default, coloured output is enabled. @@ -7181,7 +7244,7 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> Enables or disables if no pacts are found to verify results in an error. [Rust - `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) + `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) Args: handle: @@ -7214,7 +7277,7 @@ def verifier_set_publish_options( Set the options used when publishing verification results to the Broker. [Rust - `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_publish_options) + `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_set_publish_options) Args: handle: @@ -7257,7 +7320,7 @@ def verifier_set_consumer_filters( Set the consumer filters for the Pact verifier. [Rust - `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_consumer_filters) + `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_set_consumer_filters) """ lib.pactffi_verifier_set_consumer_filters( handle._ref, @@ -7275,7 +7338,7 @@ def verifier_add_custom_header( Adds a custom header to be added to the requests made to the provider. [Rust - `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_add_custom_header) + `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_add_custom_header) """ lib.pactffi_verifier_add_custom_header( handle._ref, @@ -7293,7 +7356,7 @@ def verifier_set_follow_redirects( Sets whether redirects should be automatically followed. [Rust - `pactffi_verifier_set_follow_redirects`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_set_follow_redirects) + `pactffi_verifier_set_follow_redirects`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_set_follow_redirects) Args: handle: @@ -7314,7 +7377,7 @@ def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: Adds a Pact file as a source to verify. [Rust - `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_add_file_source) + `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_add_file_source) """ lib.pactffi_verifier_add_file_source(handle._ref, file.encode("utf-8")) @@ -7326,7 +7389,7 @@ def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> Non All pacts from the directory that match the provider name will be verified. [Rust - `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_add_directory_source) + `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_add_directory_source) # Safety @@ -7348,7 +7411,7 @@ def verifier_url_source( Adds a URL as a source to verify. [Rust - `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_url_source) + `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_url_source) Args: handle: @@ -7388,7 +7451,7 @@ def verifier_broker_source( Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_broker_source) + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_broker_source) This will fetch all the pact files from the broker that match the provider name. @@ -7436,7 +7499,7 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) + `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) This will fetch all the pact files from the broker that match the provider name and the consumer version selectors (See [Consumer Version @@ -7516,7 +7579,7 @@ def verifier_execute(handle: VerifierHandle) -> None: Runs the verification. [Rust - `pactffi_verifier_execute`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_execute) + `pactffi_verifier_execute`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_execute) Raises: RuntimeError: @@ -7533,7 +7596,7 @@ def verifier_logs(handle: VerifierHandle) -> OwnedString: Extracts the logs for the verification run. [Rust - `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_logs) + `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_logs) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` @@ -7555,7 +7618,7 @@ def verifier_logs_for_provider(provider_name: str) -> OwnedString: Extracts the logs for the verification run for the provider name. [Rust - `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_logs_for_provider) + `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_logs_for_provider) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` @@ -7577,7 +7640,7 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: Extracts the standard output for the verification run. [Rust - `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_output) + `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_output) Args: handle: @@ -7604,7 +7667,7 @@ def verifier_json(handle: VerifierHandle) -> OwnedString: Extracts the verification result as a JSON document. [Rust - `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_verifier_json) + `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_verifier_json) Raises: RuntimeError: @@ -7632,7 +7695,7 @@ def using_plugin_with_delay( afterwards by calling [`cleanup_plugins`][pact_ffi.cleanup_plugins] otherwise you will have plugin processes left running. - [Rust `pactffi_using_plugin_with_delay`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_using_plugin_with_delay) + [Rust `pactffi_using_plugin_with_delay`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_using_plugin_with_delay) Args: pact: @@ -7687,7 +7750,7 @@ def using_plugin( otherwise you will have plugin processes left running. [Rust - `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_using_plugin) + `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_using_plugin) Args: pact: @@ -7730,7 +7793,7 @@ def cleanup_plugins(pact: PactHandle) -> None: zero). [Rust - `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_cleanup_plugins) + `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_cleanup_plugins) """ lib.pactffi_cleanup_plugins(pact._ref) @@ -7749,7 +7812,7 @@ def interaction_contents( format of the JSON contents. [Rust - `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_interaction_contents) + `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_interaction_contents) Args: interaction: @@ -7809,7 +7872,7 @@ def matches_string_value( function once it is no longer required. [Rust - `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_string_value) + `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matches_string_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string @@ -7840,7 +7903,7 @@ def matches_u64_value( function once it is no longer required. [Rust - `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_u64_value) + `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matches_u64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7870,7 +7933,7 @@ def matches_i64_value( function once it is no longer required. [Rust - `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_i64_value) + `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matches_i64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7900,7 +7963,7 @@ def matches_f64_value( function once it is no longer required. [Rust - `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_f64_value) + `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matches_f64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7930,7 +7993,7 @@ def matches_bool_value( function once it is no longer required. [Rust - `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_bool_value) + `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matches_bool_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get, 0 == false and 1 == true @@ -7962,7 +8025,7 @@ def matches_binary_value( # noqa: PLR0913 function once it is no longer required. [Rust - `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_binary_value) + `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matches_binary_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7997,7 +8060,7 @@ def matches_json_value( function once it is no longer required. [Rust - `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.5.3/pact_ffi/?search=pactffi_matches_json_value) + `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.5.4/pact_ffi/?search=pactffi_matches_json_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string From a64c2270749a85fe38769325f498d023f0a53e49 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 4 May 2026 10:24:03 +1000 Subject: [PATCH 1366/1376] feat: add external reference dsl --- src/pact/interaction/_base.py | 49 +++++++++++++++++++ .../test_async_message_interaction.py | 22 +++++++++ tests/interaction/test_http_interaction.py | 40 +++++++++++++++ .../test_sync_message_interaction.py | 26 ++++++++++ 4 files changed, 137 insertions(+) diff --git a/src/pact/interaction/_base.py b/src/pact/interaction/_base.py index 825f7d485..fc1dbc137 100644 --- a/src/pact/interaction/_base.py +++ b/src/pact/interaction/_base.py @@ -480,6 +480,55 @@ def add_text_comment(self, comment: str) -> Self: pact_ffi.add_text_comment(self._handle, comment) return self + def add_external_reference( + self, + group: str, + name: str, + value: str, + ) -> Self: + """ + Add an external reference to the interaction. + + This is used by V4 interactions to record references to external + resources, such as tickets or pull requests, against the interaction. + References are stored under `comments.references[group][name]` in the + generated Pact file. + + This method may be called multiple times to add references to multiple + external systems. Calling it with the same `group` and `name` will + overwrite the previous value. + + Args: + group: + Group or system the reference belongs to (e.g. `"Jira"`, + `"OpenAPI"`, `"GitHub"`). + + name: + Name or identifier of the reference (e.g. `"TICKET"`, + `"OperationID"`, `"PullRequest"`). + + value: + Value of the reference, typically an ID (e.g., `"TICKET-123"`, + `"getUserById"`, `"#123"`). + + Example: + ```python + ( + pact + .upon_receiving("a request") + .add_external_reference( + "Jira", + "TICKET-123", + "https://jira.example.com/browse/TICKET-123", + ) + .with_request("GET", "/users/123") + .will_respond_with(200) + ) + ``` + """ + pact_ffi.add_interaction_reference(self._handle, group, name, value) + return self + def test_name( self, name: str, diff --git a/tests/interaction/test_async_message_interaction.py b/tests/interaction/test_async_message_interaction.py index 761d8b328..1078b17f4 100644 --- a/tests/interaction/test_async_message_interaction.py +++ b/tests/interaction/test_async_message_interaction.py @@ -4,12 +4,17 @@ from __future__ import annotations +import json import re +from typing import TYPE_CHECKING import pytest from pact import Pact +if TYPE_CHECKING: + from pathlib import Path + @pytest.fixture def pact() -> Pact: @@ -33,3 +38,20 @@ def test_repr(pact: Pact) -> None: ) is not None ) + + +def test_add_external_reference(pact: Pact, tmp_path: Path) -> None: + ( + pact + .upon_receiving("an async message with an external reference", "Async") + .add_external_reference( + "GitHub", + "PR-456", + "https://github.com/org/repo/pull/456", + ) + .with_body({"event": "user.created"}) + ) + pact.write_file(tmp_path) + data = json.load((tmp_path / "consumer-provider.json").open()) + references = data["interactions"][0]["comments"]["references"] + assert references["GitHub"]["PR-456"] == "https://github.com/org/repo/pull/456" diff --git a/tests/interaction/test_http_interaction.py b/tests/interaction/test_http_interaction.py index 589e447c3..6e7445a44 100644 --- a/tests/interaction/test_http_interaction.py +++ b/tests/interaction/test_http_interaction.py @@ -589,6 +589,46 @@ async def test_name(pact: Pact) -> None: assert await resp.read() == b"" +def test_add_external_reference(pact: Pact, tmp_path: Path) -> None: + ( + pact + .upon_receiving("a request with an external reference") + .add_external_reference( + "Jira", + "TICKET-123", + "https://jira.example.com/browse/TICKET-123", + ) + .with_request("GET", "/") + .will_respond_with(200) + ) + pact.write_file(tmp_path) + data = json.load((tmp_path / "consumer-provider.json").open()) + references = data["interactions"][0]["comments"]["references"] + assert ( + references["Jira"]["TICKET-123"] == "https://jira.example.com/browse/TICKET-123" + ) + + +def test_add_external_reference_multiple(pact: Pact, tmp_path: Path) -> None: + ( + pact + .upon_receiving("a request with multiple external references") + .add_external_reference( + "Jira", "TICKET-123", "https://jira.example.com/TICKET-123" + ) + .add_external_reference( + "GitHub", "PR-456", "https://github.com/org/repo/pull/456" + ) + .with_request("GET", "/") + .will_respond_with(200) + ) + pact.write_file(tmp_path) + data = json.load((tmp_path / "consumer-provider.json").open()) + references = data["interactions"][0]["comments"]["references"] + assert references["Jira"]["TICKET-123"] == "https://jira.example.com/TICKET-123" + assert references["GitHub"]["PR-456"] == "https://github.com/org/repo/pull/456" + + @pytest.mark.asyncio async def test_with_plugin(pact: Pact) -> None: ( diff --git a/tests/interaction/test_sync_message_interaction.py b/tests/interaction/test_sync_message_interaction.py index 98164de66..ef3603da6 100644 --- a/tests/interaction/test_sync_message_interaction.py +++ b/tests/interaction/test_sync_message_interaction.py @@ -4,13 +4,18 @@ from __future__ import annotations +import json import re +from typing import TYPE_CHECKING from unittest.mock import MagicMock import pytest from pact import Pact +if TYPE_CHECKING: + from pathlib import Path + @pytest.fixture def pact() -> Pact: @@ -106,3 +111,24 @@ def test_with_metadata_with_part(pact: Pact) -> None: assert handler.call_args[0][1]["foo"] == {"bar": 1.23} assert "metadata" in handler.call_args[0][1] assert handler.call_args[0][1]["metadata"] == 123 + + +def test_add_external_reference(pact: Pact, tmp_path: Path) -> None: + ( + pact + .upon_receiving("a sync message with an external reference", "Sync") + .add_external_reference( + "Jira", + "TICKET-789", + "https://jira.example.com/browse/TICKET-789", + ) + .with_body("request", content_type="text/plain") + .will_respond_with() + .with_body("response", content_type="text/plain") + ) + pact.write_file(tmp_path) + data = json.load((tmp_path / "consumer-provider.json").open()) + references = data["interactions"][0]["comments"]["references"] + assert ( + references["Jira"]["TICKET-789"] == "https://jira.example.com/browse/TICKET-789" + ) From ff3ea64ab25912068fc26dad37cf420048f28bb6 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 4 May 2026 07:25:38 +0000 Subject: [PATCH 1367/1376] chore(release): pact-python v3.4.0 --- CHANGELOG.md | 11 +++++++++++ pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8d4dfd37..ecf5b0046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ All notable changes to this project will be documented in this file. +## [pact-python/3.4.0] _2026-05-04_ + +### 🚀 Features + +- Add external reference dsl + +### Contributors + +- @rholshausen +- @JP-Ellis + ## [pact-python/3.3.1] _2026-04-22_ ### 🐛 Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index db9d08a7c..4fe303db1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ [project] name = "pact-python" -version = "3.3.1" +version = "3.4.0" description = "Tool for creating and verifying consumer-driven contracts using the Pact framework." readme = "README.md" license = { file = "LICENSE" } From 5b81ac05d042ae2635f6f26e23824d8f6b025821 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 09:49:31 +1000 Subject: [PATCH 1368/1376] chore(deps): update dependency mypy to v2 (#1592) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index d62f9ebd3..a70d6d0fd 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -56,7 +56,7 @@ requires-python = ">=3.10" # developper consistency. All other dependencies are as above. dev = ["ruff==0.15.12", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] -types = ["mypy==1.20.2"] +types = ["mypy==2.0.0"] ## Build System [build-system] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index ce3c06b2a..e822c3db1 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -43,7 +43,7 @@ dependencies = ["cffi~=2.0"] [dependency-groups] dev = ["ruff==0.15.12", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] -types = ["mypy==1.20.2", "typing-extensions~=4.0"] +types = ["mypy==2.0.0", "typing-extensions~=4.0"] [build-system] requires = ["cffi", "hatchling", "packaging", "setuptools"] diff --git a/pyproject.toml b/pyproject.toml index 4fe303db1..b0f6722ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ test = [ "testcontainers~=4.0", ] types = [ - "mypy==1.20.2", + "mypy==2.0.0", "types-grpcio~=1.0", "types-protobuf~=7.34", "types-requests~=2.0", From 5265218356f143b8d4727958ef309089c238cea1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 09:46:48 +1000 Subject: [PATCH 1369/1376] chore(deps): update python:3.14-slim docker digest to 1697e8e (#1593) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index cb1664690..f32181153 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:5b3879b6f3cb77e712644d50262d05a7c146b7312d784a18eff7ff5462e77033 +FROM python:3.14-slim@sha256:1697e8e8d39bf168e177ac6b5fdab6df86d81cfc24dae17dfb96cfc3ef76b4dd ARG USERNAME=vscode ARG USER_UID=1000 From 0a708b5cfa4e37f84971a012cd3f43cc427ccf86 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 09:57:58 +1000 Subject: [PATCH 1370/1376] chore(deps): update python:3.14-slim docker digest to 33ef744 (#1596) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index f32181153..bf8119fcc 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:1697e8e8d39bf168e177ac6b5fdab6df86d81cfc24dae17dfb96cfc3ef76b4dd +FROM python:3.14-slim@sha256:33ef7446e8c14b21cb247e23afbcdc90e98853b70812ca46b2265e769a7dfb8b ARG USERNAME=vscode ARG USER_UID=1000 From 48f990f77e9b14323a910a688466ff1f72deecfe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 10:09:18 +1000 Subject: [PATCH 1371/1376] chore(deps): update taiki-e/install-action action to v2.77.6 (#1594) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release-cli.yml | 2 +- .github/workflows/release-core.yml | 2 +- .github/workflows/release-ffi.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 40b966329..e3e1898ce 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -91,7 +91,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 + uses: taiki-e/install-action@c070f87102a1c75b3183910f391c1cb887fe13c8 # v2.77.6 with: tool: git-cliff,typos diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index 41b14068c..9ca491389 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -87,7 +87,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 + uses: taiki-e/install-action@c070f87102a1c75b3183910f391c1cb887fe13c8 # v2.77.6 with: tool: git-cliff,typos diff --git a/.github/workflows/release-ffi.yml b/.github/workflows/release-ffi.yml index 4b5262cf7..30cb5567e 100644 --- a/.github/workflows/release-ffi.yml +++ b/.github/workflows/release-ffi.yml @@ -92,7 +92,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 + uses: taiki-e/install-action@c070f87102a1c75b3183910f391c1cb887fe13c8 # v2.77.6 with: tool: git-cliff,typos From 02636ee5b72ecb9db43380141d56c98b7c69c24d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 10:09:37 +1000 Subject: [PATCH 1372/1376] chore(deps): update dependency mypy to v2.1.0 (#1595) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index a70d6d0fd..7a1cb04f1 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -56,7 +56,7 @@ requires-python = ">=3.10" # developper consistency. All other dependencies are as above. dev = ["ruff==0.15.12", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] -types = ["mypy==2.0.0"] +types = ["mypy==2.1.0"] ## Build System [build-system] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index e822c3db1..e851b009f 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -43,7 +43,7 @@ dependencies = ["cffi~=2.0"] [dependency-groups] dev = ["ruff==0.15.12", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] -types = ["mypy==2.0.0", "typing-extensions~=4.0"] +types = ["mypy==2.1.0", "typing-extensions~=4.0"] [build-system] requires = ["cffi", "hatchling", "packaging", "setuptools"] diff --git a/pyproject.toml b/pyproject.toml index b0f6722ac..9ed2d48c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ test = [ "testcontainers~=4.0", ] types = [ - "mypy==2.0.0", + "mypy==2.1.0", "types-grpcio~=1.0", "types-protobuf~=7.34", "types-requests~=2.0", From f728af79d615c3af0fed05a95c4598926023bd77 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 10:05:20 +1000 Subject: [PATCH 1373/1376] chore(deps): update python:3.14-slim docker digest to 7a50012 (#1597) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index bf8119fcc..d6a4f4f5d 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:33ef7446e8c14b21cb247e23afbcdc90e98853b70812ca46b2265e769a7dfb8b +FROM python:3.14-slim@sha256:7a500125bc50693f2214e842a621440a1b1b9cbb2188f74ab045d29ed2ea5856 ARG USERNAME=vscode ARG USER_UID=1000 From bf1e4e53bff7400f2e4b9dedcdb5123507bba027 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 10:06:17 +1000 Subject: [PATCH 1374/1376] chore(deps): update dependency ruff to v0.15.13 (#1598) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pact-python-cli/pyproject.toml | 2 +- pact-python-ffi/pyproject.toml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index 7a1cb04f1..8cb440061 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -54,7 +54,7 @@ requires-python = ">=3.10" [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. -dev = ["ruff==0.15.12", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.13", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] types = ["mypy==2.1.0"] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index e851b009f..9214c367e 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -41,7 +41,7 @@ dependencies = ["cffi~=2.0"] "Repository" = "https://github.com/pact-foundation/pact-python" [dependency-groups] -dev = ["ruff==0.15.12", { include-group = "test" }, { include-group = "types" }] +dev = ["ruff==0.15.13", { include-group = "test" }, { include-group = "types" }] test = ["pytest~=9.0", "pytest-cov~=7.0"] types = ["mypy==2.1.0", "typing-extensions~=4.0"] diff --git a/pyproject.toml b/pyproject.toml index 9ed2d48c0..31c090a6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. dev = [ - "ruff==0.15.12", + "ruff==0.15.13", { include-group = "docs" }, { include-group = "example" }, { include-group = "example-v2" }, From 969e4a1f73ee836dbf273d49b56c5a79a36ac8b3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 12:10:21 +1000 Subject: [PATCH 1375/1376] chore(deps): update j178/prek-action action to v2.0.4 (#1599) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72b3ac7b0..523c1aa86 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -292,4 +292,4 @@ jobs: - name: Install hatch run: uv tool install hatch - - uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3 + - uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4 From 2ee72c4e0ce26017cde22911e1bf5a082ab14110 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 12:27:53 +1000 Subject: [PATCH 1376/1376] chore(deps): update taiki-e/install-action action to v2.79.0 (#1600) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release-cli.yml | 2 +- .github/workflows/release-core.yml | 2 +- .github/workflows/release-ffi.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index e3e1898ce..9f3a77ac5 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -91,7 +91,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@c070f87102a1c75b3183910f391c1cb887fe13c8 # v2.77.6 + uses: taiki-e/install-action@7be9fd86bd1707236395105d6e9329dd1511a7e1 # v2.79.0 with: tool: git-cliff,typos diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index 9ca491389..a12059bce 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -87,7 +87,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@c070f87102a1c75b3183910f391c1cb887fe13c8 # v2.77.6 + uses: taiki-e/install-action@7be9fd86bd1707236395105d6e9329dd1511a7e1 # v2.79.0 with: tool: git-cliff,typos diff --git a/.github/workflows/release-ffi.yml b/.github/workflows/release-ffi.yml index 30cb5567e..fba4dedfc 100644 --- a/.github/workflows/release-ffi.yml +++ b/.github/workflows/release-ffi.yml @@ -92,7 +92,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} - name: Install git-cliff and typos - uses: taiki-e/install-action@c070f87102a1c75b3183910f391c1cb887fe13c8 # v2.77.6 + uses: taiki-e/install-action@7be9fd86bd1707236395105d6e9329dd1511a7e1 # v2.79.0 with: tool: git-cliff,typos