Compare commits

..

No commits in common. "main" and "v0.7.4" have entirely different histories.
main ... v0.7.4

50 changed files with 499 additions and 2849 deletions

View File

@ -21,30 +21,32 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- name: Install uv and set the python version - name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
cache: "pip"
allow-prereleases: true
- name: Install dependencies - name: Install hatch
run: uv sync --frozen run: pip install hatch
- name: Test with pytest - name: Test with pytest
run: uv run cov --frozen run: hatch run cov
- name: Run E2E tests with behave - name: Run E2E tests with behave
run: uv run e2e --frozen run: hatch run e2e
- if: matrix.python-version == '3.13' - if: matrix.python-version == '3.11'
name: Upload coverage to Codecov name: Upload coverage to Codecov
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 uses: codecov/codecov-action@v5.0.7
with: with:
flags: unittests # optional flags: unittests # optional
name: coverage # optional name: coverage # optional
@ -56,17 +58,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@v4
- name: Install uv and set the python version - uses: actions/setup-python@v5
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6
with: with:
python-version: "3.13" python-version: "3.11"
cache: "pip"
- name: Install dependencies
run: uv sync --frozen
- name: Run pre-commit - name: Run pre-commit
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 uses: pre-commit/action@v3.0.1
sast: sast:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -75,18 +74,16 @@ jobs:
contents: read contents: read
security-events: write security-events: write
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@v4
- name: Install uv and set the python version - uses: actions/setup-python@v5
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6
with: with:
python-version: "3.13" python-version: "3.11"
ignore-nothing-to-cache: true
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3 uses: github/codeql-action/init@v3
with: with:
languages: python languages: python
config-file: ./.github/codeql-config.yml config-file: ./.github/codeql-config.yml
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3 uses: github/codeql-action/analyze@v3

View File

@ -20,6 +20,6 @@ jobs:
name: Validate PR title name: Validate PR title
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 - uses: amannn/action-semantic-pull-request@v5
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -21,39 +21,38 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: googleapis/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3 - uses: googleapis/release-please-action@v4
id: release id: release
with: with:
command: manifest
token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}} token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}}
default-branch: main target-branch: main
signoff: "OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>"
outputs: outputs:
release_created: ${{ steps.release.outputs.release_created }} release_created: ${{ steps.release.outputs.release_created }}
release_tag_name: ${{ steps.release.outputs.tag_name }} release_tag_name: ${{ steps.release.outputs.tag_name }}
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: publish environment: release
permissions: permissions:
# IMPORTANT: this permission is mandatory for trusted publishing to pypi # IMPORTANT: this permission is mandatory for trusted publishing to pypi
id-token: write id-token: write
needs: release-please needs: release-please
if: ${{ fromJSON(needs.release-please.outputs.release_created || false) }} if: ${{ needs.release-please.outputs.release_created }}
container:
image: "python:3.13"
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@v4
- name: Install uv and set the python version - name: Upgrade pip
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6 run: pip install --upgrade pip
with:
python-version: "3.13"
- name: Install dependencies - name: Install hatch
run: uv sync --frozen run: pip install hatch
- name: Build a binary wheel and a source tarball - name: Build a binary wheel and a source tarball
run: uv build run: hatch build
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1 # pinning till fixed https://github.com/pypa/gh-action-pypi-publish/issues/300
uses: pypa/gh-action-pypi-publish@release/v1.11

6
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "spec"] [submodule "test-harness"]
path = spec path = test-harness
url = https://github.com/open-feature/spec.git url = https://github.com/open-feature/test-harness.git

View File

@ -1,9 +1,9 @@
default_stages: [pre-commit] default_stages: [commit]
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.4 rev: v0.8.0
hooks: hooks:
- id: ruff-check - id: ruff
args: [--fix] args: [--fix]
- id: ruff-format - id: ruff-format
@ -16,7 +16,7 @@ repos:
- id: check-merge-conflict - id: check-merge-conflict
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.17.1 rev: v1.13.0
hooks: hooks:
- id: mypy - id: mypy
files: openfeature files: openfeature

View File

@ -1 +1 @@
{".":"0.8.2"} {".":"0.7.4"}

View File

@ -1,190 +1,5 @@
# Changelog # Changelog
## [0.8.2](https://github.com/open-feature/python-sdk/compare/v0.8.1...v0.8.2) (2025-07-30)
### 🐛 Bug Fixes
* merge transaction context into hook context evaluation context ([#521](https://github.com/open-feature/python-sdk/issues/521)) ([#523](https://github.com/open-feature/python-sdk/issues/523)) ([a5b3aa9](https://github.com/open-feature/python-sdk/commit/a5b3aa9c5213dda311068695f9209282f5faaff5))
### ✨ New Features
* starting migration to uv ([#512](https://github.com/open-feature/python-sdk/issues/512)) ([fb47cbb](https://github.com/open-feature/python-sdk/commit/fb47cbb2a51da9154adf977aad0b16575d227c33))
### 🧹 Chore
* **deps:** pin astral-sh/setup-uv action to bd01e18 ([#514](https://github.com/open-feature/python-sdk/issues/514)) ([6da7890](https://github.com/open-feature/python-sdk/commit/6da7890ac6488bccf640f74bdc530fa9ce8bbec3))
* **deps:** update actions/setup-python digest to a26af69 ([#489](https://github.com/open-feature/python-sdk/issues/489)) ([ad69f2c](https://github.com/open-feature/python-sdk/commit/ad69f2c55f3c8170a8a53981238130eb106207ba))
* **deps:** update astral-sh/setup-uv digest to 7edac99 ([#524](https://github.com/open-feature/python-sdk/issues/524)) ([5652c0c](https://github.com/open-feature/python-sdk/commit/5652c0c457cc5a524e91405f3b229cf245ae4531))
* **deps:** update codecov/codecov-action action to v5.4.2 ([#486](https://github.com/open-feature/python-sdk/issues/486)) ([798ac8d](https://github.com/open-feature/python-sdk/commit/798ac8ded00b8509068003367f36e6c04c574cbc))
* **deps:** update codecov/codecov-action action to v5.4.3 ([#497](https://github.com/open-feature/python-sdk/issues/497)) ([7bb0f5e](https://github.com/open-feature/python-sdk/commit/7bb0f5e499ff8e0985b24696d0680251c90af32b))
* **deps:** update github/codeql-action digest to 181d5ee ([#517](https://github.com/open-feature/python-sdk/issues/517)) ([a04e52c](https://github.com/open-feature/python-sdk/commit/a04e52c0224a6c1c269218df050ce7a56076211d))
* **deps:** update github/codeql-action digest to 28deaed ([#488](https://github.com/open-feature/python-sdk/issues/488)) ([e0de4b2](https://github.com/open-feature/python-sdk/commit/e0de4b2faa109454a8079b934320f1c2b2b2b06e))
* **deps:** update github/codeql-action digest to 39edc49 ([#515](https://github.com/open-feature/python-sdk/issues/515)) ([21ef53a](https://github.com/open-feature/python-sdk/commit/21ef53a156b17ce24db79c75b5bbfeaf2bd77f01))
* **deps:** update github/codeql-action digest to 60168ef ([#492](https://github.com/open-feature/python-sdk/issues/492)) ([8aedfe8](https://github.com/open-feature/python-sdk/commit/8aedfe81ef67af3210ea9921e6b364fdd21ef8ac))
* **deps:** update github/codeql-action digest to ce28f5b ([#508](https://github.com/open-feature/python-sdk/issues/508)) ([4628c24](https://github.com/open-feature/python-sdk/commit/4628c24f5c94821aecb06388703173bd5a8efc30))
* **deps:** update github/codeql-action digest to fca7ace ([#505](https://github.com/open-feature/python-sdk/issues/505)) ([347517a](https://github.com/open-feature/python-sdk/commit/347517a7ccaf145a940fc6e2a37a8d1df621f3a3))
* **deps:** update github/codeql-action digest to ff0a06e ([#498](https://github.com/open-feature/python-sdk/issues/498)) ([c722cf0](https://github.com/open-feature/python-sdk/commit/c722cf0239f2b9b95a1214b99447a9316c2c73d8))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.10 ([#496](https://github.com/open-feature/python-sdk/issues/496)) ([1dd8b29](https://github.com/open-feature/python-sdk/commit/1dd8b294930ee94e9c27a7bad4e43fda76bb1f9d))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.11 ([#499](https://github.com/open-feature/python-sdk/issues/499)) ([abb3137](https://github.com/open-feature/python-sdk/commit/abb31377790f95e4900e92f57050a4a12dc9f311))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.12 ([#501](https://github.com/open-feature/python-sdk/issues/501)) ([8dfa88c](https://github.com/open-feature/python-sdk/commit/8dfa88cf8aacada8b8b20803a51667b586e2182a))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.13 ([#507](https://github.com/open-feature/python-sdk/issues/507)) ([a21413b](https://github.com/open-feature/python-sdk/commit/a21413bd5069a9fd32378e197ae7709b34f001a5))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.5 ([#484](https://github.com/open-feature/python-sdk/issues/484)) ([95be943](https://github.com/open-feature/python-sdk/commit/95be943d33b9cfca137f94e5a1ad52bc562e4b4a))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.6 ([#487](https://github.com/open-feature/python-sdk/issues/487)) ([7fe752d](https://github.com/open-feature/python-sdk/commit/7fe752d8fd6148a38d53e5c7dfc071a6c8121c85))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.7 ([#490](https://github.com/open-feature/python-sdk/issues/490)) ([f4f9a12](https://github.com/open-feature/python-sdk/commit/f4f9a12081871f4797dc0e649219dc7be58d4116))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.8 ([#491](https://github.com/open-feature/python-sdk/issues/491)) ([1f16955](https://github.com/open-feature/python-sdk/commit/1f169551e37f286597400e5cb2c171a60a38436e))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.9 ([#493](https://github.com/open-feature/python-sdk/issues/493)) ([42fed6b](https://github.com/open-feature/python-sdk/commit/42fed6b2001b9056ed9bc7a390213206829a7b99))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.12.0 ([#510](https://github.com/open-feature/python-sdk/issues/510)) ([d21d9db](https://github.com/open-feature/python-sdk/commit/d21d9db90ae6f5f15f1aaf25a4e5d0669dbe1d96))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.12.1 ([#513](https://github.com/open-feature/python-sdk/issues/513)) ([7783a8b](https://github.com/open-feature/python-sdk/commit/7783a8b6c798246fc861fcb83e24427ea0f43e5f))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.12.2 ([#518](https://github.com/open-feature/python-sdk/issues/518)) ([288bd6b](https://github.com/open-feature/python-sdk/commit/288bd6bb34f2d5e857ae5b2b17f02f79276049c5))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.12.4 ([#525](https://github.com/open-feature/python-sdk/issues/525)) ([90a193d](https://github.com/open-feature/python-sdk/commit/90a193d22c64b8b708300dd436a16b8c7d632686))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.16.0 ([#503](https://github.com/open-feature/python-sdk/issues/503)) ([87e4485](https://github.com/open-feature/python-sdk/commit/87e448593d723b8239244a198e54e6cb056d3f95))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.16.1 ([#509](https://github.com/open-feature/python-sdk/issues/509)) ([ac95c7a](https://github.com/open-feature/python-sdk/commit/ac95c7a5b72d56e78dcb8e3411178967b00c04d9))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.17.0 ([#526](https://github.com/open-feature/python-sdk/issues/526)) ([00cab65](https://github.com/open-feature/python-sdk/commit/00cab65315ff3bc38f26b7281d8b258e6ec0f47d))
* switch build backend to uv ([#527](https://github.com/open-feature/python-sdk/issues/527)) ([a369890](https://github.com/open-feature/python-sdk/commit/a3698902b55a1c9d53d4a7db2e6b18e4f2e77c70))
* use publish env ([d54d239](https://github.com/open-feature/python-sdk/commit/d54d239a2d55359d9b49aa534a4b53339bf26571))
### 🔄 Refactoring
* refine typing.Any type hints ([#504](https://github.com/open-feature/python-sdk/issues/504)) ([f95b27a](https://github.com/open-feature/python-sdk/commit/f95b27a25ae1dda7281c2039ec9060363de2703e))
## [0.8.1](https://github.com/open-feature/python-sdk/compare/v0.8.0...v0.8.1) (2025-04-09)
### 🐛 Bug Fixes
* add passthrough init to abstract provider ([#450](https://github.com/open-feature/python-sdk/issues/450)) ([088409e](https://github.com/open-feature/python-sdk/commit/088409ea5cdefef33f28fc4f45026fabac52377a))
* fix cycle dependency between api and client ([#480](https://github.com/open-feature/python-sdk/issues/480)) ([3636a0d](https://github.com/open-feature/python-sdk/commit/3636a0d75f69712844a768cbc6c2f80fdcf6eb84))
### ✨ New Features
* add OTel utility function ([#451](https://github.com/open-feature/python-sdk/issues/451)) ([2d1ba85](https://github.com/open-feature/python-sdk/commit/2d1ba85c93cdd954f539d2872783b21683bd8b07))
### 🧹 Chore
* add codeowner file to be consistent with the rest of openfeature ([#477](https://github.com/open-feature/python-sdk/issues/477)) ([7a30ef9](https://github.com/open-feature/python-sdk/commit/7a30ef914b3180fc72be9a1d2072a8a288e8b54d))
* **deps:** update actions/setup-python digest to 8d9ed9a ([#473](https://github.com/open-feature/python-sdk/issues/473)) ([a135911](https://github.com/open-feature/python-sdk/commit/a1359112e9c1d740bcca501cbb5aadd9da3602b6))
* **deps:** update codecov/codecov-action action to v5.4.0 ([#456](https://github.com/open-feature/python-sdk/issues/456)) ([a666227](https://github.com/open-feature/python-sdk/commit/a666227f55b14d4d2b6e43b6487ac643b6893739))
* **deps:** update github/codeql-action digest to 1b549b9 ([#470](https://github.com/open-feature/python-sdk/issues/470)) ([4eeab3b](https://github.com/open-feature/python-sdk/commit/4eeab3b6914bd947a63f8d3c5bb89b85b7c2ced1))
* **deps:** update github/codeql-action digest to 45775bd ([#483](https://github.com/open-feature/python-sdk/issues/483)) ([5a2825b](https://github.com/open-feature/python-sdk/commit/5a2825b00db0653c6d0496ec7f4703f9125cbed7))
* **deps:** update github/codeql-action digest to 5f8171a ([#467](https://github.com/open-feature/python-sdk/issues/467)) ([d69b759](https://github.com/open-feature/python-sdk/commit/d69b7594a956a49385ef3030c212624d628aec74))
* **deps:** update github/codeql-action digest to 6bb031a ([#462](https://github.com/open-feature/python-sdk/issues/462)) ([0396592](https://github.com/open-feature/python-sdk/commit/0396592586b6f721754c18a46b6d2fee3c2f80e8))
* **deps:** update github/codeql-action digest to b56ba49 ([#454](https://github.com/open-feature/python-sdk/issues/454)) ([613388d](https://github.com/open-feature/python-sdk/commit/613388ddde33b6ce5ff3a39760970297dfa83255))
* **deps:** update github/codeql-action digest to fc7e4a0 ([#481](https://github.com/open-feature/python-sdk/issues/481)) ([1ae9fc2](https://github.com/open-feature/python-sdk/commit/1ae9fc2361f1671cee8c794f02c01eb6ca0b77a6))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.10.0 ([#463](https://github.com/open-feature/python-sdk/issues/463)) ([5fede4d](https://github.com/open-feature/python-sdk/commit/5fede4d4f0cb6e39f84e85c72c9a2dd13434bc78))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.0 ([#465](https://github.com/open-feature/python-sdk/issues/465)) ([d1eb3a0](https://github.com/open-feature/python-sdk/commit/d1eb3a08a8da75022788cc4b9ea7b7d95aec4e69))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.1 ([#468](https://github.com/open-feature/python-sdk/issues/468)) ([c07d3d6](https://github.com/open-feature/python-sdk/commit/c07d3d64677c2ce475b098580b5eba1dd7f95a2e))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.2 ([#469](https://github.com/open-feature/python-sdk/issues/469)) ([95e87c7](https://github.com/open-feature/python-sdk/commit/95e87c71fc835cde7f7528e974509438ab8f2dc3))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.3 ([#475](https://github.com/open-feature/python-sdk/issues/475)) ([2be2c06](https://github.com/open-feature/python-sdk/commit/2be2c06569d89309a70793bb14a82be91d2ccf20))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.4 ([#476](https://github.com/open-feature/python-sdk/issues/476)) ([8acc883](https://github.com/open-feature/python-sdk/commit/8acc88328836c70f168ca87b71f4c49a6dba9381))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.10 ([#461](https://github.com/open-feature/python-sdk/issues/461)) ([9057c6b](https://github.com/open-feature/python-sdk/commit/9057c6b3df6ca5dc9e429db231eb4427cce031ea))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.7 ([#453](https://github.com/open-feature/python-sdk/issues/453)) ([a5cb27b](https://github.com/open-feature/python-sdk/commit/a5cb27b67839d60ea631001759478b2e74b75f28))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.8 ([#457](https://github.com/open-feature/python-sdk/issues/457)) ([0c1a388](https://github.com/open-feature/python-sdk/commit/0c1a388ca121e232f5c36b4b7a550d541ae34e5b))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.9 ([#458](https://github.com/open-feature/python-sdk/issues/458)) ([9ce51eb](https://github.com/open-feature/python-sdk/commit/9ce51ebff5a896b818e241fd8e3c2dea2fee610c))
* **deps:** update spec digest to 09aef37 ([#460](https://github.com/open-feature/python-sdk/issues/460)) ([547781f](https://github.com/open-feature/python-sdk/commit/547781fbd82d1e2ee8a17988d11da3875d6a73dd))
* **deps:** update spec digest to 0cd553d ([#455](https://github.com/open-feature/python-sdk/issues/455)) ([fe99f08](https://github.com/open-feature/python-sdk/commit/fe99f08e9465e8d35dd2b187d8ac01eae98432b7))
* **deps:** update spec digest to 130df3e ([#471](https://github.com/open-feature/python-sdk/issues/471)) ([9ced6bf](https://github.com/open-feature/python-sdk/commit/9ced6bf2d1c7e3b0f01d062564ee63e49254af00))
* **deps:** update spec digest to 25c57ee ([#459](https://github.com/open-feature/python-sdk/issues/459)) ([40cbd82](https://github.com/open-feature/python-sdk/commit/40cbd82dda20604a7a7be00e6913710d4a1ab56f))
* **deps:** update spec digest to 27e4461 ([#472](https://github.com/open-feature/python-sdk/issues/472)) ([490cd06](https://github.com/open-feature/python-sdk/commit/490cd068533bb5ad702adf71915b6e0ac49706d8))
* **deps:** update spec digest to 54952f3 ([#447](https://github.com/open-feature/python-sdk/issues/447)) ([f907855](https://github.com/open-feature/python-sdk/commit/f907855966cf788a3522e7626c76bd050de59a7e))
* **deps:** update spec digest to a69f748 ([#452](https://github.com/open-feature/python-sdk/issues/452)) ([95b33b3](https://github.com/open-feature/python-sdk/commit/95b33b39e6ef472264002322162e83665054d71b))
* **deps:** update spec digest to aad6193 ([#464](https://github.com/open-feature/python-sdk/issues/464)) ([d15388b](https://github.com/open-feature/python-sdk/commit/d15388b542798f7703578927dc5013863a83efa1))
* improve resolve details callable type hints ([#449](https://github.com/open-feature/python-sdk/issues/449)) ([31afa64](https://github.com/open-feature/python-sdk/commit/31afa6490f7c2fc7a553b69c56840d494a520836))
* revert spec to commit 0cd553d ([#479](https://github.com/open-feature/python-sdk/issues/479)) ([0ebec53](https://github.com/open-feature/python-sdk/commit/0ebec538db4d1180bad05e89bb62db23ca606a27))
* use existing submodule version for e2e tests ([#444](https://github.com/open-feature/python-sdk/issues/444)) ([5ae8571](https://github.com/open-feature/python-sdk/commit/5ae8571ccd5f30c0aef87b0bc7f1a08a65254df0))
* use keyword arguments, validate test ([#446](https://github.com/open-feature/python-sdk/issues/446)) ([f29c450](https://github.com/open-feature/python-sdk/commit/f29c4506a6a13307ba95a9b450a1b19c328975b3))
### 📚 Documentation
* fix linting issue on the readme ([1198728](https://github.com/open-feature/python-sdk/commit/11987280ba53ba087b1792316acc920a81434630))
### 🔄 Refactoring
* replace exception raising with error flag resolution ([#474](https://github.com/open-feature/python-sdk/issues/474)) ([e61b69b](https://github.com/open-feature/python-sdk/commit/e61b69bb5079547c62a3ad51499326057db69e7a))
## [0.8.0](https://github.com/open-feature/python-sdk/compare/v0.7.5...v0.8.0) (2025-02-11)
### ⚠ BREAKING CHANGES
* drop Python 3.8 support ([#441](https://github.com/open-feature/python-sdk/issues/441))
### ✨ New Features
* Add async functionality to providers ([#413](https://github.com/open-feature/python-sdk/issues/413)) ([86e7c07](https://github.com/open-feature/python-sdk/commit/86e7c07112cfa9fa6bec15cb7a47f8a675034b8b))
### 🧹 Chore
* **deps:** pin dependencies ([#435](https://github.com/open-feature/python-sdk/issues/435)) ([38d13fa](https://github.com/open-feature/python-sdk/commit/38d13fa454e8b7d5a55a8e4e12dcbe4c37f70706))
* **deps:** update github/codeql-action digest to 9e8d078 ([#440](https://github.com/open-feature/python-sdk/issues/440)) ([ba0213e](https://github.com/open-feature/python-sdk/commit/ba0213e701958a9962676646bec267a5c530184c))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.5 ([#439](https://github.com/open-feature/python-sdk/issues/439)) ([75b41dd](https://github.com/open-feature/python-sdk/commit/75b41dd0202e9651801d2144ceec2c16ebe4989f))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.6 ([#443](https://github.com/open-feature/python-sdk/issues/443)) ([37296dc](https://github.com/open-feature/python-sdk/commit/37296dc0b5b7450815b3b63d7877968fe07f06be))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.15.0 ([#430](https://github.com/open-feature/python-sdk/issues/430)) ([fe0fea1](https://github.com/open-feature/python-sdk/commit/fe0fea1f7328bb97c6985628be2f5c11bae13f22))
* drop Python 3.8 support ([#441](https://github.com/open-feature/python-sdk/issues/441)) ([bcd1a38](https://github.com/open-feature/python-sdk/commit/bcd1a3807e635dcd80a7894859ae14d54a3dc485))
* fix renovate syntax issue ([e705af4](https://github.com/open-feature/python-sdk/commit/e705af47b1b44705f0f0cca1846ccb97e820f042))
* use centralized renovate config, downgrade release please ([#433](https://github.com/open-feature/python-sdk/issues/433)) ([ff52163](https://github.com/open-feature/python-sdk/commit/ff521630a1962f73a1d3f8e3fc65c8cdc691f54b))
### 📚 Documentation
* fix eval context link ([#438](https://github.com/open-feature/python-sdk/issues/438)) ([154d834](https://github.com/open-feature/python-sdk/commit/154d8345e7a65f3409c168a87d157df583fc8aa8))
* fix links to the openfeature ecosystem page ([#432](https://github.com/open-feature/python-sdk/issues/432)) ([49edce2](https://github.com/open-feature/python-sdk/commit/49edce226996d7d27a6dd64a1ae45e0def9e9b29))
## [0.7.5](https://github.com/open-feature/python-sdk/compare/v0.7.4...v0.7.5) (2025-01-31)
### ⚠ BREAKING CHANGES
The signature of the `finally_after` hook stage has been changed. The signature now includes the `evaluation details`, as per the [OpenFeature specification](https://openfeature.dev/specification/sections/hooks#requirement-438). To migrate, update any hook that implements the `finally_after` stage to accept `evaluation details` as the second argument.
* Add evaluation details to finally hook stage [#403](https://github.com/open-feature/python-sdk/issues/403) ([#423](https://github.com/open-feature/python-sdk/issues/423)) ([9e9bb5c](https://github.com/open-feature/python-sdk/commit/9e9bb5c6269cfa5d9c9ffc7141c6dc63e399cdca))
### 🐛 Bug Fixes
* Finally hooks do not get called when the provider is not ready [#424](https://github.com/open-feature/python-sdk/issues/424) ([#425](https://github.com/open-feature/python-sdk/issues/425)) ([8f2caba](https://github.com/open-feature/python-sdk/commit/8f2cabaa32f595304ecd6964b6ae21909672ef4a))
### ✨ New Features
* Add evaluation details to finally hook stage [#403](https://github.com/open-feature/python-sdk/issues/403) ([#423](https://github.com/open-feature/python-sdk/issues/423)) ([9e9bb5c](https://github.com/open-feature/python-sdk/commit/9e9bb5c6269cfa5d9c9ffc7141c6dc63e399cdca))
* Update test harness (add assertions) [#1467](https://github.com/open-feature/python-sdk/issues/1467) ([#415](https://github.com/open-feature/python-sdk/issues/415)) ([f559d1b](https://github.com/open-feature/python-sdk/commit/f559d1b27a096c585bd81f32f6472039c9ce5e03))
* Update test harness (copy test files) [#1467](https://github.com/open-feature/python-sdk/issues/1467) ([#416](https://github.com/open-feature/python-sdk/issues/416)) ([192f7c4](https://github.com/open-feature/python-sdk/commit/192f7c40bd07616030e86ff2aba7e993390d4af4))
### 🧹 Chore
* **config:** migrate config renovate.json ([26bc964](https://github.com/open-feature/python-sdk/commit/26bc9642270f9371170ce8ab2d9b938a0fca187e))
* **config:** migrate renovate config ([#408](https://github.com/open-feature/python-sdk/issues/408)) ([26bc964](https://github.com/open-feature/python-sdk/commit/26bc9642270f9371170ce8ab2d9b938a0fca187e))
* **deps:** update actions/setup-python digest to 4237552 ([#422](https://github.com/open-feature/python-sdk/issues/422)) ([9c2ed71](https://github.com/open-feature/python-sdk/commit/9c2ed71c6efdc2ece9ae89cf91f3b019c44b0033))
* **deps:** update codecov/codecov-action action to v5.1.0 ([#401](https://github.com/open-feature/python-sdk/issues/401)) ([0459330](https://github.com/open-feature/python-sdk/commit/0459330cb91e9b28a15bdd380aec4c56c3b5d8df))
* **deps:** update codecov/codecov-action action to v5.1.1 ([#402](https://github.com/open-feature/python-sdk/issues/402)) ([a6907d6](https://github.com/open-feature/python-sdk/commit/a6907d610e6dde1eecef56f25f3cc6a569b6eee4))
* **deps:** update codecov/codecov-action action to v5.1.2 ([#405](https://github.com/open-feature/python-sdk/issues/405)) ([1c56480](https://github.com/open-feature/python-sdk/commit/1c564804afad474151489d695af7fa0409a768c6))
* **deps:** update codecov/codecov-action action to v5.2.0 ([#418](https://github.com/open-feature/python-sdk/issues/418)) ([b69e81a](https://github.com/open-feature/python-sdk/commit/b69e81a63676240ef2abb98e96d2954a50f0c20a))
* **deps:** update codecov/codecov-action action to v5.3.0 ([#420](https://github.com/open-feature/python-sdk/issues/420)) ([6af37b1](https://github.com/open-feature/python-sdk/commit/6af37b1c2bc2e10161673af5726932129ed02506))
* **deps:** update codecov/codecov-action action to v5.3.1 ([#421](https://github.com/open-feature/python-sdk/issues/421)) ([e99e481](https://github.com/open-feature/python-sdk/commit/e99e481524ccfbea5bc7554d531f9c883dce6b5f))
* **deps:** update googleapis/release-please-action action to v4 ([#428](https://github.com/open-feature/python-sdk/issues/428)) ([99905d5](https://github.com/open-feature/python-sdk/commit/99905d57f8b21a640f8f64f177679eee127ebba6))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.1 ([#398](https://github.com/open-feature/python-sdk/issues/398)) ([043385a](https://github.com/open-feature/python-sdk/commit/043385a8369e253a5e0ad1e184e980f8e8d7e5c7))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.2 ([#400](https://github.com/open-feature/python-sdk/issues/400)) ([2b6e210](https://github.com/open-feature/python-sdk/commit/2b6e210bc9dda72335e646fc60cde79b5bdd76c1))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.3 ([#404](https://github.com/open-feature/python-sdk/issues/404)) ([01ec388](https://github.com/open-feature/python-sdk/commit/01ec388d2d93fc87a4f2eca856cf507cbee35785))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.4 ([#406](https://github.com/open-feature/python-sdk/issues/406)) ([3296d3b](https://github.com/open-feature/python-sdk/commit/3296d3b229d41c7f879adfb7ab36e19de36617e4))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.6 ([#410](https://github.com/open-feature/python-sdk/issues/410)) ([7f9d422](https://github.com/open-feature/python-sdk/commit/7f9d422497a6d8392eee18ccfd12050eef7a2338))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.0 ([#411](https://github.com/open-feature/python-sdk/issues/411)) ([bc6e333](https://github.com/open-feature/python-sdk/commit/bc6e3332157788ec3f8037f68b9087e26a4634b5))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.1 ([#412](https://github.com/open-feature/python-sdk/issues/412)) ([cbace6a](https://github.com/open-feature/python-sdk/commit/cbace6a24c3fa091242aedd4f7c2e8de4332e463))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.2 ([#414](https://github.com/open-feature/python-sdk/issues/414)) ([9304292](https://github.com/open-feature/python-sdk/commit/9304292ea91580cf8cbfee7dbde922caa818189d))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.3 ([#419](https://github.com/open-feature/python-sdk/issues/419)) ([8f9cc7c](https://github.com/open-feature/python-sdk/commit/8f9cc7ca96a1210bab104e6342b4f7d1553bad3b))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.4 ([#426](https://github.com/open-feature/python-sdk/issues/426)) ([f726706](https://github.com/open-feature/python-sdk/commit/f72670689d25b82c8d54cacde3b1af179e0bc7e6))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.14.0 ([#407](https://github.com/open-feature/python-sdk/issues/407)) ([89d6997](https://github.com/open-feature/python-sdk/commit/89d6997b1fe04df82e0873de7e7a1ea1aca2b071))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.14.1 ([#409](https://github.com/open-feature/python-sdk/issues/409)) ([2c1840c](https://github.com/open-feature/python-sdk/commit/2c1840c87d00177d87b93135a38a7df98a9f6c0b))
## [0.7.4](https://github.com/open-feature/python-sdk/compare/v0.7.3...v0.7.4) (2024-11-25) ## [0.7.4](https://github.com/open-feature/python-sdk/compare/v0.7.3...v0.7.4) (2024-11-25)

View File

@ -1 +0,0 @@
* @open-feature/sdk-python-maintainers @open-feature/maintainers

View File

@ -4,59 +4,27 @@
### System Requirements ### System Requirements
Python 3.9 and above are required. Python 3.8 and above are required.
### Target version(s) ### Target version(s)
Python 3.9 and above are supported by the SDK. Python 3.8 and above are supported by the SDK.
### Installation and Dependencies ### Installation and Dependencies
We use [uv](https://github.com/astral-sh/uv) for fast Python package management and dependency resolution. We use [Hatch](https://hatch.pypa.io/) to manage the project.
To install uv, follow the [installation guide](https://docs.astral.sh/uv/getting-started/installation/). To install Hatch, just run `pip install hatch`.
### Setup Development Environment You will also need to set up the `pre-commit` hooks.
Run `pre-commit install` in the root directory of the repository.
1. **Clone the repository:** If you don't have `pre-commit` installed, you can install it with `pip install pre-commit`.
```bash
git clone https://github.com/open-feature/python-sdk.git
cd python-sdk
```
2. **Install dependencies:**
```bash
uv sync --frozen
```
### Testing ### Testing
Run tests: Run tests with `hatch run test`.
```bash
uv run test --frozen
```
### Coverage
Run tests with a coverage report:
```bash
uv run cov --frozen
```
### End-to-End Tests
Run e2e tests with behave:
```bash
uv run e2e --frozen
```
### Pre-commit
Run pre-commit hooks
```bash
uv run precommit --frozen
```
We use `pytest` for our unit testing, making use of `parametrized` to inject cases at scale.
### Integration tests ### Integration tests
@ -91,7 +59,7 @@ git remote add fork https://github.com/YOUR_GITHUB_USERNAME/python-sdk.git
Ensure your development environment is all set up by building and testing Ensure your development environment is all set up by building and testing
```bash ```bash
uv run test --frozen hatch run test
``` ```
To start working on a new feature or bugfix, create a new branch and start working on it. To start working on a new feature or bugfix, create a new branch and start working on it.

View File

@ -19,8 +19,8 @@
<!-- x-release-please-start-version --> <!-- x-release-please-start-version -->
<a href="https://github.com/open-feature/python-sdk/releases/tag/v0.8.2"> <a href="https://github.com/open-feature/python-sdk/releases/tag/v0.7.4">
<img alt="Latest version" src="https://img.shields.io/static/v1?label=release&message=v0.8.2&color=blue&style=for-the-badge" /> <img alt="Latest version" src="https://img.shields.io/static/v1?label=release&message=v0.7.4&color=blue&style=for-the-badge" />
</a> </a>
<!-- x-release-please-end --> <!-- x-release-please-end -->
@ -34,7 +34,7 @@
</a> </a>
<a href="https://www.python.org/downloads/"> <a href="https://www.python.org/downloads/">
<img alt="Min python version" src="https://img.shields.io/badge/python->=3.9-blue.svg" /> <img alt="Min python version" src="https://img.shields.io/badge/python->=3.8-blue.svg" />
</a> </a>
<a href="https://www.repostatus.org/#wip"> <a href="https://www.repostatus.org/#wip">
@ -51,7 +51,7 @@
### Requirements ### Requirements
- Python 3.9+ - Python 3.8+
### Install ### Install
@ -60,13 +60,13 @@
#### Pip install #### Pip install
```bash ```bash
pip install openfeature-sdk==0.8.2 pip install openfeature-sdk==0.7.4
``` ```
#### requirements.txt #### requirements.txt
```bash ```bash
openfeature-sdk==0.8.2 openfeature-sdk==0.7.4
``` ```
```python ```python
@ -108,7 +108,7 @@ print("Value: " + str(flag_value))
| ✅ | [Domains](#domains) | Logically bind clients with providers. | | ✅ | [Domains](#domains) | Logically bind clients with providers. |
| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | | ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | | ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
| ✅ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread) | | ✅ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread) |
| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | | ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub> <sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>
@ -116,7 +116,7 @@ print("Value: " + str(flag_value))
### Providers ### Providers
[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. [Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK.
Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Python) for a complete list of available providers. Look [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=python) for a complete list of available providers.
If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself.
Once you've added a provider as a dependency, it can be registered with OpenFeature like this: Once you've added a provider as a dependency, it can be registered with OpenFeature like this:
@ -165,7 +165,7 @@ client.get_string_value("email", "fallback", request_context)
### Hooks ### Hooks
[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. [Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle.
Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Python) for a complete list of available hooks. Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=python) for a complete list of available hooks.
If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself.
Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level.
@ -316,25 +316,6 @@ async def some_endpoint():
return create_response() return create_response()
``` ```
### Asynchronous Feature Retrieval
The OpenFeature API supports asynchronous calls, enabling non-blocking feature evaluations for improved performance, especially useful in concurrent or latency-sensitive scenarios. If a provider *hasn't* implemented asynchronous calls, the client can still be used asynchronously, but calls will be blocking (synchronous).
```python
import asyncio
from openfeature import api
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
my_flags = { "v2_enabled": InMemoryFlag("on", {"on": True, "off": False}) }
api.set_provider(InMemoryProvider(my_flags))
client = api.get_client()
flag_value = await client.get_boolean_value_async("v2_enabled", False) # API calls are suffixed by _async
print("Value: " + str(flag_value))
```
See the [develop a provider](#develop-a-provider) for how to support asynchronous functionality in providers.
### Shutdown ### Shutdown
The OpenFeature API provides a shutdown function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. The OpenFeature API provides a shutdown function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down.
@ -409,57 +390,6 @@ class MyProvider(AbstractProvider):
... ...
``` ```
Providers can also be extended to support async functionality.
To support add asynchronous calls to a provider:
- Implement the `AbstractProvider` as shown above.
- Define asynchronous calls for each data type.
```python
class MyProvider(AbstractProvider):
...
async def resolve_boolean_details_async(
self,
flag_key: str,
default_value: bool,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
...
async def resolve_string_details_async(
self,
flag_key: str,
default_value: str,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]:
...
async def resolve_integer_details_async(
self,
flag_key: str,
default_value: int,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]:
...
async def resolve_float_details_async(
self,
flag_key: str,
default_value: float,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]:
...
async def resolve_object_details_async(
self,
flag_key: str,
default_value: Union[dict, list],
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[Union[dict, list]]:
...
```
> Built a new provider? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&projects=&template=document-provider.yaml&title=%5BProvider%5D%3A+) so we can add it to the docs! > Built a new provider? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&projects=&template=document-provider.yaml&title=%5BProvider%5D%3A+) so we can add it to the docs!
### Develop a hook ### Develop a hook

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import threading import threading
from collections import defaultdict from collections import defaultdict
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, List
from openfeature.event import ( from openfeature.event import (
EventDetails, EventDetails,
@ -17,10 +17,10 @@ if TYPE_CHECKING:
_global_lock = threading.RLock() _global_lock = threading.RLock()
_global_handlers: dict[ProviderEvent, list[EventHandler]] = defaultdict(list) _global_handlers: Dict[ProviderEvent, List[EventHandler]] = defaultdict(list)
_client_lock = threading.RLock() _client_lock = threading.RLock()
_client_handlers: dict[OpenFeatureClient, dict[ProviderEvent, list[EventHandler]]] = ( _client_handlers: Dict[OpenFeatureClient, Dict[ProviderEvent, List[EventHandler]]] = (
defaultdict(lambda: defaultdict(list)) defaultdict(lambda: defaultdict(list))
) )
@ -61,7 +61,7 @@ def add_global_handler(event: ProviderEvent, handler: EventHandler) -> None:
with _global_lock: with _global_lock:
_global_handlers[event].append(handler) _global_handlers[event].append(handler)
from openfeature.api import get_client # noqa: PLC0415 from openfeature.api import get_client
_run_immediate_handler(get_client(), event, handler) _run_immediate_handler(get_client(), event, handler)

View File

@ -2,22 +2,19 @@ import typing
from openfeature import _event_support from openfeature import _event_support
from openfeature.client import OpenFeatureClient from openfeature.client import OpenFeatureClient
from openfeature.evaluation_context import ( from openfeature.evaluation_context import EvaluationContext
get_evaluation_context,
set_evaluation_context,
)
from openfeature.event import ( from openfeature.event import (
EventHandler, EventHandler,
ProviderEvent, ProviderEvent,
) )
from openfeature.hook import add_hooks, clear_hooks, get_hooks from openfeature.exception import GeneralError
from openfeature.hook import Hook
from openfeature.provider import FeatureProvider from openfeature.provider import FeatureProvider
from openfeature.provider._registry import provider_registry from openfeature.provider._registry import provider_registry
from openfeature.provider.metadata import Metadata from openfeature.provider.metadata import Metadata
from openfeature.transaction_context import ( from openfeature.transaction_context import TransactionContextPropagator
get_transaction_context, from openfeature.transaction_context.no_op_transaction_context_propagator import (
set_transaction_context, NoOpTransactionContextPropagator,
set_transaction_context_propagator,
) )
__all__ = [ __all__ = [
@ -38,6 +35,13 @@ __all__ = [
"shutdown", "shutdown",
] ]
_evaluation_context = EvaluationContext()
_evaluation_transaction_context_propagator: TransactionContextPropagator = (
NoOpTransactionContextPropagator()
)
_hooks: typing.List[Hook] = []
def get_client( def get_client(
domain: typing.Optional[str] = None, version: typing.Optional[str] = None domain: typing.Optional[str] = None, version: typing.Optional[str] = None
@ -63,6 +67,49 @@ def get_provider_metadata(domain: typing.Optional[str] = None) -> Metadata:
return provider_registry.get_provider(domain).get_metadata() return provider_registry.get_provider(domain).get_metadata()
def get_evaluation_context() -> EvaluationContext:
return _evaluation_context
def set_evaluation_context(evaluation_context: EvaluationContext) -> None:
global _evaluation_context
if evaluation_context is None:
raise GeneralError(error_message="No api level evaluation context")
_evaluation_context = evaluation_context
def set_transaction_context_propagator(
transaction_context_propagator: TransactionContextPropagator,
) -> None:
global _evaluation_transaction_context_propagator
_evaluation_transaction_context_propagator = transaction_context_propagator
def get_transaction_context() -> EvaluationContext:
return _evaluation_transaction_context_propagator.get_transaction_context()
def set_transaction_context(evaluation_context: EvaluationContext) -> None:
global _evaluation_transaction_context_propagator
_evaluation_transaction_context_propagator.set_transaction_context(
evaluation_context
)
def add_hooks(hooks: typing.List[Hook]) -> None:
global _hooks
_hooks = _hooks + hooks
def clear_hooks() -> None:
global _hooks
_hooks = []
def get_hooks() -> typing.List[Hook]:
return _hooks
def shutdown() -> None: def shutdown() -> None:
provider_registry.shutdown() provider_registry.shutdown()

View File

@ -1,10 +1,9 @@
import logging import logging
import typing import typing
from collections.abc import Awaitable, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from openfeature import _event_support from openfeature import _event_support, api
from openfeature.evaluation_context import EvaluationContext, get_evaluation_context from openfeature.evaluation_context import EvaluationContext
from openfeature.event import EventHandler, ProviderEvent from openfeature.event import EventHandler, ProviderEvent
from openfeature.exception import ( from openfeature.exception import (
ErrorCode, ErrorCode,
@ -19,10 +18,9 @@ from openfeature.flag_evaluation import (
FlagEvaluationOptions, FlagEvaluationOptions,
FlagResolutionDetails, FlagResolutionDetails,
FlagType, FlagType,
FlagValueType,
Reason, Reason,
) )
from openfeature.hook import Hook, HookContext, HookHints, get_hooks from openfeature.hook import Hook, HookContext
from openfeature.hook._hook_support import ( from openfeature.hook._hook_support import (
after_all_hooks, after_all_hooks,
after_hooks, after_hooks,
@ -31,7 +29,6 @@ from openfeature.hook._hook_support import (
) )
from openfeature.provider import FeatureProvider, ProviderStatus from openfeature.provider import FeatureProvider, ProviderStatus
from openfeature.provider._registry import provider_registry from openfeature.provider._registry import provider_registry
from openfeature.transaction_context import get_transaction_context
__all__ = [ __all__ = [
"ClientMetadata", "ClientMetadata",
@ -40,36 +37,34 @@ __all__ = [
logger = logging.getLogger("openfeature") logger = logging.getLogger("openfeature")
TypeMap = dict[ GetDetailCallable = typing.Union[
FlagType, typing.Callable[
typing.Union[ [str, bool, typing.Optional[EvaluationContext]], FlagResolutionDetails[bool]
type[bool], ],
type[int], typing.Callable[
type[float], [str, int, typing.Optional[EvaluationContext]], FlagResolutionDetails[int]
type[str], ],
tuple[type[dict], type[list]], typing.Callable[
[str, float, typing.Optional[EvaluationContext]], FlagResolutionDetails[float]
],
typing.Callable[
[str, str, typing.Optional[EvaluationContext]], FlagResolutionDetails[str]
],
typing.Callable[
[str, typing.Union[dict, list], typing.Optional[EvaluationContext]],
FlagResolutionDetails[typing.Union[dict, list]],
],
]
TypeMap = typing.Dict[
FlagType,
typing.Union[
typing.Type[bool],
typing.Type[int],
typing.Type[float],
typing.Type[str],
typing.Tuple[typing.Type[dict], typing.Type[list]],
], ],
] ]
T = typing.TypeVar("T", bool, int, float, str, typing.Union[dict, list])
class ResolveDetailsCallable(typing.Protocol[T]):
def __call__(
self,
flag_key: str,
default_value: T,
evaluation_context: typing.Optional[EvaluationContext],
) -> FlagResolutionDetails[T]: ...
class ResolveDetailsCallableAsync(typing.Protocol[T]):
def __call__(
self,
flag_key: str,
default_value: T,
evaluation_context: typing.Optional[EvaluationContext],
) -> Awaitable[FlagResolutionDetails[T]]: ...
@dataclass @dataclass
@ -84,7 +79,7 @@ class OpenFeatureClient:
domain: typing.Optional[str], domain: typing.Optional[str],
version: typing.Optional[str], version: typing.Optional[str],
context: typing.Optional[EvaluationContext] = None, context: typing.Optional[EvaluationContext] = None,
hooks: typing.Optional[list[Hook]] = None, hooks: typing.Optional[typing.List[Hook]] = None,
) -> None: ) -> None:
self.domain = domain self.domain = domain
self.version = version self.version = version
@ -101,7 +96,7 @@ class OpenFeatureClient:
def get_metadata(self) -> ClientMetadata: def get_metadata(self) -> ClientMetadata:
return ClientMetadata(domain=self.domain) return ClientMetadata(domain=self.domain)
def add_hooks(self, hooks: list[Hook]) -> None: def add_hooks(self, hooks: typing.List[Hook]) -> None:
self.hooks = self.hooks + hooks self.hooks = self.hooks + hooks
def get_boolean_value( def get_boolean_value(
@ -118,21 +113,6 @@ class OpenFeatureClient:
flag_evaluation_options, flag_evaluation_options,
).value ).value
async def get_boolean_value_async(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> bool:
details = await self.get_boolean_details_async(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
return details.value
def get_boolean_details( def get_boolean_details(
self, self,
flag_key: str, flag_key: str,
@ -148,21 +128,6 @@ class OpenFeatureClient:
flag_evaluation_options, flag_evaluation_options,
) )
async def get_boolean_details_async(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[bool]:
return await self.evaluate_flag_details_async(
FlagType.BOOLEAN,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
def get_string_value( def get_string_value(
self, self,
flag_key: str, flag_key: str,
@ -177,21 +142,6 @@ class OpenFeatureClient:
flag_evaluation_options, flag_evaluation_options,
).value ).value
async def get_string_value_async(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> str:
details = await self.get_string_details_async(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
return details.value
def get_string_details( def get_string_details(
self, self,
flag_key: str, flag_key: str,
@ -207,21 +157,6 @@ class OpenFeatureClient:
flag_evaluation_options, flag_evaluation_options,
) )
async def get_string_details_async(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[str]:
return await self.evaluate_flag_details_async(
FlagType.STRING,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
def get_integer_value( def get_integer_value(
self, self,
flag_key: str, flag_key: str,
@ -236,21 +171,6 @@ class OpenFeatureClient:
flag_evaluation_options, flag_evaluation_options,
).value ).value
async def get_integer_value_async(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> int:
details = await self.get_integer_details_async(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
return details.value
def get_integer_details( def get_integer_details(
self, self,
flag_key: str, flag_key: str,
@ -266,21 +186,6 @@ class OpenFeatureClient:
flag_evaluation_options, flag_evaluation_options,
) )
async def get_integer_details_async(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[int]:
return await self.evaluate_flag_details_async(
FlagType.INTEGER,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
def get_float_value( def get_float_value(
self, self,
flag_key: str, flag_key: str,
@ -295,21 +200,6 @@ class OpenFeatureClient:
flag_evaluation_options, flag_evaluation_options,
).value ).value
async def get_float_value_async(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> float:
details = await self.get_float_details_async(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
return details.value
def get_float_details( def get_float_details(
self, self,
flag_key: str, flag_key: str,
@ -325,30 +215,13 @@ class OpenFeatureClient:
flag_evaluation_options, flag_evaluation_options,
) )
async def get_float_details_async(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[float]:
return await self.evaluate_flag_details_async(
FlagType.FLOAT,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
def get_object_value( def get_object_value(
self, self,
flag_key: str, flag_key: str,
default_value: typing.Union[ default_value: typing.Union[dict, list],
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]: ) -> typing.Union[dict, list]:
return self.get_object_details( return self.get_object_details(
flag_key, flag_key,
default_value, default_value,
@ -356,34 +229,13 @@ class OpenFeatureClient:
flag_evaluation_options, flag_evaluation_options,
).value ).value
async def get_object_value_async(
self,
flag_key: str,
default_value: typing.Union[
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]:
details = await self.get_object_details_async(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
return details.value
def get_object_details( def get_object_details(
self, self,
flag_key: str, flag_key: str,
default_value: typing.Union[ default_value: typing.Union[dict, list],
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[ ) -> FlagEvaluationDetails[typing.Union[dict, list]]:
typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]
]:
return self.evaluate_flag_details( return self.evaluate_flag_details(
FlagType.OBJECT, FlagType.OBJECT,
flag_key, flag_key,
@ -392,39 +244,26 @@ class OpenFeatureClient:
flag_evaluation_options, flag_evaluation_options,
) )
async def get_object_details_async( def evaluate_flag_details( # noqa: PLR0915
self,
flag_key: str,
default_value: typing.Union[
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[
typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]
]:
return await self.evaluate_flag_details_async(
FlagType.OBJECT,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
def _establish_hooks_and_provider(
self, self,
flag_type: FlagType, flag_type: FlagType,
flag_key: str, flag_key: str,
default_value: FlagValueType, default_value: typing.Any,
evaluation_context: typing.Optional[EvaluationContext], evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions], flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> tuple[ ) -> FlagEvaluationDetails[typing.Any]:
FeatureProvider, """
HookContext, Evaluate the flag requested by the user from the clients provider.
HookHints,
list[Hook], :param flag_type: the type of the flag being returned
list[Hook], :param flag_key: the string key of the selected flag
]: :param default_value: backup value returned if no result found by the provider
:param evaluation_context: Information for the purposes of flag evaluation
:param flag_evaluation_options: Additional flag evaluation information
:return: a FlagEvaluationDetails object with the fully evaluated flag from a
provider
"""
if evaluation_context is None: if evaluation_context is None:
evaluation_context = EvaluationContext() evaluation_context = EvaluationContext()
@ -435,20 +274,11 @@ class OpenFeatureClient:
evaluation_hooks = flag_evaluation_options.hooks evaluation_hooks = flag_evaluation_options.hooks
hook_hints = flag_evaluation_options.hook_hints hook_hints = flag_evaluation_options.hook_hints
# Merge transaction context into evaluation context before creating hook_context
# This ensures hooks have access to the complete context including transaction context
merged_eval_context = (
get_evaluation_context()
.merge(get_transaction_context())
.merge(self.context)
.merge(evaluation_context)
)
hook_context = HookContext( hook_context = HookContext(
flag_key=flag_key, flag_key=flag_key,
flag_type=flag_type, flag_type=flag_type,
default_value=default_value, default_value=default_value,
evaluation_context=merged_eval_context, evaluation_context=evaluation_context,
client_metadata=self.get_metadata(), client_metadata=self.get_metadata(),
provider_metadata=provider.get_metadata(), provider_metadata=provider.get_metadata(),
) )
@ -456,335 +286,61 @@ class OpenFeatureClient:
# in the flag evaluation # in the flag evaluation
# before: API, Client, Invocation, Provider # before: API, Client, Invocation, Provider
merged_hooks = ( merged_hooks = (
get_hooks() + self.hooks + evaluation_hooks + provider.get_provider_hooks() api.get_hooks()
+ self.hooks
+ evaluation_hooks
+ provider.get_provider_hooks()
) )
# after, error, finally: Provider, Invocation, Client, API # after, error, finally: Provider, Invocation, Client, API
reversed_merged_hooks = merged_hooks[:] reversed_merged_hooks = merged_hooks[:]
reversed_merged_hooks.reverse() reversed_merged_hooks.reverse()
return provider, hook_context, hook_hints, merged_hooks, reversed_merged_hooks
def _assert_provider_status(
self,
) -> typing.Optional[OpenFeatureError]:
status = self.get_provider_status() status = self.get_provider_status()
if status == ProviderStatus.NOT_READY: if status == ProviderStatus.NOT_READY:
return ProviderNotReadyError() error_hooks(
flag_type,
hook_context,
ProviderNotReadyError(),
reversed_merged_hooks,
hook_hints,
)
return FlagEvaluationDetails(
flag_key=flag_key,
value=default_value,
reason=Reason.ERROR,
error_code=ErrorCode.PROVIDER_NOT_READY,
)
if status == ProviderStatus.FATAL: if status == ProviderStatus.FATAL:
return ProviderFatalError() error_hooks(
return None
def _run_before_hooks_and_update_context(
self,
flag_type: FlagType,
hook_context: HookContext,
merged_hooks: list[Hook],
hook_hints: HookHints,
evaluation_context: typing.Optional[EvaluationContext],
) -> EvaluationContext:
# https://github.com/open-feature/spec/blob/main/specification/sections/03-evaluation-context.md
# Any resulting evaluation context from a before hook will overwrite
# duplicate fields defined globally, on the client, or in the invocation.
# Requirement 3.2.2, 4.3.4: API.context->client.context->invocation.context
before_hooks_context = before_hooks(
flag_type, hook_context, merged_hooks, hook_hints
)
# The hook_context.evaluation_context already contains the merged context from
# _establish_hooks_and_provider, so we just need to merge with the before hooks result
merged_context = hook_context.evaluation_context.merge(before_hooks_context)
return merged_context
@typing.overload
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[bool]: ...
@typing.overload
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[int]: ...
@typing.overload
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[float]: ...
@typing.overload
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[str]: ...
@typing.overload
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: Sequence["FlagValueType"],
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[Sequence["FlagValueType"]]: ...
@typing.overload
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: typing.Mapping[str, "FlagValueType"],
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[typing.Mapping[str, "FlagValueType"]]: ...
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: FlagValueType,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[FlagValueType]:
"""
Evaluate the flag requested by the user from the clients provider.
:param flag_type: the type of the flag being returned
:param flag_key: the string key of the selected flag
:param default_value: backup value returned if no result found by the provider
:param evaluation_context: Information for the purposes of flag evaluation
:param flag_evaluation_options: Additional flag evaluation information
:return: a typing.Awaitable[FlagEvaluationDetails] object with the fully evaluated flag from a
provider
"""
provider, hook_context, hook_hints, merged_hooks, reversed_merged_hooks = (
self._establish_hooks_and_provider(
flag_type,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
)
try:
if provider_err := self._assert_provider_status():
error_hooks(
flag_type,
hook_context,
provider_err,
reversed_merged_hooks,
hook_hints,
)
flag_evaluation = FlagEvaluationDetails(
flag_key=flag_key,
value=default_value,
reason=Reason.ERROR,
error_code=provider_err.error_code,
error_message=provider_err.error_message,
)
return flag_evaluation
merged_context = self._run_before_hooks_and_update_context(
flag_type, flag_type,
hook_context, hook_context,
merged_hooks, ProviderFatalError(),
hook_hints,
evaluation_context,
)
flag_evaluation = await self._create_provider_evaluation_async(
provider,
flag_type,
flag_key,
default_value,
merged_context,
)
if err := flag_evaluation.get_exception():
error_hooks(
flag_type, hook_context, err, reversed_merged_hooks, hook_hints
)
return flag_evaluation
after_hooks(
flag_type,
hook_context,
flag_evaluation,
reversed_merged_hooks, reversed_merged_hooks,
hook_hints, hook_hints,
) )
return FlagEvaluationDetails(
return flag_evaluation
except OpenFeatureError as err:
error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints)
flag_evaluation = FlagEvaluationDetails(
flag_key=flag_key, flag_key=flag_key,
value=default_value, value=default_value,
reason=Reason.ERROR, reason=Reason.ERROR,
error_code=err.error_code, error_code=ErrorCode.PROVIDER_FATAL,
error_message=err.error_message,
) )
return flag_evaluation
# Catch any type of exception here since the user can provide any exception
# in the error hooks
except Exception as err: # pragma: no cover
logger.exception(
"Unable to correctly evaluate flag with key: '%s'", flag_key
)
error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints)
error_message = getattr(err, "error_message", str(err))
flag_evaluation = FlagEvaluationDetails(
flag_key=flag_key,
value=default_value,
reason=Reason.ERROR,
error_code=ErrorCode.GENERAL,
error_message=error_message,
)
return flag_evaluation
finally:
after_all_hooks(
flag_type,
hook_context,
flag_evaluation,
reversed_merged_hooks,
hook_hints,
)
@typing.overload
def evaluate_flag_details(
self,
flag_type: FlagType,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[bool]: ...
@typing.overload
def evaluate_flag_details(
self,
flag_type: FlagType,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[int]: ...
@typing.overload
def evaluate_flag_details(
self,
flag_type: FlagType,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[float]: ...
@typing.overload
def evaluate_flag_details(
self,
flag_type: FlagType,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[str]: ...
@typing.overload
def evaluate_flag_details(
self,
flag_type: FlagType,
flag_key: str,
default_value: Sequence["FlagValueType"],
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[Sequence["FlagValueType"]]: ...
@typing.overload
def evaluate_flag_details(
self,
flag_type: FlagType,
flag_key: str,
default_value: typing.Mapping[str, "FlagValueType"],
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[typing.Mapping[str, "FlagValueType"]]: ...
def evaluate_flag_details(
self,
flag_type: FlagType,
flag_key: str,
default_value: FlagValueType,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[FlagValueType]:
"""
Evaluate the flag requested by the user from the clients provider.
:param flag_type: the type of the flag being returned
:param flag_key: the string key of the selected flag
:param default_value: backup value returned if no result found by the provider
:param evaluation_context: Information for the purposes of flag evaluation
:param flag_evaluation_options: Additional flag evaluation information
:return: a FlagEvaluationDetails object with the fully evaluated flag from a
provider
"""
provider, hook_context, hook_hints, merged_hooks, reversed_merged_hooks = (
self._establish_hooks_and_provider(
flag_type,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
)
try: try:
if provider_err := self._assert_provider_status(): # https://github.com/open-feature/spec/blob/main/specification/sections/03-evaluation-context.md
error_hooks( # Any resulting evaluation context from a before hook will overwrite
flag_type, # duplicate fields defined globally, on the client, or in the invocation.
hook_context, # Requirement 3.2.2, 4.3.4: API.context->client.context->invocation.context
provider_err, invocation_context = before_hooks(
reversed_merged_hooks, flag_type, hook_context, merged_hooks, hook_hints
hook_hints, )
) invocation_context = invocation_context.merge(ctx2=evaluation_context)
flag_evaluation = FlagEvaluationDetails(
flag_key=flag_key,
value=default_value,
reason=Reason.ERROR,
error_code=provider_err.error_code,
error_message=provider_err.error_message,
)
return flag_evaluation
merged_context = self._run_before_hooks_and_update_context( # Requirement 3.2.2 merge: API.context->transaction.context->client.context->invocation.context
flag_type, merged_context = (
hook_context, api.get_evaluation_context()
merged_hooks, .merge(api.get_transaction_context())
hook_hints, .merge(self.context)
evaluation_context, .merge(invocation_context)
) )
flag_evaluation = self._create_provider_evaluation( flag_evaluation = self._create_provider_evaluation(
@ -794,12 +350,6 @@ class OpenFeatureClient:
default_value, default_value,
merged_context, merged_context,
) )
if err := flag_evaluation.get_exception():
error_hooks(
flag_type, hook_context, err, reversed_merged_hooks, hook_hints
)
flag_evaluation.value = default_value
return flag_evaluation
after_hooks( after_hooks(
flag_type, flag_type,
@ -814,14 +364,13 @@ class OpenFeatureClient:
except OpenFeatureError as err: except OpenFeatureError as err:
error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints) error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints)
flag_evaluation = FlagEvaluationDetails( return FlagEvaluationDetails(
flag_key=flag_key, flag_key=flag_key,
value=default_value, value=default_value,
reason=Reason.ERROR, reason=Reason.ERROR,
error_code=err.error_code, error_code=err.error_code,
error_message=err.error_message, error_message=err.error_message,
) )
return flag_evaluation
# Catch any type of exception here since the user can provide any exception # Catch any type of exception here since the user can provide any exception
# in the error hooks # in the error hooks
except Exception as err: # pragma: no cover except Exception as err: # pragma: no cover
@ -832,79 +381,25 @@ class OpenFeatureClient:
error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints) error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints)
error_message = getattr(err, "error_message", str(err)) error_message = getattr(err, "error_message", str(err))
flag_evaluation = FlagEvaluationDetails( return FlagEvaluationDetails(
flag_key=flag_key, flag_key=flag_key,
value=default_value, value=default_value,
reason=Reason.ERROR, reason=Reason.ERROR,
error_code=ErrorCode.GENERAL, error_code=ErrorCode.GENERAL,
error_message=error_message, error_message=error_message,
) )
return flag_evaluation
finally: finally:
after_all_hooks( after_all_hooks(flag_type, hook_context, reversed_merged_hooks, hook_hints)
flag_type,
hook_context,
flag_evaluation,
reversed_merged_hooks,
hook_hints,
)
async def _create_provider_evaluation_async(
self,
provider: FeatureProvider,
flag_type: FlagType,
flag_key: str,
default_value: FlagValueType,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagEvaluationDetails[FlagValueType]:
get_details_callables_async: typing.Mapping[
FlagType, ResolveDetailsCallableAsync
] = {
FlagType.BOOLEAN: provider.resolve_boolean_details_async,
FlagType.INTEGER: provider.resolve_integer_details_async,
FlagType.FLOAT: provider.resolve_float_details_async,
FlagType.OBJECT: provider.resolve_object_details_async,
FlagType.STRING: provider.resolve_string_details_async,
}
get_details_callable = get_details_callables_async.get(flag_type)
if not get_details_callable:
return FlagEvaluationDetails(
flag_key=flag_key,
value=default_value,
reason=Reason.ERROR,
error_code=ErrorCode.GENERAL,
error_message="Unknown flag type",
)
resolution = await get_details_callable(
flag_key=flag_key,
default_value=default_value,
evaluation_context=evaluation_context,
)
if resolution.error_code:
return resolution.to_flag_evaluation_details(flag_key)
# we need to check the get_args to be compatible with union types.
if err := _typecheck_flag_value(value=resolution.value, flag_type=flag_type):
return FlagEvaluationDetails(
flag_key=flag_key,
value=resolution.value,
reason=Reason.ERROR,
error_code=err.error_code,
error_message=err.error_message,
)
return resolution.to_flag_evaluation_details(flag_key)
def _create_provider_evaluation( def _create_provider_evaluation(
self, self,
provider: FeatureProvider, provider: FeatureProvider,
flag_type: FlagType, flag_type: FlagType,
flag_key: str, flag_key: str,
default_value: FlagValueType, default_value: typing.Any,
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagEvaluationDetails[FlagValueType]: ) -> FlagEvaluationDetails[typing.Any]:
""" """
Encapsulated method to create a FlagEvaluationDetail from a specific provider. Encapsulated method to create a FlagEvaluationDetail from a specific provider.
@ -915,7 +410,13 @@ class OpenFeatureClient:
:return: a FlagEvaluationDetails object with the fully evaluated flag from a :return: a FlagEvaluationDetails object with the fully evaluated flag from a
provider provider
""" """
get_details_callables: typing.Mapping[FlagType, ResolveDetailsCallable] = { args = (
flag_key,
default_value,
evaluation_context,
)
get_details_callables: typing.Mapping[FlagType, GetDetailCallable] = {
FlagType.BOOLEAN: provider.resolve_boolean_details, FlagType.BOOLEAN: provider.resolve_boolean_details,
FlagType.INTEGER: provider.resolve_integer_details, FlagType.INTEGER: provider.resolve_integer_details,
FlagType.FLOAT: provider.resolve_float_details, FlagType.FLOAT: provider.resolve_float_details,
@ -925,33 +426,23 @@ class OpenFeatureClient:
get_details_callable = get_details_callables.get(flag_type) get_details_callable = get_details_callables.get(flag_type)
if not get_details_callable: if not get_details_callable:
return FlagEvaluationDetails( raise GeneralError(error_message="Unknown flag type")
flag_key=flag_key,
value=default_value,
reason=Reason.ERROR,
error_code=ErrorCode.GENERAL,
error_message="Unknown flag type",
)
resolution = get_details_callable( resolution = get_details_callable(*args)
flag_key=flag_key, resolution.raise_for_error()
default_value=default_value,
evaluation_context=evaluation_context,
)
if resolution.error_code:
return resolution.to_flag_evaluation_details(flag_key)
# we need to check the get_args to be compatible with union types. # we need to check the get_args to be compatible with union types.
if err := _typecheck_flag_value(value=resolution.value, flag_type=flag_type): _typecheck_flag_value(resolution.value, flag_type)
return FlagEvaluationDetails(
flag_key=flag_key,
value=resolution.value,
reason=Reason.ERROR,
error_code=err.error_code,
error_message=err.error_message,
)
return resolution.to_flag_evaluation_details(flag_key) return FlagEvaluationDetails(
flag_key=flag_key,
value=resolution.value,
variant=resolution.variant,
flag_metadata=resolution.flag_metadata or {},
reason=resolution.reason,
error_code=resolution.error_code,
error_message=resolution.error_message,
)
def add_handler(self, event: ProviderEvent, handler: EventHandler) -> None: def add_handler(self, event: ProviderEvent, handler: EventHandler) -> None:
_event_support.add_client_handler(self, event, handler) _event_support.add_client_handler(self, event, handler)
@ -960,9 +451,7 @@ class OpenFeatureClient:
_event_support.remove_client_handler(self, event, handler) _event_support.remove_client_handler(self, event, handler)
def _typecheck_flag_value( def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None:
value: typing.Any, flag_type: FlagType
) -> typing.Optional[OpenFeatureError]:
type_map: TypeMap = { type_map: TypeMap = {
FlagType.BOOLEAN: bool, FlagType.BOOLEAN: bool,
FlagType.STRING: str, FlagType.STRING: str,
@ -972,7 +461,6 @@ def _typecheck_flag_value(
} }
_type = type_map.get(flag_type) _type = type_map.get(flag_type)
if not _type: if not _type:
return GeneralError(error_message="Unknown flag type") raise GeneralError(error_message="Unknown flag type")
if not isinstance(value, _type): if not isinstance(value, _type):
return TypeMismatchError(f"Expected type {_type} but got {type(value)}") raise TypeMismatchError(f"Expected type {_type} but got {type(value)}")
return None

View File

@ -0,0 +1,19 @@
import typing
from dataclasses import dataclass, field
__all__ = ["EvaluationContext"]
@dataclass
class EvaluationContext:
targeting_key: typing.Optional[str] = None
attributes: dict = field(default_factory=dict)
def merge(self, ctx2: "EvaluationContext") -> "EvaluationContext":
if not (self and ctx2):
return self or ctx2
attributes = {**self.attributes, **ctx2.attributes}
targeting_key = ctx2.targeting_key or self.targeting_key
return EvaluationContext(targeting_key=targeting_key, attributes=attributes)

View File

@ -1,54 +0,0 @@
from __future__ import annotations
import typing
from collections.abc import Sequence
from dataclasses import dataclass, field
from datetime import datetime
from openfeature.exception import GeneralError
__all__ = ["EvaluationContext", "get_evaluation_context", "set_evaluation_context"]
# https://openfeature.dev/specification/sections/evaluation-context#requirement-312
EvaluationContextAttributes = typing.Mapping[
str,
typing.Union[
bool,
int,
float,
str,
datetime,
Sequence["EvaluationContextAttributes"],
typing.Mapping[str, "EvaluationContextAttributes"],
],
]
@dataclass
class EvaluationContext:
targeting_key: typing.Optional[str] = None
attributes: EvaluationContextAttributes = field(default_factory=dict)
def merge(self, ctx2: EvaluationContext) -> EvaluationContext:
if not (self and ctx2):
return self or ctx2
attributes = {**self.attributes, **ctx2.attributes}
targeting_key = ctx2.targeting_key or self.targeting_key
return EvaluationContext(targeting_key=targeting_key, attributes=attributes)
def get_evaluation_context() -> EvaluationContext:
return _evaluation_context
def set_evaluation_context(evaluation_context: EvaluationContext) -> None:
global _evaluation_context
if evaluation_context is None:
raise GeneralError(error_message="No api level evaluation context")
_evaluation_context = evaluation_context
# need to be at the bottom, because of the definition order
_evaluation_context = EvaluationContext()

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import Callable, Optional, Union from typing import Callable, Dict, List, Optional, Union
from openfeature.exception import ErrorCode from openfeature.exception import ErrorCode
@ -18,19 +18,19 @@ class ProviderEvent(Enum):
@dataclass @dataclass
class ProviderEventDetails: class ProviderEventDetails:
flags_changed: Optional[list[str]] = None flags_changed: Optional[List[str]] = None
message: Optional[str] = None message: Optional[str] = None
error_code: Optional[ErrorCode] = None error_code: Optional[ErrorCode] = None
metadata: dict[str, Union[bool, str, int, float]] = field(default_factory=dict) metadata: Dict[str, Union[bool, str, int, float]] = field(default_factory=dict)
@dataclass @dataclass
class EventDetails(ProviderEventDetails): class EventDetails(ProviderEventDetails):
provider_name: str = "" provider_name: str = ""
flags_changed: Optional[list[str]] = None flags_changed: Optional[List[str]] = None
message: Optional[str] = None message: Optional[str] = None
error_code: Optional[ErrorCode] = None error_code: Optional[ErrorCode] = None
metadata: dict[str, Union[bool, str, int, float]] = field(default_factory=dict) metadata: Dict[str, Union[bool, str, int, float]] = field(default_factory=dict)
@classmethod @classmethod
def from_provider_event_details( def from_provider_event_details(

View File

@ -2,8 +2,7 @@ from __future__ import annotations
import typing import typing
from collections.abc import Mapping from collections.abc import Mapping
from enum import Enum
from openfeature._backports.strenum import StrEnum
__all__ = [ __all__ = [
"ErrorCode", "ErrorCode",
@ -164,7 +163,7 @@ class InvalidContextError(OpenFeatureError):
super().__init__(ErrorCode.INVALID_CONTEXT, error_message) super().__init__(ErrorCode.INVALID_CONTEXT, error_message)
class ErrorCode(StrEnum): class ErrorCode(Enum):
PROVIDER_NOT_READY = "PROVIDER_NOT_READY" PROVIDER_NOT_READY = "PROVIDER_NOT_READY"
PROVIDER_FATAL = "PROVIDER_FATAL" PROVIDER_FATAL = "PROVIDER_FATAL"
FLAG_NOT_FOUND = "FLAG_NOT_FOUND" FLAG_NOT_FOUND = "FLAG_NOT_FOUND"

View File

@ -1,11 +1,10 @@
from __future__ import annotations from __future__ import annotations
import typing import typing
from collections.abc import Sequence
from dataclasses import dataclass, field from dataclasses import dataclass, field
from openfeature._backports.strenum import StrEnum from openfeature._backports.strenum import StrEnum
from openfeature.exception import ErrorCode, OpenFeatureError from openfeature.exception import ErrorCode
if typing.TYPE_CHECKING: # pragma: no cover if typing.TYPE_CHECKING: # pragma: no cover
# resolves a circular dependency in type annotations # resolves a circular dependency in type annotations
@ -35,22 +34,13 @@ class Reason(StrEnum):
DEFAULT = "DEFAULT" DEFAULT = "DEFAULT"
DISABLED = "DISABLED" DISABLED = "DISABLED"
ERROR = "ERROR" ERROR = "ERROR"
SPLIT = "SPLIT"
STATIC = "STATIC" STATIC = "STATIC"
STALE = "STALE" SPLIT = "SPLIT"
TARGETING_MATCH = "TARGETING_MATCH" TARGETING_MATCH = "TARGETING_MATCH"
UNKNOWN = "UNKNOWN" UNKNOWN = "UNKNOWN"
FlagMetadata = typing.Mapping[str, typing.Union[bool, int, float, str]] FlagMetadata = typing.Mapping[str, typing.Any]
FlagValueType = typing.Union[
bool,
int,
float,
str,
Sequence["FlagValueType"],
typing.Mapping[str, "FlagValueType"],
]
T_co = typing.TypeVar("T_co", covariant=True) T_co = typing.TypeVar("T_co", covariant=True)
@ -65,15 +55,10 @@ class FlagEvaluationDetails(typing.Generic[T_co]):
error_code: typing.Optional[ErrorCode] = None error_code: typing.Optional[ErrorCode] = None
error_message: typing.Optional[str] = None error_message: typing.Optional[str] = None
def get_exception(self) -> typing.Optional[OpenFeatureError]:
if self.error_code:
return ErrorCode.to_exception(self.error_code, self.error_message or "")
return None
@dataclass @dataclass
class FlagEvaluationOptions: class FlagEvaluationOptions:
hooks: list[Hook] = field(default_factory=list) hooks: typing.List[Hook] = field(default_factory=list)
hook_hints: HookHints = field(default_factory=dict) hook_hints: HookHints = field(default_factory=dict)
@ -93,14 +78,3 @@ class FlagResolutionDetails(typing.Generic[U_co]):
if self.error_code: if self.error_code:
raise ErrorCode.to_exception(self.error_code, self.error_message or "") raise ErrorCode.to_exception(self.error_code, self.error_message or "")
return None return None
def to_flag_evaluation_details(self, flag_key: str) -> FlagEvaluationDetails[U_co]:
return FlagEvaluationDetails(
flag_key=flag_key,
value=self.value,
variant=self.variant,
flag_metadata=self.flag_metadata,
reason=self.reason,
error_code=self.error_code,
error_message=self.error_message,
)

View File

@ -1,29 +1,18 @@
from __future__ import annotations from __future__ import annotations
import typing import typing
from collections.abc import Sequence
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from openfeature.evaluation_context import EvaluationContext from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType, FlagValueType from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType
if TYPE_CHECKING: if TYPE_CHECKING:
from openfeature.client import ClientMetadata from openfeature.client import ClientMetadata
from openfeature.provider.metadata import Metadata from openfeature.provider.metadata import Metadata
__all__ = [ __all__ = ["Hook", "HookContext", "HookHints", "HookType"]
"Hook",
"HookContext",
"HookHints",
"HookType",
"add_hooks",
"clear_hooks",
"get_hooks",
]
_hooks: list[Hook] = []
class HookType(Enum): class HookType(Enum):
@ -38,7 +27,7 @@ class HookContext:
self, self,
flag_key: str, flag_key: str,
flag_type: FlagType, flag_type: FlagType,
default_value: FlagValueType, default_value: typing.Any,
evaluation_context: EvaluationContext, evaluation_context: EvaluationContext,
client_metadata: typing.Optional[ClientMetadata] = None, client_metadata: typing.Optional[ClientMetadata] = None,
provider_metadata: typing.Optional[Metadata] = None, provider_metadata: typing.Optional[Metadata] = None,
@ -71,8 +60,8 @@ HookHints = typing.Mapping[
float, float,
str, str,
datetime, datetime,
Sequence["HookHints"], typing.List[typing.Any],
typing.Mapping[str, "HookHints"], typing.Dict[str, typing.Any],
], ],
] ]
@ -95,7 +84,7 @@ class Hook:
def after( def after(
self, self,
hook_context: HookContext, hook_context: HookContext,
details: FlagEvaluationDetails[FlagValueType], details: FlagEvaluationDetails[typing.Any],
hints: HookHints, hints: HookHints,
) -> None: ) -> None:
""" """
@ -120,12 +109,7 @@ class Hook:
""" """
pass pass
def finally_after( def finally_after(self, hook_context: HookContext, hints: HookHints) -> None:
self,
hook_context: HookContext,
details: FlagEvaluationDetails[FlagValueType],
hints: HookHints,
) -> None:
""" """
Run after flag evaluation, including any error processing. Run after flag evaluation, including any error processing.
This will always run. Errors will be swallowed. This will always run. Errors will be swallowed.
@ -144,17 +128,3 @@ class Hook:
or not (False) or not (False)
""" """
return True return True
def add_hooks(hooks: list[Hook]) -> None:
global _hooks
_hooks = _hooks + hooks
def clear_hooks() -> None:
global _hooks
_hooks = []
def get_hooks() -> list[Hook]:
return _hooks

View File

@ -13,7 +13,7 @@ def error_hooks(
flag_type: FlagType, flag_type: FlagType,
hook_context: HookContext, hook_context: HookContext,
exception: Exception, exception: Exception,
hooks: list[Hook], hooks: typing.List[Hook],
hints: typing.Optional[HookHints] = None, hints: typing.Optional[HookHints] = None,
) -> None: ) -> None:
kwargs = {"hook_context": hook_context, "exception": exception, "hints": hints} kwargs = {"hook_context": hook_context, "exception": exception, "hints": hints}
@ -25,11 +25,10 @@ def error_hooks(
def after_all_hooks( def after_all_hooks(
flag_type: FlagType, flag_type: FlagType,
hook_context: HookContext, hook_context: HookContext,
details: FlagEvaluationDetails[typing.Any], hooks: typing.List[Hook],
hooks: list[Hook],
hints: typing.Optional[HookHints] = None, hints: typing.Optional[HookHints] = None,
) -> None: ) -> None:
kwargs = {"hook_context": hook_context, "details": details, "hints": hints} kwargs = {"hook_context": hook_context, "hints": hints}
_execute_hooks( _execute_hooks(
flag_type=flag_type, hooks=hooks, hook_method=HookType.FINALLY_AFTER, **kwargs flag_type=flag_type, hooks=hooks, hook_method=HookType.FINALLY_AFTER, **kwargs
) )
@ -39,7 +38,7 @@ def after_hooks(
flag_type: FlagType, flag_type: FlagType,
hook_context: HookContext, hook_context: HookContext,
details: FlagEvaluationDetails[typing.Any], details: FlagEvaluationDetails[typing.Any],
hooks: list[Hook], hooks: typing.List[Hook],
hints: typing.Optional[HookHints] = None, hints: typing.Optional[HookHints] = None,
) -> None: ) -> None:
kwargs = {"hook_context": hook_context, "details": details, "hints": hints} kwargs = {"hook_context": hook_context, "details": details, "hints": hints}
@ -51,7 +50,7 @@ def after_hooks(
def before_hooks( def before_hooks(
flag_type: FlagType, flag_type: FlagType,
hook_context: HookContext, hook_context: HookContext,
hooks: list[Hook], hooks: typing.List[Hook],
hints: typing.Optional[HookHints] = None, hints: typing.Optional[HookHints] = None,
) -> EvaluationContext: ) -> EvaluationContext:
kwargs = {"hook_context": hook_context, "hints": hints} kwargs = {"hook_context": hook_context, "hints": hints}
@ -68,7 +67,7 @@ def before_hooks(
def _execute_hooks( def _execute_hooks(
flag_type: FlagType, flag_type: FlagType,
hooks: list[Hook], hooks: typing.List[Hook],
hook_method: HookType, hook_method: HookType,
**kwargs: typing.Any, **kwargs: typing.Any,
) -> list: ) -> list:
@ -91,10 +90,10 @@ def _execute_hooks(
def _execute_hooks_unchecked( def _execute_hooks_unchecked(
flag_type: FlagType, flag_type: FlagType,
hooks: list[Hook], hooks: typing.List[Hook],
hook_method: HookType, hook_method: HookType,
**kwargs: typing.Any, **kwargs: typing.Any,
) -> list[typing.Optional[EvaluationContext]]: ) -> typing.List[typing.Optional[EvaluationContext]]:
""" """
Execute a single hook without checking whether an exception is thrown. This is Execute a single hook without checking whether an exception is thrown. This is
used in the before and after hooks since any exception will be caught in the used in the before and after hooks since any exception will be caught in the

View File

@ -26,7 +26,7 @@ class MappingProxyType(dict):
__setitem__ = _immutable __setitem__ = _immutable
__delitem__ = _immutable __delitem__ = _immutable
clear = _immutable clear = _immutable
update = _immutable update = _immutable # type:ignore[assignment]
setdefault = _immutable setdefault = _immutable # type:ignore[assignment]
pop = _immutable pop = _immutable # type:ignore[assignment]
popitem = _immutable popitem = _immutable

View File

@ -2,7 +2,6 @@ from __future__ import annotations
import typing import typing
from abc import abstractmethod from abc import abstractmethod
from collections.abc import Sequence
from enum import Enum from enum import Enum
from openfeature.evaluation_context import EvaluationContext from openfeature.evaluation_context import EvaluationContext
@ -12,9 +11,6 @@ from openfeature.hook import Hook
from .metadata import Metadata from .metadata import Metadata
if typing.TYPE_CHECKING:
from openfeature.flag_evaluation import FlagValueType
__all__ = ["AbstractProvider", "FeatureProvider", "Metadata", "ProviderStatus"] __all__ = ["AbstractProvider", "FeatureProvider", "Metadata", "ProviderStatus"]
@ -42,7 +38,7 @@ class FeatureProvider(typing.Protocol): # pragma: no cover
def get_metadata(self) -> Metadata: ... def get_metadata(self) -> Metadata: ...
def get_provider_hooks(self) -> list[Hook]: ... def get_provider_hooks(self) -> typing.List[Hook]: ...
def resolve_boolean_details( def resolve_boolean_details(
self, self,
@ -51,13 +47,6 @@ class FeatureProvider(typing.Protocol): # pragma: no cover
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]: ... ) -> FlagResolutionDetails[bool]: ...
async def resolve_boolean_details_async(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]: ...
def resolve_string_details( def resolve_string_details(
self, self,
flag_key: str, flag_key: str,
@ -65,13 +54,6 @@ class FeatureProvider(typing.Protocol): # pragma: no cover
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]: ... ) -> FlagResolutionDetails[str]: ...
async def resolve_string_details_async(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]: ...
def resolve_integer_details( def resolve_integer_details(
self, self,
flag_key: str, flag_key: str,
@ -79,13 +61,6 @@ class FeatureProvider(typing.Protocol): # pragma: no cover
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]: ... ) -> FlagResolutionDetails[int]: ...
async def resolve_integer_details_async(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]: ...
def resolve_float_details( def resolve_float_details(
self, self,
flag_key: str, flag_key: str,
@ -93,41 +68,15 @@ class FeatureProvider(typing.Protocol): # pragma: no cover
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]: ... ) -> FlagResolutionDetails[float]: ...
async def resolve_float_details_async(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]: ...
def resolve_object_details( def resolve_object_details(
self, self,
flag_key: str, flag_key: str,
default_value: typing.Union[ default_value: typing.Union[dict, list],
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[ ) -> FlagResolutionDetails[typing.Union[dict, list]]: ...
typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]
]: ...
async def resolve_object_details_async(
self,
flag_key: str,
default_value: typing.Union[
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[
typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]
]: ...
class AbstractProvider(FeatureProvider): class AbstractProvider(FeatureProvider):
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
# this makes sure to invoke the parent of `FeatureProvider` -> `object`
super(FeatureProvider, self).__init__(*args, **kwargs)
def attach( def attach(
self, self,
on_emit: typing.Callable[ on_emit: typing.Callable[
@ -150,7 +99,7 @@ class AbstractProvider(FeatureProvider):
def get_metadata(self) -> Metadata: def get_metadata(self) -> Metadata:
pass pass
def get_provider_hooks(self) -> list[Hook]: def get_provider_hooks(self) -> typing.List[Hook]:
return [] return []
@abstractmethod @abstractmethod
@ -162,14 +111,6 @@ class AbstractProvider(FeatureProvider):
) -> FlagResolutionDetails[bool]: ) -> FlagResolutionDetails[bool]:
pass pass
async def resolve_boolean_details_async(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
return self.resolve_boolean_details(flag_key, default_value, evaluation_context)
@abstractmethod @abstractmethod
def resolve_string_details( def resolve_string_details(
self, self,
@ -179,14 +120,6 @@ class AbstractProvider(FeatureProvider):
) -> FlagResolutionDetails[str]: ) -> FlagResolutionDetails[str]:
pass pass
async def resolve_string_details_async(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]:
return self.resolve_string_details(flag_key, default_value, evaluation_context)
@abstractmethod @abstractmethod
def resolve_integer_details( def resolve_integer_details(
self, self,
@ -196,14 +129,6 @@ class AbstractProvider(FeatureProvider):
) -> FlagResolutionDetails[int]: ) -> FlagResolutionDetails[int]:
pass pass
async def resolve_integer_details_async(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]:
return self.resolve_integer_details(flag_key, default_value, evaluation_context)
@abstractmethod @abstractmethod
def resolve_float_details( def resolve_float_details(
self, self,
@ -213,39 +138,15 @@ class AbstractProvider(FeatureProvider):
) -> FlagResolutionDetails[float]: ) -> FlagResolutionDetails[float]:
pass pass
async def resolve_float_details_async(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]:
return self.resolve_float_details(flag_key, default_value, evaluation_context)
@abstractmethod @abstractmethod
def resolve_object_details( def resolve_object_details(
self, self,
flag_key: str, flag_key: str,
default_value: typing.Union[ default_value: typing.Union[dict, list],
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[ ) -> FlagResolutionDetails[typing.Union[dict, list]]:
typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]
]:
pass pass
async def resolve_object_details_async(
self,
flag_key: str,
default_value: typing.Union[
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[
typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]
]:
return self.resolve_object_details(flag_key, default_value, evaluation_context)
def emit_provider_ready(self, details: ProviderEventDetails) -> None: def emit_provider_ready(self, details: ProviderEventDetails) -> None:
self.emit(ProviderEvent.PROVIDER_READY, details) self.emit(ProviderEvent.PROVIDER_READY, details)

View File

@ -1,7 +1,7 @@
import typing import typing
from openfeature._event_support import run_handlers_for_provider from openfeature._event_support import run_handlers_for_provider
from openfeature.evaluation_context import EvaluationContext, get_evaluation_context from openfeature.evaluation_context import EvaluationContext
from openfeature.event import ( from openfeature.event import (
ProviderEvent, ProviderEvent,
ProviderEventDetails, ProviderEventDetails,
@ -13,8 +13,8 @@ from openfeature.provider.no_op_provider import NoOpProvider
class ProviderRegistry: class ProviderRegistry:
_default_provider: FeatureProvider _default_provider: FeatureProvider
_providers: dict[str, FeatureProvider] _providers: typing.Dict[str, FeatureProvider]
_provider_status: dict[FeatureProvider, ProviderStatus] _provider_status: typing.Dict[FeatureProvider, ProviderStatus]
def __init__(self) -> None: def __init__(self) -> None:
self._default_provider = NoOpProvider() self._default_provider = NoOpProvider()
@ -65,6 +65,9 @@ class ProviderRegistry:
self._shutdown_provider(provider) self._shutdown_provider(provider)
def _get_evaluation_context(self) -> EvaluationContext: def _get_evaluation_context(self) -> EvaluationContext:
# imported here to avoid circular imports
from openfeature.api import get_evaluation_context
return get_evaluation_context() return get_evaluation_context()
def _initialize_provider(self, provider: FeatureProvider) -> None: def _initialize_provider(self, provider: FeatureProvider) -> None:

View File

@ -1,19 +1,13 @@
from __future__ import annotations
import typing import typing
from collections.abc import Sequence
from dataclasses import dataclass, field from dataclasses import dataclass, field
from openfeature._backports.strenum import StrEnum from openfeature._backports.strenum import StrEnum
from openfeature.evaluation_context import EvaluationContext from openfeature.evaluation_context import EvaluationContext
from openfeature.exception import ErrorCode from openfeature.exception import FlagNotFoundError
from openfeature.flag_evaluation import FlagResolutionDetails, Reason from openfeature.flag_evaluation import FlagMetadata, FlagResolutionDetails, Reason
from openfeature.hook import Hook
from openfeature.provider import AbstractProvider, Metadata from openfeature.provider import AbstractProvider, Metadata
if typing.TYPE_CHECKING:
from openfeature.flag_evaluation import FlagMetadata, FlagValueType
from openfeature.hook import Hook
PASSED_IN_DEFAULT = "Passed in default" PASSED_IN_DEFAULT = "Passed in default"
@ -32,12 +26,12 @@ class InMemoryFlag(typing.Generic[T_co]):
DISABLED = "DISABLED" DISABLED = "DISABLED"
default_variant: str default_variant: str
variants: dict[str, T_co] variants: typing.Dict[str, T_co]
flag_metadata: FlagMetadata = field(default_factory=dict) flag_metadata: FlagMetadata = field(default_factory=dict)
state: State = State.ENABLED state: State = State.ENABLED
context_evaluator: typing.Optional[ context_evaluator: typing.Optional[
typing.Callable[ typing.Callable[
[InMemoryFlag[T_co], EvaluationContext], FlagResolutionDetails[T_co] ["InMemoryFlag[T_co]", EvaluationContext], FlagResolutionDetails[T_co]
] ]
] = None ] = None
@ -57,7 +51,7 @@ class InMemoryFlag(typing.Generic[T_co]):
) )
FlagStorage = dict[str, InMemoryFlag[typing.Any]] FlagStorage = typing.Dict[str, InMemoryFlag[typing.Any]]
V = typing.TypeVar("V") V = typing.TypeVar("V")
@ -71,7 +65,7 @@ class InMemoryProvider(AbstractProvider):
def get_metadata(self) -> Metadata: def get_metadata(self) -> Metadata:
return InMemoryMetadata() return InMemoryMetadata()
def get_provider_hooks(self) -> list[Hook]: def get_provider_hooks(self) -> typing.List[Hook]:
return [] return []
def resolve_boolean_details( def resolve_boolean_details(
@ -80,15 +74,7 @@ class InMemoryProvider(AbstractProvider):
default_value: bool, default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]: ) -> FlagResolutionDetails[bool]:
return self._resolve(flag_key, default_value, evaluation_context) return self._resolve(flag_key, evaluation_context)
async def resolve_boolean_details_async(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
return await self._resolve_async(flag_key, default_value, evaluation_context)
def resolve_string_details( def resolve_string_details(
self, self,
@ -96,15 +82,7 @@ class InMemoryProvider(AbstractProvider):
default_value: str, default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]: ) -> FlagResolutionDetails[str]:
return self._resolve(flag_key, default_value, evaluation_context) return self._resolve(flag_key, evaluation_context)
async def resolve_string_details_async(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]:
return await self._resolve_async(flag_key, default_value, evaluation_context)
def resolve_integer_details( def resolve_integer_details(
self, self,
@ -112,15 +90,7 @@ class InMemoryProvider(AbstractProvider):
default_value: int, default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]: ) -> FlagResolutionDetails[int]:
return self._resolve(flag_key, default_value, evaluation_context) return self._resolve(flag_key, evaluation_context)
async def resolve_integer_details_async(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]:
return await self._resolve_async(flag_key, default_value, evaluation_context)
def resolve_float_details( def resolve_float_details(
self, self,
@ -128,60 +98,22 @@ class InMemoryProvider(AbstractProvider):
default_value: float, default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]: ) -> FlagResolutionDetails[float]:
return self._resolve(flag_key, default_value, evaluation_context) return self._resolve(flag_key, evaluation_context)
async def resolve_float_details_async(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]:
return await self._resolve_async(flag_key, default_value, evaluation_context)
def resolve_object_details( def resolve_object_details(
self, self,
flag_key: str, flag_key: str,
default_value: typing.Union[ default_value: typing.Union[dict, list],
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[ ) -> FlagResolutionDetails[typing.Union[dict, list]]:
typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]] return self._resolve(flag_key, evaluation_context)
]:
return self._resolve(flag_key, default_value, evaluation_context)
async def resolve_object_details_async(
self,
flag_key: str,
default_value: typing.Union[
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[
typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]
]:
return await self._resolve_async(flag_key, default_value, evaluation_context)
def _resolve( def _resolve(
self, self,
flag_key: str, flag_key: str,
default_value: V,
evaluation_context: typing.Optional[EvaluationContext], evaluation_context: typing.Optional[EvaluationContext],
) -> FlagResolutionDetails[V]: ) -> FlagResolutionDetails[V]:
flag = self._flags.get(flag_key) flag = self._flags.get(flag_key)
if flag is None: if flag is None:
return FlagResolutionDetails( raise FlagNotFoundError(f"Flag '{flag_key}' not found")
value=default_value,
reason=Reason.ERROR,
error_code=ErrorCode.FLAG_NOT_FOUND,
error_message=f"Flag '{flag_key}' not found",
)
return flag.resolve(evaluation_context) return flag.resolve(evaluation_context)
async def _resolve_async(
self,
flag_key: str,
default_value: V,
evaluation_context: typing.Optional[EvaluationContext],
) -> FlagResolutionDetails[V]:
return self._resolve(flag_key, default_value, evaluation_context)

View File

@ -1,18 +1,11 @@
from __future__ import annotations
import typing import typing
from collections.abc import Sequence
from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagResolutionDetails, Reason from openfeature.flag_evaluation import FlagResolutionDetails, Reason
from openfeature.provider import AbstractProvider from openfeature.hook import Hook
from openfeature.provider import AbstractProvider, Metadata
from openfeature.provider.no_op_metadata import NoOpMetadata from openfeature.provider.no_op_metadata import NoOpMetadata
if typing.TYPE_CHECKING:
from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagValueType
from openfeature.hook import Hook
from openfeature.provider import Metadata
PASSED_IN_DEFAULT = "Passed in default" PASSED_IN_DEFAULT = "Passed in default"
@ -20,7 +13,7 @@ class NoOpProvider(AbstractProvider):
def get_metadata(self) -> Metadata: def get_metadata(self) -> Metadata:
return NoOpMetadata() return NoOpMetadata()
def get_provider_hooks(self) -> list[Hook]: def get_provider_hooks(self) -> typing.List[Hook]:
return [] return []
def resolve_boolean_details( def resolve_boolean_details(
@ -74,13 +67,9 @@ class NoOpProvider(AbstractProvider):
def resolve_object_details( def resolve_object_details(
self, self,
flag_key: str, flag_key: str,
default_value: typing.Union[ default_value: typing.Union[dict, list],
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[ ) -> FlagResolutionDetails[typing.Union[dict, list]]:
typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]
]:
return FlagResolutionDetails( return FlagResolutionDetails(
value=default_value, value=default_value,
reason=Reason.DEFAULT, reason=Reason.DEFAULT,

View File

@ -1,75 +0,0 @@
import typing
from collections.abc import Mapping
from dataclasses import dataclass
from openfeature.exception import ErrorCode
from openfeature.flag_evaluation import FlagEvaluationDetails, Reason
from openfeature.hook import HookContext
from openfeature.telemetry.attributes import TelemetryAttribute
from openfeature.telemetry.body import TelemetryBodyField
from openfeature.telemetry.metadata import TelemetryFlagMetadata
__all__ = [
"EvaluationEvent",
"TelemetryAttribute",
"TelemetryBodyField",
"TelemetryFlagMetadata",
"create_evaluation_event",
]
FLAG_EVALUATION_EVENT_NAME = "feature_flag.evaluation"
T_co = typing.TypeVar("T_co", covariant=True)
@dataclass
class EvaluationEvent(typing.Generic[T_co]):
name: str
attributes: Mapping[TelemetryAttribute, typing.Union[str, T_co]]
body: Mapping[TelemetryBodyField, T_co]
def create_evaluation_event(
hook_context: HookContext, details: FlagEvaluationDetails[T_co]
) -> EvaluationEvent[T_co]:
attributes = {
TelemetryAttribute.KEY: details.flag_key,
TelemetryAttribute.EVALUATION_REASON: (
details.reason or Reason.UNKNOWN
).lower(),
}
body = {}
if variant := details.variant:
attributes[TelemetryAttribute.VARIANT] = variant
else:
body[TelemetryBodyField.VALUE] = details.value
context_id = details.flag_metadata.get(
TelemetryFlagMetadata.CONTEXT_ID, hook_context.evaluation_context.targeting_key
)
if context_id:
attributes[TelemetryAttribute.CONTEXT_ID] = typing.cast("str", context_id)
if set_id := details.flag_metadata.get(TelemetryFlagMetadata.FLAG_SET_ID):
attributes[TelemetryAttribute.SET_ID] = typing.cast("str", set_id)
if version := details.flag_metadata.get(TelemetryFlagMetadata.VERSION):
attributes[TelemetryAttribute.VERSION] = typing.cast("str", version)
if metadata := hook_context.provider_metadata:
attributes[TelemetryAttribute.PROVIDER_NAME] = metadata.name
if details.reason == Reason.ERROR:
attributes[TelemetryAttribute.ERROR_TYPE] = (
details.error_code or ErrorCode.GENERAL
).lower()
if err_msg := details.error_message:
attributes[TelemetryAttribute.EVALUATION_ERROR_MESSAGE] = err_msg
return EvaluationEvent(
name=FLAG_EVALUATION_EVENT_NAME,
attributes=attributes,
body=body,
)

View File

@ -1,19 +0,0 @@
from openfeature._backports.strenum import StrEnum
class TelemetryAttribute(StrEnum):
"""
The attributes of an OpenTelemetry compliant event for flag evaluation.
See: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
"""
CONTEXT_ID = "feature_flag.context.id"
ERROR_TYPE = "error.type"
EVALUATION_ERROR_MESSAGE = "feature_flag.evaluation.error.message"
EVALUATION_REASON = "feature_flag.evaluation.reason"
KEY = "feature_flag.key"
PROVIDER_NAME = "feature_flag.provider_name"
SET_ID = "feature_flag.set.id"
VARIANT = "feature_flag.variant"
VERSION = "feature_flag.version"

View File

@ -1,11 +0,0 @@
from openfeature._backports.strenum import StrEnum
class TelemetryBodyField(StrEnum):
"""
OpenTelemetry event body fields.
See: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
"""
VALUE = "value"

View File

@ -1,13 +0,0 @@
from openfeature._backports.strenum import StrEnum
class TelemetryFlagMetadata(StrEnum):
"""
Well-known flag metadata attributes for telemetry events.
See: https://openfeature.dev/specification/appendix-d/#flag-metadata
"""
CONTEXT_ID = "contextId"
FLAG_SET_ID = "flagSetId"
VERSION = "version"

View File

@ -1,10 +1,6 @@
from openfeature.evaluation_context import EvaluationContext
from openfeature.transaction_context.context_var_transaction_context_propagator import ( from openfeature.transaction_context.context_var_transaction_context_propagator import (
ContextVarsTransactionContextPropagator, ContextVarsTransactionContextPropagator,
) )
from openfeature.transaction_context.no_op_transaction_context_propagator import (
NoOpTransactionContextPropagator,
)
from openfeature.transaction_context.transaction_context_propagator import ( from openfeature.transaction_context.transaction_context_propagator import (
TransactionContextPropagator, TransactionContextPropagator,
) )
@ -12,29 +8,4 @@ from openfeature.transaction_context.transaction_context_propagator import (
__all__ = [ __all__ = [
"ContextVarsTransactionContextPropagator", "ContextVarsTransactionContextPropagator",
"TransactionContextPropagator", "TransactionContextPropagator",
"get_transaction_context",
"set_transaction_context",
"set_transaction_context_propagator",
] ]
_evaluation_transaction_context_propagator: TransactionContextPropagator = (
NoOpTransactionContextPropagator()
)
def set_transaction_context_propagator(
transaction_context_propagator: TransactionContextPropagator,
) -> None:
global _evaluation_transaction_context_propagator
_evaluation_transaction_context_propagator = transaction_context_propagator
def get_transaction_context() -> EvaluationContext:
return _evaluation_transaction_context_propagator.get_transaction_context()
def set_transaction_context(evaluation_context: EvaluationContext) -> None:
global _evaluation_transaction_context_propagator
_evaluation_transaction_context_propagator.set_transaction_context(
evaluation_context
)

View File

@ -1 +1 @@
__version__ = "0.8.2" __version__ = "0.7.4"

View File

@ -1,11 +1,11 @@
# pyproject.toml # pyproject.toml
[build-system] [build-system]
requires = ["uv_build~=0.8.0"] requires = ["hatchling"]
build-backend = "uv_build" build-backend = "hatchling.build"
[project] [project]
name = "openfeature_sdk" name = "openfeature_sdk"
version = "0.8.2" version = "0.7.4"
description = "Standardizing Feature Flagging for Everyone" description = "Standardizing Feature Flagging for Everyone"
readme = "README.md" readme = "README.md"
authors = [{ name = "OpenFeature", email = "openfeature-core@groups.io" }] authors = [{ name = "OpenFeature", email = "openfeature-core@groups.io" }]
@ -22,32 +22,50 @@ keywords = [
"toggles", "toggles",
] ]
dependencies = [] dependencies = []
requires-python = ">=3.9" requires-python = ">=3.8"
[project.urls] [project.urls]
Homepage = "https://github.com/open-feature/python-sdk" Homepage = "https://github.com/open-feature/python-sdk"
[dependency-groups] [tool.hatch]
dev = [
[tool.hatch.envs.default]
dependencies = [
"behave", "behave",
"coverage[toml]>=6.5", "coverage[toml]>=6.5",
"pytest", "pytest",
"pytest-asyncio", "pytest-asyncio"
"pre-commit"
] ]
[tool.uv] [tool.hatch.envs.default.scripts]
required-version = "~=0.8.0" test = "pytest {args:tests}"
test-cov = "coverage run -m pytest {args:tests}"
cov-report = [
"coverage xml",
]
cov = [
"test-cov",
"cov-report",
]
e2e = [
"git submodule update --init",
"cp test-harness/features/evaluation.feature tests/features/",
"behave tests/features/",
"rm tests/features/*.feature",
]
[tool.uv.build-backend] [tool.hatch.build.targets.sdist]
module-name = "openfeature" exclude = [
module-root = "" ".gitignore",
namespace = true "test-harness",
"venv",
]
[tool.hatch.build.targets.wheel]
packages = ["openfeature"]
[tool.mypy] [tool.mypy]
files = "openfeature" files = "openfeature"
python_version = "3.9" # should be identical to the minimum supported version
namespace_packages = true namespace_packages = true
explicit_package_bases = true explicit_package_bases = true
local_partial_types = true # will become the new default from version 2 local_partial_types = true # will become the new default from version 2
@ -55,15 +73,6 @@ pretty = true
strict = true strict = true
disallow_any_generics = false disallow_any_generics = false
[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
[tool.coverage.report]
exclude_also = [
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",
]
[tool.ruff] [tool.ruff]
exclude = [ exclude = [
".git", ".git",
@ -71,7 +80,7 @@ exclude = [
"__pycache__", "__pycache__",
"venv", "venv",
] ]
target-version = "py39" target-version = "py38"
[tool.ruff.lint] [tool.ruff.lint]
select = [ select = [
@ -114,13 +123,3 @@ max-statements = 30
[tool.ruff.lint.pyupgrade] [tool.ruff.lint.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`. # Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true keep-runtime-typing = true
[project.scripts]
# workaround while UV doesn't support scripts directly in the pyproject.toml
# see: https://github.com/astral-sh/uv/issues/5903
test = "scripts.scripts:test"
test-cov = "scripts.scripts:test_cov"
cov-report = "scripts.scripts:cov_report"
cov = "scripts.scripts:cov"
e2e = "scripts.scripts:e2e"
precommit = "scripts.scripts:precommit"

View File

@ -1,5 +1,6 @@
{ {
"bootstrap-sha": "198336b098f167f858675235214cc907ede10182", "bootstrap-sha": "198336b098f167f858675235214cc907ede10182",
"signoff": "OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>",
"packages": { "packages": {
".": { ".": {
"release-type": "python", "release-type": "python",

View File

@ -1,15 +1,30 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": [
"github>open-feature/community-tooling" "config:base"
], ],
"semanticCommits": "enabled",
"pep621": { "pep621": {
"enabled": true "enabled": true
}, },
"pre-commit": { "pre-commit": {
"enabled": true "enabled": true
}, },
"lockFileMaintenance": { "packageRules": [
"enabled": true {
} "description": "Automerge non-major updates",
"matchUpdateTypes": [
"minor",
"patch"
],
"matchCurrentVersion": "!/^0/",
"automerge": true
},
{
"matchManagers": [
"github-actions"
],
"automerge": true
}
]
} }

View File

@ -1,38 +0,0 @@
# ruff: noqa: S602, S607
import subprocess
def test():
"""Run pytest tests."""
subprocess.run("pytest tests", shell=True, check=True)
def test_cov():
"""Run tests with coverage."""
subprocess.run("coverage run -m pytest tests", shell=True, check=True)
def cov_report():
"""Generate coverage report."""
subprocess.run("coverage xml", shell=True, check=True)
def cov():
"""Run tests with coverage and generate report."""
test_cov()
cov_report()
def e2e():
"""Run end-to-end tests."""
subprocess.run("git submodule update --init --recursive", shell=True, check=True)
subprocess.run(
"cp spec/specification/assets/gherkin/* tests/features/", shell=True, check=True
)
subprocess.run("behave tests/features/", shell=True, check=True)
subprocess.run("rm tests/features/*.feature", shell=True, check=True)
def precommit():
"""Run pre-commit hooks."""
subprocess.run("uv run pre-commit run --all-files", shell=True, check=True)

1
spec

@ -1 +0,0 @@
Subproject commit 0cd553d85f03b0b7ab983a988ce1720a3190bc88

1
test-harness Submodule

@ -0,0 +1 @@
Subproject commit bd13458f7e3587ab2ed98b8017bea3c2eb472cc9

View File

@ -69,16 +69,4 @@ IN_MEMORY_FLAGS = {
variants={"one": "uno", "two": "dos"}, variants={"one": "uno", "two": "dos"},
default_variant="one", default_variant="one",
), ),
"metadata-flag": InMemoryFlag(
state=InMemoryFlag.State.ENABLED,
default_variant="on",
variants={"on": True, "off": False},
context_evaluator=None,
flag_metadata={
"string": "1.0.2",
"integer": 2,
"float": 0.1,
"boolean": True,
},
),
} }

View File

@ -1,31 +0,0 @@
import contextlib
from behave import given, when
@given('a {flag_type}-flag with key "{flag_key}" and a default value "{default_value}"')
def step_impl_flag(context, flag_type: str, flag_key, default_value):
if default_value.lower() == "true" or default_value.lower() == "false":
default_value = bool(default_value)
try:
default_value = int(default_value)
except ValueError:
with contextlib.suppress(ValueError):
default_value = float(default_value)
context.flag = (flag_type, flag_key, default_value)
@when("the flag was evaluated with details")
def step_impl_evaluation(context):
client = context.client
flag_type, key, default_value = context.flag
if flag_type.lower() == "string":
context.evaluation = client.get_string_details(key, default_value)
elif flag_type.lower() == "boolean":
context.evaluation = client.get_boolean_details(key, default_value)
elif flag_type.lower() == "object":
context.evaluation = client.get_object_details(key, default_value)
elif flag_type.lower() == "float":
context.evaluation = client.get_float_details(key, default_value)
elif flag_type.lower() == "integer":
context.evaluation = client.get_integer_details(key, default_value)

View File

@ -1,66 +0,0 @@
from unittest.mock import MagicMock
from behave import given, then
from openfeature.exception import ErrorCode
from openfeature.flag_evaluation import Reason
from openfeature.hook import Hook
@given("a client with added hook")
def step_impl_add_hook(context):
hook = MagicMock(spec=Hook)
hook.before = MagicMock()
hook.after = MagicMock()
hook.error = MagicMock()
hook.finally_after = MagicMock()
context.hook = hook
context.client.add_hooks([hook])
@then('the "{hook_name}" hook should have been executed')
def step_impl_should_called(context, hook_name):
hook = get_hook_from_name(context, hook_name)
assert hook.called
@then('the "{hook_names}" hooks should be called with evaluation details')
def step_impl_should_have_eval_details(context, hook_names):
for hook_name in hook_names.split(", "):
hook = get_hook_from_name(context, hook_name)
for row in context.table:
flag_type, key, value = row
value = convert_value_from_key_and_flag_type(value, key, flag_type)
actual = hook.call_args[1]["details"].__dict__[key]
assert actual == value
def get_hook_from_name(context, hook_name):
if hook_name.lower() == "before":
return context.hook.before
elif hook_name.lower() == "after":
return context.hook.after
elif hook_name.lower() == "error":
return context.hook.error
elif hook_name.lower() == "finally":
return context.hook.finally_after
else:
raise ValueError(str(hook_name) + " is not a valid hook name")
def convert_value_from_key_and_flag_type(value, key, flag_type):
if value in ("None", "null"):
return None
if flag_type.lower() == "boolean":
return bool(value)
elif flag_type.lower() == "integer":
return int(value)
elif flag_type.lower() == "float":
return float(value)
elif key == "reason":
return Reason(value)
elif key == "error_code":
return ErrorCode(value)
return value

View File

@ -1,43 +0,0 @@
from behave import given, then
from openfeature.api import get_client, set_provider
from openfeature.provider.in_memory_provider import InMemoryProvider
from tests.features.data import IN_MEMORY_FLAGS
@given("a stable provider")
def step_impl_stable_provider(context):
set_provider(InMemoryProvider(IN_MEMORY_FLAGS))
context.client = get_client()
@then('the resolved metadata value "{key}" should be "{value}"')
def step_impl_check_metadata(context, key, value):
assert context.evaluation.flag_metadata[key] == value
@then("the resolved metadata is empty")
def step_impl_empty_metadata(context):
assert not context.evaluation.flag_metadata
@then("the resolved metadata should contain")
def step_impl_metadata_contains(context):
for row in context.table:
key, metadata_type, value = row
assert context.evaluation.flag_metadata[
key
] == convert_value_from_metadata_type(value, metadata_type)
def convert_value_from_metadata_type(value, metadata_type):
if value == "None":
return None
if metadata_type.lower() == "boolean":
return bool(value)
elif metadata_type.lower() == "integer":
return int(value)
elif metadata_type.lower() == "float":
return float(value)
return value

View File

@ -1,7 +1,5 @@
# flake8: noqa: F811 # flake8: noqa: F811
from time import sleep
from behave import given, then, when from behave import given, then, when
from openfeature.api import get_client, set_provider from openfeature.api import get_client, set_provider
@ -19,7 +17,7 @@ from tests.features.data import IN_MEMORY_FLAGS
'the resolved {flag_type} details reason of flag with key "{key}" should be ' 'the resolved {flag_type} details reason of flag with key "{key}" should be '
'"{reason}"' '"{reason}"'
) )
def step_impl_resolved_should_be(context, flag_type, key, expected_reason): def step_impl(context, flag_type, key, expected_reason):
details: FlagEvaluationDetails = None details: FlagEvaluationDetails = None
if flag_type == "boolean": if flag_type == "boolean":
details = context.boolean_flag_details details = context.boolean_flag_details
@ -27,13 +25,7 @@ def step_impl_resolved_should_be(context, flag_type, key, expected_reason):
@given("a provider is registered with cache disabled") @given("a provider is registered with cache disabled")
def step_impl_provider_without_cache(context): def step_impl(context):
set_provider(InMemoryProvider(IN_MEMORY_FLAGS))
context.client = get_client()
@given("a provider is registered")
def step_impl_provider(context):
set_provider(InMemoryProvider(IN_MEMORY_FLAGS)) set_provider(InMemoryProvider(IN_MEMORY_FLAGS))
context.client = get_client() context.client = get_client()
@ -42,9 +34,8 @@ def step_impl_provider(context):
'a {flag_type} flag with key "{key}" is evaluated with details and default value ' 'a {flag_type} flag with key "{key}" is evaluated with details and default value '
'"{default_value}"' '"{default_value}"'
) )
def step_impl_evaluated_with_details(context, flag_type, key, default_value): def step_impl(context, flag_type, key, default_value):
if context.client is None: context.client = get_client()
context.client = get_client()
if flag_type == "boolean": if flag_type == "boolean":
context.boolean_flag_details = context.client.get_boolean_details( context.boolean_flag_details = context.client.get_boolean_details(
key, default_value key, default_value
@ -59,9 +50,7 @@ def step_impl_evaluated_with_details(context, flag_type, key, default_value):
'a boolean flag with key "{key}" is evaluated with {eval_details} and default ' 'a boolean flag with key "{key}" is evaluated with {eval_details} and default '
'value "{default_value}"' 'value "{default_value}"'
) )
def step_impl_bool_evaluated_with_details_and_default( def step_impl(context, key, eval_details, default_value):
context, key, eval_details, default_value
):
client: OpenFeatureClient = context.client client: OpenFeatureClient = context.client
context.boolean_flag_details = client.get_boolean_details(key, default_value) context.boolean_flag_details = client.get_boolean_details(key, default_value)
@ -71,7 +60,7 @@ def step_impl_bool_evaluated_with_details_and_default(
'a {flag_type} flag with key "{key}" is evaluated with default value ' 'a {flag_type} flag with key "{key}" is evaluated with default value '
'"{default_value}"' '"{default_value}"'
) )
def step_impl_evaluated_with_default(context, flag_type, key, default_value): def step_impl(context, flag_type, key, default_value):
client: OpenFeatureClient = context.client client: OpenFeatureClient = context.client
if flag_type == "boolean": if flag_type == "boolean":
@ -81,12 +70,12 @@ def step_impl_evaluated_with_default(context, flag_type, key, default_value):
@then('the resolved string value should be "{expected_value}"') @then('the resolved string value should be "{expected_value}"')
def step_impl_resolved_string_should_be(context, expected_value): def step_impl(context, expected_value):
assert expected_value == context.string_flag_details.value assert expected_value == context.string_flag_details.value
@then('the resolved boolean value should be "{expected_value}"') @then('the resolved boolean value should be "{expected_value}"')
def step_impl_resolved_bool_should_be(context, expected_value): def step_impl(context, expected_value):
assert parse_boolean(expected_value) == context.boolean_flag_details.value assert parse_boolean(expected_value) == context.boolean_flag_details.value
@ -94,7 +83,7 @@ def step_impl_resolved_bool_should_be(context, expected_value):
'an integer flag with key "{key}" is evaluated with details and default value ' 'an integer flag with key "{key}" is evaluated with details and default value '
"{default_value:d}" "{default_value:d}"
) )
def step_impl_int_evaluated_with_details_and_default(context, key, default_value): def step_impl(context, key, default_value):
context.flag_key = key context.flag_key = key
context.default_value = default_value context.default_value = default_value
context.integer_flag_details = context.client.get_integer_details( context.integer_flag_details = context.client.get_integer_details(
@ -105,7 +94,7 @@ def step_impl_int_evaluated_with_details_and_default(context, key, default_value
@when( @when(
'an integer flag with key "{key}" is evaluated with default value {default_value:d}' 'an integer flag with key "{key}" is evaluated with default value {default_value:d}'
) )
def step_impl_int_evaluated_with_default(context, key, default_value): def step_impl(context, key, default_value):
context.flag_key = key context.flag_key = key
context.default_value = default_value context.default_value = default_value
context.integer_flag_details = context.client.get_integer_details( context.integer_flag_details = context.client.get_integer_details(
@ -114,26 +103,26 @@ def step_impl_int_evaluated_with_default(context, key, default_value):
@when('a float flag with key "{key}" is evaluated with default value {default_value:f}') @when('a float flag with key "{key}" is evaluated with default value {default_value:f}')
def step_impl_float_evaluated_with_default(context, key, default_value): def step_impl(context, key, default_value):
context.flag_key = key context.flag_key = key
context.default_value = default_value context.default_value = default_value
context.float_flag_details = context.client.get_float_details(key, default_value) context.float_flag_details = context.client.get_float_details(key, default_value)
@when('an object flag with key "{key}" is evaluated with a null default value') @when('an object flag with key "{key}" is evaluated with a null default value')
def step_impl_obj_evaluated_with_default(context, key): def step_impl(context, key):
context.flag_key = key context.flag_key = key
context.default_value = None context.default_value = None
context.object_flag_details = context.client.get_object_details(key, None) context.object_flag_details = context.client.get_object_details(key, None)
@then("the resolved integer value should be {expected_value:d}") @then("the resolved integer value should be {expected_value:d}")
def step_impl_resolved_int_should_be(context, expected_value): def step_impl(context, expected_value):
assert expected_value == context.integer_flag_details.value assert expected_value == context.integer_flag_details.value
@then("the resolved float value should be {expected_value:f}") @then("the resolved float value should be {expected_value:f}")
def step_impl_resolved_bool_should_be(context, expected_value): def step_impl(context, expected_value):
assert expected_value == context.float_flag_details.value assert expected_value == context.float_flag_details.value
@ -142,9 +131,7 @@ def step_impl_resolved_bool_should_be(context, expected_value):
'the resolved boolean details value should be "{expected_value}", the variant ' 'the resolved boolean details value should be "{expected_value}", the variant '
'should be "{variant}", and the reason should be "{reason}"' 'should be "{variant}", and the reason should be "{reason}"'
) )
def step_impl_resolved_bool_should_be_with_reason( def step_impl(context, expected_value, variant, reason):
context, expected_value, variant, reason
):
assert parse_boolean(expected_value) == context.boolean_flag_details.value assert parse_boolean(expected_value) == context.boolean_flag_details.value
assert variant == context.boolean_flag_details.variant assert variant == context.boolean_flag_details.variant
assert reason == context.boolean_flag_details.reason assert reason == context.boolean_flag_details.reason
@ -154,9 +141,7 @@ def step_impl_resolved_bool_should_be_with_reason(
'the resolved string details value should be "{expected_value}", the variant ' 'the resolved string details value should be "{expected_value}", the variant '
'should be "{variant}", and the reason should be "{reason}"' 'should be "{variant}", and the reason should be "{reason}"'
) )
def step_impl_resolved_string_should_be_with_reason( def step_impl(context, expected_value, variant, reason):
context, expected_value, variant, reason
):
assert expected_value == context.string_flag_details.value assert expected_value == context.string_flag_details.value
assert variant == context.string_flag_details.variant assert variant == context.string_flag_details.variant
assert reason == context.string_flag_details.reason assert reason == context.string_flag_details.reason
@ -166,9 +151,7 @@ def step_impl_resolved_string_should_be_with_reason(
'the resolved object value should be contain fields "{field1}", "{field2}", and ' 'the resolved object value should be contain fields "{field1}", "{field2}", and '
'"{field3}", with values "{val1}", "{val2}" and {val3}, respectively' '"{field3}", with values "{val1}", "{val2}" and {val3}, respectively'
) )
def step_impl_resolved_obj_should_contain( def step_impl(context, field1, field2, field3, val1, val2, val3):
context, field1, field2, field3, val1, val2, val3
):
value = context.object_flag_details.value value = context.object_flag_details.value
assert field1 in value assert field1 in value
assert field2 in value assert field2 in value
@ -179,7 +162,7 @@ def step_impl_resolved_obj_should_contain(
@then('the resolved flag value is "{flag_value}" when the context is empty') @then('the resolved flag value is "{flag_value}" when the context is empty')
def step_impl_resolved_is_with_empty_context(context, flag_value): def step_impl(context, flag_value):
context.string_flag_details = context.client.get_boolean_details( context.string_flag_details = context.client.get_boolean_details(
context.flag_key, context.default_value context.flag_key, context.default_value
) )
@ -190,13 +173,13 @@ def step_impl_resolved_is_with_empty_context(context, flag_value):
"the reason should indicate an error and the error code should indicate a missing " "the reason should indicate an error and the error code should indicate a missing "
'flag with "{error_code}"' 'flag with "{error_code}"'
) )
def step_impl_reason_should_indicate(context, error_code): def step_impl(context, error_code):
assert context.string_flag_details.reason == Reason.ERROR assert context.string_flag_details.reason == Reason.ERROR
assert context.string_flag_details.error_code == ErrorCode[error_code] assert context.string_flag_details.error_code == ErrorCode[error_code]
@then("the default {flag_type} value should be returned") @then("the default {flag_type} value should be returned")
def step_impl_return_default(context, flag_type): def step_impl(context, flag_type):
flag_details = getattr(context, f"{flag_type}_flag_details") flag_details = getattr(context, f"{flag_type}_flag_details")
assert context.default_value == flag_details.value assert context.default_value == flag_details.value
@ -205,7 +188,7 @@ def step_impl_return_default(context, flag_type):
'a float flag with key "{key}" is evaluated with details and default value ' 'a float flag with key "{key}" is evaluated with details and default value '
"{default_value:f}" "{default_value:f}"
) )
def step_impl_float_with_details(context, key, default_value): def step_impl(context, key, default_value):
context.float_flag_details = context.client.get_float_details(key, default_value) context.float_flag_details = context.client.get_float_details(key, default_value)
@ -213,7 +196,7 @@ def step_impl_float_with_details(context, key, default_value):
"the resolved float details value should be {expected_value:f}, the variant should " "the resolved float details value should be {expected_value:f}, the variant should "
'be "{variant}", and the reason should be "{reason}"' 'be "{variant}", and the reason should be "{reason}"'
) )
def step_impl_resolved_float_with_variant(context, expected_value, variant, reason): def step_impl(context, expected_value, variant, reason):
assert expected_value == context.float_flag_details.value assert expected_value == context.float_flag_details.value
assert variant == context.float_flag_details.variant assert variant == context.float_flag_details.variant
assert reason == context.float_flag_details.reason assert reason == context.float_flag_details.reason
@ -222,7 +205,7 @@ def step_impl_resolved_float_with_variant(context, expected_value, variant, reas
@when( @when(
'an object flag with key "{key}" is evaluated with details and a null default value' 'an object flag with key "{key}" is evaluated with details and a null default value'
) )
def step_impl_eval_obj(context, key): def step_impl(context, key):
context.object_flag_details = context.client.get_object_details(key, None) context.object_flag_details = context.client.get_object_details(key, None)
@ -230,7 +213,7 @@ def step_impl_eval_obj(context, key):
'the resolved object details value should be contain fields "{field1}", "{field2}",' 'the resolved object details value should be contain fields "{field1}", "{field2}",'
' and "{field3}", with values "{val1}", "{val2}" and {val3}, respectively' ' and "{field3}", with values "{val1}", "{val2}" and {val3}, respectively'
) )
def step_impl_eval_obj_with_fields(context, field1, field2, field3, val1, val2, val3): def step_impl(context, field1, field2, field3, val1, val2, val3):
value = context.object_flag_details.value value = context.object_flag_details.value
assert field1 in value assert field1 in value
assert field2 in value assert field2 in value
@ -241,7 +224,7 @@ def step_impl_eval_obj_with_fields(context, field1, field2, field3, val1, val2,
@then('the variant should be "{variant}", and the reason should be "{reason}"') @then('the variant should be "{variant}", and the reason should be "{reason}"')
def step_impl_variant(context, variant, reason): def step_impl(context, variant, reason):
assert variant == context.object_flag_details.variant assert variant == context.object_flag_details.variant
assert reason == context.object_flag_details.reason assert reason == context.object_flag_details.reason
@ -250,7 +233,7 @@ def step_impl_variant(context, variant, reason):
'context contains keys "{key1}", "{key2}", "{key3}", "{key4}" with values "{val1}",' 'context contains keys "{key1}", "{key2}", "{key3}", "{key4}" with values "{val1}",'
' "{val2}", {val3:d}, "{val4}"' ' "{val2}", {val3:d}, "{val4}"'
) )
def step_impl_context(context, key1, key2, key3, key4, val1, val2, val3, val4): def step_impl(context, key1, key2, key3, key4, val1, val2, val3, val4):
context.evaluation_context = EvaluationContext( context.evaluation_context = EvaluationContext(
None, None,
{ {
@ -263,7 +246,7 @@ def step_impl_context(context, key1, key2, key3, key4, val1, val2, val3, val4):
@when('a flag with key "{key}" is evaluated with default value "{default_value}"') @when('a flag with key "{key}" is evaluated with default value "{default_value}"')
def step_impl_flag_with_key_and_default(context, key, default_value): def step_impl(context, key, default_value):
context.flag_key = key context.flag_key = key
context.default_value = default_value context.default_value = default_value
context.string_flag_details = context.client.get_string_details( context.string_flag_details = context.client.get_string_details(
@ -272,7 +255,7 @@ def step_impl_flag_with_key_and_default(context, key, default_value):
@then('the resolved string response should be "{expected_value}"') @then('the resolved string response should be "{expected_value}"')
def step_impl_reason(context, expected_value): def step_impl(context, expected_value):
assert expected_value == context.string_flag_details.value assert expected_value == context.string_flag_details.value
@ -280,7 +263,7 @@ def step_impl_reason(context, expected_value):
'a non-existent string flag with key "{flag_key}" is evaluated with details and a ' 'a non-existent string flag with key "{flag_key}" is evaluated with details and a '
'default value "{default_value}"' 'default value "{default_value}"'
) )
def step_impl_non_existing(context, flag_key, default_value): def step_impl(context, flag_key, default_value):
context.flag_key = flag_key context.flag_key = flag_key
context.default_value = default_value context.default_value = default_value
context.string_flag_details = context.client.get_string_details( context.string_flag_details = context.client.get_string_details(
@ -292,7 +275,7 @@ def step_impl_non_existing(context, flag_key, default_value):
'a string flag with key "{flag_key}" is evaluated as an integer, with details and a' 'a string flag with key "{flag_key}" is evaluated as an integer, with details and a'
" default value {default_value:d}" " default value {default_value:d}"
) )
def step_impl_string_with_details(context, flag_key, default_value): def step_impl(context, flag_key, default_value):
context.flag_key = flag_key context.flag_key = flag_key
context.default_value = default_value context.default_value = default_value
context.integer_flag_details = context.client.get_integer_details( context.integer_flag_details = context.client.get_integer_details(
@ -304,7 +287,7 @@ def step_impl_string_with_details(context, flag_key, default_value):
"the reason should indicate an error and the error code should indicate a type " "the reason should indicate an error and the error code should indicate a type "
'mismatch with "{error_code}"' 'mismatch with "{error_code}"'
) )
def step_impl_type_mismatch(context, error_code): def step_impl(context, error_code):
assert context.integer_flag_details.reason == Reason.ERROR assert context.integer_flag_details.reason == Reason.ERROR
assert context.integer_flag_details.error_code == ErrorCode[error_code] assert context.integer_flag_details.error_code == ErrorCode[error_code]
@ -316,17 +299,17 @@ def step_impl_type_mismatch(context, error_code):
'the flag\'s configuration with key "{key}" is updated to defaultVariant ' 'the flag\'s configuration with key "{key}" is updated to defaultVariant '
'"{variant}"' '"{variant}"'
) )
def step_impl_config_update(context, key, variant): def step_impl(context, key, variant):
raise NotImplementedError("Step definition not implemented yet") raise NotImplementedError("Step definition not implemented yet")
@given("sleep for {duration} milliseconds") @given("sleep for {duration} milliseconds")
def step_impl_sleep(context, duration): def step_impl(context, duration):
sleep(float(duration) * 0.001) raise NotImplementedError("Step definition not implemented yet")
@then('the resolved string details reason should be "{reason}"') @then('the resolved string details reason should be "{reason}"')
def step_impl_reason_should_be(context, reason): def step_impl(context, reason):
raise NotImplementedError("Step definition not implemented yet") raise NotImplementedError("Step definition not implemented yet")

View File

@ -137,17 +137,12 @@ def test_after_hooks_run_after_method(mock_hook):
def test_finally_after_hooks_run_finally_after_method(mock_hook): def test_finally_after_hooks_run_finally_after_method(mock_hook):
# Given # Given
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, "") hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, "")
flag_evaluation_details = FlagEvaluationDetails(
hook_context.flag_key, "val", "unknown"
)
hook_hints = MappingProxyType({}) hook_hints = MappingProxyType({})
# When # When
after_all_hooks( after_all_hooks(FlagType.BOOLEAN, hook_context, [mock_hook], hook_hints)
FlagType.BOOLEAN, hook_context, flag_evaluation_details, [mock_hook], hook_hints
)
# Then # Then
mock_hook.supports_flag_value_type.assert_called_once() mock_hook.supports_flag_value_type.assert_called_once()
mock_hook.finally_after.assert_called_once() mock_hook.finally_after.assert_called_once()
mock_hook.finally_after.assert_called_with( mock_hook.finally_after.assert_called_with(
hook_context=hook_context, details=flag_evaluation_details, hints=hook_hints hook_context=hook_context, hints=hook_hints
) )

View File

@ -2,7 +2,7 @@ from numbers import Number
import pytest import pytest
from openfeature.exception import ErrorCode from openfeature.exception import FlagNotFoundError
from openfeature.flag_evaluation import FlagResolutionDetails, Reason from openfeature.flag_evaluation import FlagResolutionDetails, Reason
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
@ -17,27 +17,16 @@ def test_should_return_in_memory_provider_metadata():
assert metadata.name == "In-Memory Provider" assert metadata.name == "In-Memory Provider"
@pytest.mark.asyncio def test_should_handle_unknown_flags_correctly():
async def test_should_handle_unknown_flags_correctly():
# Given # Given
provider = InMemoryProvider({}) provider = InMemoryProvider({})
# When # When
flag_sync = provider.resolve_boolean_details(flag_key="Key", default_value=True) with pytest.raises(FlagNotFoundError):
flag_async = await provider.resolve_boolean_details_async( provider.resolve_boolean_details(flag_key="Key", default_value=True)
flag_key="Key", default_value=True
)
# Then # Then
assert flag_sync == flag_async
for flag in [flag_sync, flag_async]:
assert flag is not None
assert flag.value is True
assert flag.reason == Reason.ERROR
assert flag.error_code == ErrorCode.FLAG_NOT_FOUND
assert flag.error_message == "Flag 'Key' not found"
@pytest.mark.asyncio def test_calls_context_evaluator_if_present():
async def test_calls_context_evaluator_if_present():
# Given # Given
def context_evaluator(flag: InMemoryFlag, evaluation_context: dict): def context_evaluator(flag: InMemoryFlag, evaluation_context: dict):
return FlagResolutionDetails( return FlagResolutionDetails(
@ -55,81 +44,57 @@ async def test_calls_context_evaluator_if_present():
} }
) )
# When # When
flag_sync = provider.resolve_boolean_details(flag_key="Key", default_value=False) flag = provider.resolve_boolean_details(flag_key="Key", default_value=False)
flag_async = await provider.resolve_boolean_details_async(
flag_key="Key", default_value=False
)
# Then # Then
assert flag_sync == flag_async assert flag is not None
for flag in [flag_sync, flag_async]: assert flag.value is False
assert flag is not None assert isinstance(flag.value, bool)
assert flag.value is False assert flag.reason == Reason.TARGETING_MATCH
assert isinstance(flag.value, bool)
assert flag.reason == Reason.TARGETING_MATCH
@pytest.mark.asyncio def test_should_resolve_boolean_flag_from_in_memory():
async def test_should_resolve_boolean_flag_from_in_memory():
# Given # Given
provider = InMemoryProvider( provider = InMemoryProvider(
{"Key": InMemoryFlag("true", {"true": True, "false": False})} {"Key": InMemoryFlag("true", {"true": True, "false": False})}
) )
# When # When
flag_sync = provider.resolve_boolean_details(flag_key="Key", default_value=False) flag = provider.resolve_boolean_details(flag_key="Key", default_value=False)
flag_async = await provider.resolve_boolean_details_async(
flag_key="Key", default_value=False
)
# Then # Then
assert flag_sync == flag_async assert flag is not None
for flag in [flag_sync, flag_async]: assert flag.value is True
assert flag is not None assert isinstance(flag.value, bool)
assert flag.value is True assert flag.variant == "true"
assert isinstance(flag.value, bool)
assert flag.variant == "true"
@pytest.mark.asyncio def test_should_resolve_integer_flag_from_in_memory():
async def test_should_resolve_integer_flag_from_in_memory():
# Given # Given
provider = InMemoryProvider( provider = InMemoryProvider(
{"Key": InMemoryFlag("hundred", {"zero": 0, "hundred": 100})} {"Key": InMemoryFlag("hundred", {"zero": 0, "hundred": 100})}
) )
# When # When
flag_sync = provider.resolve_integer_details(flag_key="Key", default_value=0) flag = provider.resolve_integer_details(flag_key="Key", default_value=0)
flag_async = await provider.resolve_integer_details_async(
flag_key="Key", default_value=0
)
# Then # Then
assert flag_sync == flag_async assert flag is not None
for flag in [flag_sync, flag_async]: assert flag.value == 100
assert flag is not None assert isinstance(flag.value, Number)
assert flag.value == 100 assert flag.variant == "hundred"
assert isinstance(flag.value, Number)
assert flag.variant == "hundred"
@pytest.mark.asyncio def test_should_resolve_float_flag_from_in_memory():
async def test_should_resolve_float_flag_from_in_memory():
# Given # Given
provider = InMemoryProvider( provider = InMemoryProvider(
{"Key": InMemoryFlag("ten", {"zero": 0.0, "ten": 10.23})} {"Key": InMemoryFlag("ten", {"zero": 0.0, "ten": 10.23})}
) )
# When # When
flag_sync = provider.resolve_float_details(flag_key="Key", default_value=0.0) flag = provider.resolve_float_details(flag_key="Key", default_value=0.0)
flag_async = await provider.resolve_float_details_async(
flag_key="Key", default_value=0.0
)
# Then # Then
assert flag_sync == flag_async assert flag is not None
for flag in [flag_sync, flag_async]: assert flag.value == 10.23
assert flag is not None assert isinstance(flag.value, Number)
assert flag.value == 10.23 assert flag.variant == "ten"
assert isinstance(flag.value, Number)
assert flag.variant == "ten"
@pytest.mark.asyncio def test_should_resolve_string_flag_from_in_memory():
async def test_should_resolve_string_flag_from_in_memory():
# Given # Given
provider = InMemoryProvider( provider = InMemoryProvider(
{ {
@ -140,41 +105,29 @@ async def test_should_resolve_string_flag_from_in_memory():
} }
) )
# When # When
flag_sync = provider.resolve_string_details(flag_key="Key", default_value="Default") flag = provider.resolve_string_details(flag_key="Key", default_value="Default")
flag_async = await provider.resolve_string_details_async(
flag_key="Key", default_value="Default"
)
# Then # Then
assert flag_sync == flag_async assert flag is not None
for flag in [flag_sync, flag_async]: assert flag.value == "String"
assert flag is not None assert isinstance(flag.value, str)
assert flag.value == "String" assert flag.variant == "stringVariant"
assert isinstance(flag.value, str)
assert flag.variant == "stringVariant"
@pytest.mark.asyncio def test_should_resolve_list_flag_from_in_memory():
async def test_should_resolve_list_flag_from_in_memory():
# Given # Given
provider = InMemoryProvider( provider = InMemoryProvider(
{"Key": InMemoryFlag("twoItems", {"empty": [], "twoItems": ["item1", "item2"]})} {"Key": InMemoryFlag("twoItems", {"empty": [], "twoItems": ["item1", "item2"]})}
) )
# When # When
flag_sync = provider.resolve_object_details(flag_key="Key", default_value=[]) flag = provider.resolve_object_details(flag_key="Key", default_value=[])
flag_async = await provider.resolve_object_details_async(
flag_key="Key", default_value=[]
)
# Then # Then
assert flag_sync == flag_async assert flag is not None
for flag in [flag_sync, flag_async]: assert flag.value == ["item1", "item2"]
assert flag is not None assert isinstance(flag.value, list)
assert flag.value == ["item1", "item2"] assert flag.variant == "twoItems"
assert isinstance(flag.value, list)
assert flag.variant == "twoItems"
@pytest.mark.asyncio def test_should_resolve_object_flag_from_in_memory():
async def test_should_resolve_object_flag_from_in_memory():
# Given # Given
return_value = { return_value = {
"String": "string", "String": "string",
@ -185,12 +138,9 @@ async def test_should_resolve_object_flag_from_in_memory():
{"Key": InMemoryFlag("obj", {"obj": return_value, "empty": {}})} {"Key": InMemoryFlag("obj", {"obj": return_value, "empty": {}})}
) )
# When # When
flag_sync = provider.resolve_object_details(flag_key="Key", default_value={}) flag = provider.resolve_object_details(flag_key="Key", default_value={})
flag_async = provider.resolve_object_details(flag_key="Key", default_value={})
# Then # Then
assert flag_sync == flag_async assert flag is not None
for flag in [flag_sync, flag_async]: assert flag.value == return_value
assert flag is not None assert isinstance(flag.value, dict)
assert flag.value == return_value assert flag.variant == "obj"
assert isinstance(flag.value, dict)
assert flag.variant == "obj"

View File

@ -1,197 +0,0 @@
from typing import Optional, Union
import pytest
from openfeature.api import get_client, set_provider
from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagResolutionDetails
from openfeature.provider import AbstractProvider, Metadata
class SynchronousProvider(AbstractProvider):
def get_metadata(self):
return Metadata(name="SynchronousProvider")
def get_provider_hooks(self):
return []
def resolve_boolean_details(
self,
flag_key: str,
default_value: bool,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
return FlagResolutionDetails(value=True)
def resolve_string_details(
self,
flag_key: str,
default_value: str,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]:
return FlagResolutionDetails(value="string")
def resolve_integer_details(
self,
flag_key: str,
default_value: int,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]:
return FlagResolutionDetails(value=1)
def resolve_float_details(
self,
flag_key: str,
default_value: float,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]:
return FlagResolutionDetails(value=10.0)
def resolve_object_details(
self,
flag_key: str,
default_value: Union[dict, list],
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[Union[dict, list]]:
return FlagResolutionDetails(value={"key": "value"})
@pytest.mark.parametrize(
"flag_type, default_value, get_method",
(
(bool, True, "get_boolean_value_async"),
(str, "string", "get_string_value_async"),
(int, 1, "get_integer_value_async"),
(float, 10.0, "get_float_value_async"),
(
dict,
{"key": "value"},
"get_object_value_async",
),
),
)
@pytest.mark.asyncio
async def test_sync_provider_can_be_called_async(flag_type, default_value, get_method):
# Given
set_provider(SynchronousProvider(), "SynchronousProvider")
client = get_client("SynchronousProvider")
# When
async_callable = getattr(client, get_method)
flag = await async_callable(flag_key="Key", default_value=default_value)
# Then
assert flag is not None
assert flag == default_value
assert isinstance(flag, flag_type)
@pytest.mark.asyncio
async def test_sync_provider_can_be_extended_async():
# Given
class ExtendedAsyncProvider(SynchronousProvider):
async def resolve_boolean_details_async(
self,
flag_key: str,
default_value: bool,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
return FlagResolutionDetails(value=False)
set_provider(ExtendedAsyncProvider(), "ExtendedAsyncProvider")
client = get_client("ExtendedAsyncProvider")
# When
flag = await client.get_boolean_value_async(flag_key="Key", default_value=True)
# Then
assert flag is not None
assert flag is False
# We're not allowing providers to only have async methods
def test_sync_methods_enforced_for_async_providers():
# Given
class AsyncProvider(AbstractProvider):
def get_metadata(self):
return Metadata(name="AsyncProvider")
async def resolve_boolean_details_async(
self,
flag_key: str,
default_value: bool,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
return FlagResolutionDetails(value=True)
# When
with pytest.raises(TypeError) as exception:
set_provider(AsyncProvider(), "AsyncProvider")
# Then
# assert
exception_message = str(exception.value)
assert exception_message.startswith(
"Can't instantiate abstract class AsyncProvider"
)
assert exception_message.__contains__("resolve_boolean_details")
@pytest.mark.asyncio
async def test_async_provider_not_implemented_exception_workaround():
# Given
class SyncNotImplementedProvider(AbstractProvider):
def get_metadata(self):
return Metadata(name="AsyncProvider")
async def resolve_boolean_details_async(
self,
flag_key: str,
default_value: bool,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
return FlagResolutionDetails(value=True)
def resolve_boolean_details(
self,
flag_key: str,
default_value: bool,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
raise NotImplementedError("Use the async method")
def resolve_string_details(
self,
flag_key: str,
default_value: str,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]:
raise NotImplementedError("Use the async method")
def resolve_integer_details(
self,
flag_key: str,
default_value: int,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]:
raise NotImplementedError("Use the async method")
def resolve_float_details(
self,
flag_key: str,
default_value: float,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]:
raise NotImplementedError("Use the async method")
def resolve_object_details(
self,
flag_key: str,
default_value: Union[dict, list],
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[Union[dict, list]]:
raise NotImplementedError("Use the async method")
# When
set_provider(SyncNotImplementedProvider(), "SyncNotImplementedProvider")
client = get_client("SyncNotImplementedProvider")
flag = await client.get_boolean_value_async(flag_key="Key", default_value=False)
# Then
assert flag is not None
assert flag is True

View File

@ -1,101 +0,0 @@
from openfeature.evaluation_context import EvaluationContext
from openfeature.exception import ErrorCode
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType, Reason
from openfeature.hook import HookContext
from openfeature.provider import Metadata
from openfeature.telemetry import (
TelemetryAttribute,
TelemetryBodyField,
TelemetryFlagMetadata,
create_evaluation_event,
)
def test_create_evaluation_event():
# given
hook_context = HookContext(
flag_key="flag_key",
flag_type=FlagType.BOOLEAN,
default_value=True,
evaluation_context=EvaluationContext(),
provider_metadata=Metadata(name="test_provider"),
)
details = FlagEvaluationDetails(
flag_key=hook_context.flag_key,
value=False,
reason=Reason.CACHED,
)
# when
event = create_evaluation_event(hook_context=hook_context, details=details)
# then
assert event.name == "feature_flag.evaluation"
assert event.attributes[TelemetryAttribute.KEY] == "flag_key"
assert event.attributes[TelemetryAttribute.EVALUATION_REASON] == "cached"
assert event.attributes[TelemetryAttribute.PROVIDER_NAME] == "test_provider"
assert event.body[TelemetryBodyField.VALUE] is False
def test_create_evaluation_event_with_variant():
# given
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, EvaluationContext())
details = FlagEvaluationDetails(
flag_key=hook_context.flag_key,
value=True,
variant="true",
)
# when
event = create_evaluation_event(hook_context=hook_context, details=details)
# then
assert event.name == "feature_flag.evaluation"
assert event.attributes[TelemetryAttribute.KEY] == "flag_key"
assert event.attributes[TelemetryAttribute.VARIANT] == "true"
assert event.attributes[TelemetryAttribute.EVALUATION_REASON] == "unknown"
def test_create_evaluation_event_with_metadata():
# given
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, EvaluationContext())
details = FlagEvaluationDetails(
flag_key=hook_context.flag_key,
value=False,
flag_metadata={
TelemetryFlagMetadata.CONTEXT_ID: "5157782b-2203-4c80-a857-dbbd5e7761db",
TelemetryFlagMetadata.FLAG_SET_ID: "proj-1",
TelemetryFlagMetadata.VERSION: "v1",
},
)
# when
event = create_evaluation_event(hook_context=hook_context, details=details)
# then
assert (
event.attributes[TelemetryAttribute.CONTEXT_ID]
== "5157782b-2203-4c80-a857-dbbd5e7761db"
)
assert event.attributes[TelemetryAttribute.SET_ID] == "proj-1"
assert event.attributes[TelemetryAttribute.VERSION] == "v1"
def test_create_evaluation_event_with_error():
# given
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, EvaluationContext())
details = FlagEvaluationDetails(
flag_key=hook_context.flag_key,
value=False,
reason=Reason.ERROR,
error_code=ErrorCode.FLAG_NOT_FOUND,
error_message="flag error",
)
# when
event = create_evaluation_event(hook_context=hook_context, details=details)
# then
assert event.attributes[TelemetryAttribute.EVALUATION_REASON] == "error"
assert event.attributes[TelemetryAttribute.ERROR_TYPE] == "flag_not_found"
assert event.attributes[TelemetryAttribute.EVALUATION_ERROR_MESSAGE] == "flag error"

View File

@ -1,4 +1,3 @@
import inspect
import time import time
import uuid import uuid
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@ -8,11 +7,11 @@ import pytest
from openfeature import api from openfeature import api
from openfeature.api import add_hooks, clear_hooks, get_client, set_provider from openfeature.api import add_hooks, clear_hooks, get_client, set_provider
from openfeature.client import OpenFeatureClient, _typecheck_flag_value from openfeature.client import OpenFeatureClient
from openfeature.evaluation_context import EvaluationContext from openfeature.evaluation_context import EvaluationContext
from openfeature.event import EventDetails, ProviderEvent, ProviderEventDetails from openfeature.event import EventDetails, ProviderEvent, ProviderEventDetails
from openfeature.exception import ErrorCode, OpenFeatureError from openfeature.exception import ErrorCode, OpenFeatureError
from openfeature.flag_evaluation import FlagResolutionDetails, FlagType, Reason from openfeature.flag_evaluation import FlagResolutionDetails, Reason
from openfeature.hook import Hook from openfeature.hook import Hook
from openfeature.provider import FeatureProvider, ProviderStatus from openfeature.provider import FeatureProvider, ProviderStatus
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
@ -24,13 +23,9 @@ from openfeature.transaction_context import ContextVarsTransactionContextPropaga
"flag_type, default_value, get_method", "flag_type, default_value, get_method",
( (
(bool, True, "get_boolean_value"), (bool, True, "get_boolean_value"),
(bool, True, "get_boolean_value_async"),
(str, "String", "get_string_value"), (str, "String", "get_string_value"),
(str, "String", "get_string_value_async"),
(int, 100, "get_integer_value"), (int, 100, "get_integer_value"),
(int, 100, "get_integer_value_async"),
(float, 10.23, "get_float_value"), (float, 10.23, "get_float_value"),
(float, 10.23, "get_float_value_async"),
( (
dict, dict,
{ {
@ -40,38 +35,21 @@ from openfeature.transaction_context import ContextVarsTransactionContextPropaga
}, },
"get_object_value", "get_object_value",
), ),
(
dict,
{
"String": "string",
"Number": 2,
"Boolean": True,
},
"get_object_value_async",
),
( (
list, list,
["string1", "string2"], ["string1", "string2"],
"get_object_value", "get_object_value",
), ),
(
list,
["string1", "string2"],
"get_object_value_async",
),
), ),
) )
@pytest.mark.asyncio def test_should_get_flag_value_based_on_method_type(
async def test_should_get_flag_value_based_on_method_type(
flag_type, default_value, get_method, no_op_provider_client flag_type, default_value, get_method, no_op_provider_client
): ):
# Given # Given
# When # When
method = getattr(no_op_provider_client, get_method) flag = getattr(no_op_provider_client, get_method)(
if inspect.iscoroutinefunction(method): flag_key="Key", default_value=default_value
flag = await method(flag_key="Key", default_value=default_value) )
else:
flag = method(flag_key="Key", default_value=default_value)
# Then # Then
assert flag is not None assert flag is not None
assert flag == default_value assert flag == default_value
@ -82,13 +60,9 @@ async def test_should_get_flag_value_based_on_method_type(
"flag_type, default_value, get_method", "flag_type, default_value, get_method",
( (
(bool, True, "get_boolean_details"), (bool, True, "get_boolean_details"),
(bool, True, "get_boolean_details_async"),
(str, "String", "get_string_details"), (str, "String", "get_string_details"),
(str, "String", "get_string_details_async"),
(int, 100, "get_integer_details"), (int, 100, "get_integer_details"),
(int, 100, "get_integer_details_async"),
(float, 10.23, "get_float_details"), (float, 10.23, "get_float_details"),
(float, 10.23, "get_float_details_async"),
( (
dict, dict,
{ {
@ -98,62 +72,38 @@ async def test_should_get_flag_value_based_on_method_type(
}, },
"get_object_details", "get_object_details",
), ),
(
dict,
{
"String": "string",
"Number": 2,
"Boolean": True,
},
"get_object_details_async",
),
( (
list, list,
["string1", "string2"], ["string1", "string2"],
"get_object_details", "get_object_details",
), ),
(
list,
["string1", "string2"],
"get_object_details_async",
),
), ),
) )
@pytest.mark.asyncio def test_should_get_flag_detail_based_on_method_type(
async def test_should_get_flag_detail_based_on_method_type(
flag_type, default_value, get_method, no_op_provider_client flag_type, default_value, get_method, no_op_provider_client
): ):
# Given # Given
# When # When
method = getattr(no_op_provider_client, get_method) flag = getattr(no_op_provider_client, get_method)(
if inspect.iscoroutinefunction(method): flag_key="Key", default_value=default_value
flag = await method(flag_key="Key", default_value=default_value) )
else:
flag = method(flag_key="Key", default_value=default_value)
# Then # Then
assert flag is not None assert flag is not None
assert flag.value == default_value assert flag.value == default_value
assert isinstance(flag.value, flag_type) assert isinstance(flag.value, flag_type)
@pytest.mark.asyncio def test_should_raise_exception_when_invalid_flag_type_provided(no_op_provider_client):
async def test_should_raise_exception_when_invalid_flag_type_provided(
no_op_provider_client,
):
# Given # Given
# When # When
flag_sync = no_op_provider_client.evaluate_flag_details( flag = no_op_provider_client.evaluate_flag_details(
flag_type=None, flag_key="Key", default_value=True
)
flag_async = await no_op_provider_client.evaluate_flag_details_async(
flag_type=None, flag_key="Key", default_value=True flag_type=None, flag_key="Key", default_value=True
) )
# Then # Then
for flag in [flag_sync, flag_async]: assert flag.value
assert flag.value assert flag.error_message == "Unknown flag type"
assert flag.error_message == "Unknown flag type" assert flag.error_code == ErrorCode.GENERAL
assert flag.error_code == ErrorCode.GENERAL assert flag.reason == Reason.ERROR
assert flag.reason == Reason.ERROR
def test_should_pass_flag_metadata_from_resolution_to_evaluation_details(): def test_should_pass_flag_metadata_from_resolution_to_evaluation_details():
@ -252,8 +202,7 @@ def test_should_define_a_provider_status_accessor(no_op_provider_client):
# Requirement 1.7.6 # Requirement 1.7.6
@pytest.mark.asyncio def test_should_shortcircuit_if_provider_is_not_ready(
async def test_should_shortcircuit_if_provider_is_not_ready(
no_op_provider_client, monkeypatch no_op_provider_client, monkeypatch
): ):
# Given # Given
@ -263,27 +212,19 @@ async def test_should_shortcircuit_if_provider_is_not_ready(
spy_hook = MagicMock(spec=Hook) spy_hook = MagicMock(spec=Hook)
no_op_provider_client.add_hooks([spy_hook]) no_op_provider_client.add_hooks([spy_hook])
# When # When
flag_details_sync = no_op_provider_client.get_boolean_details( flag_details = no_op_provider_client.get_boolean_details(
flag_key="Key", default_value=True
)
spy_hook.error.assert_called_once()
spy_hook.reset_mock()
flag_details_async = await no_op_provider_client.get_boolean_details_async(
flag_key="Key", default_value=True flag_key="Key", default_value=True
) )
# Then # Then
for flag_details in [flag_details_sync, flag_details_async]: assert flag_details is not None
assert flag_details is not None assert flag_details.value
assert flag_details.value assert flag_details.reason == Reason.ERROR
assert flag_details.reason == Reason.ERROR assert flag_details.error_code == ErrorCode.PROVIDER_NOT_READY
assert flag_details.error_code == ErrorCode.PROVIDER_NOT_READY
spy_hook.error.assert_called_once() spy_hook.error.assert_called_once()
spy_hook.finally_after.assert_called_once()
# Requirement 1.7.7 # Requirement 1.7.7
@pytest.mark.asyncio def test_should_shortcircuit_if_provider_is_in_irrecoverable_error_state(
async def test_should_shortcircuit_if_provider_is_in_irrecoverable_error_state(
no_op_provider_client, monkeypatch no_op_provider_client, monkeypatch
): ):
# Given # Given
@ -293,98 +234,39 @@ async def test_should_shortcircuit_if_provider_is_in_irrecoverable_error_state(
spy_hook = MagicMock(spec=Hook) spy_hook = MagicMock(spec=Hook)
no_op_provider_client.add_hooks([spy_hook]) no_op_provider_client.add_hooks([spy_hook])
# When # When
flag_details_sync = no_op_provider_client.get_boolean_details( flag_details = no_op_provider_client.get_boolean_details(
flag_key="Key", default_value=True
)
spy_hook.error.assert_called_once()
spy_hook.reset_mock()
flag_details_async = await no_op_provider_client.get_boolean_details_async(
flag_key="Key", default_value=True flag_key="Key", default_value=True
) )
# Then # Then
for flag_details in [flag_details_sync, flag_details_async]: assert flag_details is not None
assert flag_details is not None assert flag_details.value
assert flag_details.value assert flag_details.reason == Reason.ERROR
assert flag_details.reason == Reason.ERROR assert flag_details.error_code == ErrorCode.PROVIDER_FATAL
assert flag_details.error_code == ErrorCode.PROVIDER_FATAL
spy_hook.error.assert_called_once() spy_hook.error.assert_called_once()
spy_hook.finally_after.assert_called_once()
@pytest.mark.asyncio def test_should_run_error_hooks_if_provider_returns_resolution_with_error_code():
async def test_should_run_error_hooks_if_provider_returns_resolution_with_error_code():
# Given # Given
spy_hook = MagicMock(spec=Hook) spy_hook = MagicMock(spec=Hook)
provider = MagicMock(spec=FeatureProvider) provider = MagicMock(spec=FeatureProvider)
provider.get_provider_hooks.return_value = [] provider.get_provider_hooks.return_value = []
mock_resolution = FlagResolutionDetails( provider.resolve_boolean_details.return_value = FlagResolutionDetails(
value=True, value=True,
reason=Reason.ERROR, reason=Reason.ERROR,
error_code=ErrorCode.PROVIDER_FATAL, error_code=ErrorCode.PROVIDER_FATAL,
error_message="This is an error message", error_message="This is an error message",
) )
provider.resolve_boolean_details.return_value = mock_resolution
provider.resolve_boolean_details_async.return_value = mock_resolution
set_provider(provider) set_provider(provider)
client = get_client() client = get_client()
client.add_hooks([spy_hook]) client.add_hooks([spy_hook])
# When # When
flag_details_sync = client.get_boolean_details(flag_key="Key", default_value=True) flag_details = client.get_boolean_details(flag_key="Key", default_value=True)
# Then
assert flag_details is not None
assert flag_details.value
assert flag_details.reason == Reason.ERROR
assert flag_details.error_code == ErrorCode.PROVIDER_FATAL
spy_hook.error.assert_called_once() spy_hook.error.assert_called_once()
spy_hook.reset_mock()
flag_details_async = await client.get_boolean_details_async(
flag_key="Key", default_value=True
)
# Then
for flag_details in [flag_details_sync, flag_details_async]:
assert flag_details is not None
assert flag_details.value
assert flag_details.reason == Reason.ERROR
assert flag_details.error_code == ErrorCode.PROVIDER_FATAL
spy_hook.error.assert_called_once()
@pytest.mark.asyncio
async def test_client_type_mismatch_exceptions():
# Given
client = get_client()
# When
flag_details_sync = client.get_boolean_details(
flag_key="Key", default_value="type mismatch"
)
flag_details_async = await client.get_boolean_details_async(
flag_key="Key", default_value="type mismatch"
)
# Then
for flag_details in [flag_details_sync, flag_details_async]:
assert flag_details is not None
assert flag_details.value
assert flag_details.reason == Reason.ERROR
assert flag_details.error_code == ErrorCode.TYPE_MISMATCH
@pytest.mark.asyncio
async def test_typecheck_flag_value_general_error():
# Given
flag_value = "A"
flag_type = None
# When
err = _typecheck_flag_value(value=flag_value, flag_type=flag_type)
# Then
assert err.error_code == ErrorCode.GENERAL
assert err.error_message == "Unknown flag type"
@pytest.mark.asyncio
async def test_typecheck_flag_value_type_mismatch_error():
# Given
flag_value = "A"
flag_type = FlagType.BOOLEAN
# When
err = _typecheck_flag_value(value=flag_value, flag_type=flag_type)
# Then
assert err.error_code == ErrorCode.TYPE_MISMATCH
assert err.error_message == "Expected type <class 'bool'> but got <class 'str'>"
def test_provider_events(): def test_provider_events():
@ -538,20 +420,12 @@ def test_client_should_merge_contexts():
invocation_context = EvaluationContext( invocation_context = EvaluationContext(
targeting_key="invocation", attributes={"invocation_attr": "invocation_value"} targeting_key="invocation", attributes={"invocation_attr": "invocation_value"}
) )
flag_input = "flag" client.get_boolean_details("flag", False, invocation_context)
flag_default = False
client.get_boolean_details(flag_input, flag_default, invocation_context)
# Retrieve the call arguments # Retrieve the call arguments
args, kwargs = provider.resolve_boolean_details.call_args args, kwargs = provider.resolve_boolean_details.call_args
flag_key, default_value, context = ( flag_key, default_value, context = args
kwargs["flag_key"],
kwargs["default_value"],
kwargs["evaluation_context"],
)
assert flag_key == flag_input
assert default_value is flag_default
assert context.targeting_key == "invocation" # Last one in the merge chain assert context.targeting_key == "invocation" # Last one in the merge chain
assert context.attributes["global_attr"] == "global_value" assert context.attributes["global_attr"] == "global_value"
assert context.attributes["transaction_attr"] == "transaction_value" assert context.attributes["transaction_attr"] == "transaction_value"

View File

@ -1,51 +0,0 @@
from openfeature.api import (
set_provider,
set_transaction_context,
set_transaction_context_propagator,
)
from openfeature.client import OpenFeatureClient
from openfeature.evaluation_context import EvaluationContext
from openfeature.hook import Hook
from openfeature.provider.no_op_provider import NoOpProvider
from openfeature.transaction_context import ContextVarsTransactionContextPropagator
class TransactionContextHook(Hook):
def __init__(self):
self.before_called = False
self.transaction_attr_value = None
def before(self, hook_context, hints):
self.before_called = True
# Check if the transaction context attribute is in the hook context
if "transaction_attr" in hook_context.evaluation_context.attributes:
self.transaction_attr_value = hook_context.evaluation_context.attributes[
"transaction_attr"
]
return None
def test_transaction_context_merged_into_hook_context():
"""Test that transaction context is merged into the hook context's evaluation context."""
set_transaction_context_propagator(ContextVarsTransactionContextPropagator())
provider = NoOpProvider()
set_provider(provider)
client = OpenFeatureClient(domain=None, version=None)
hook = TransactionContextHook()
client.add_hooks([hook])
transaction_context = EvaluationContext(
targeting_key="transaction",
attributes={"transaction_attr": "transaction_value"},
)
set_transaction_context(transaction_context)
client.get_boolean_value(flag_key="test-flag", default_value=False)
assert hook.before_called, "Hook's before method was not called"
assert hook.transaction_attr_value == "transaction_value", (
"Transaction context attribute was not found in hook context"
)

456
uv.lock
View File

@ -1,456 +0,0 @@
version = 1
revision = 2
requires-python = ">=3.9"
[[package]]
name = "backports-asyncio-runner"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
]
[[package]]
name = "behave"
version = "1.2.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "parse" },
{ name = "parse-type" },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c8/4b/d0a8c23b6c8985e5544ea96d27105a273ea22051317f850c2cdbf2029fe4/behave-1.2.6.tar.gz", hash = "sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86", size = 701696, upload-time = "2018-02-25T20:06:38.851Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/6c/ec9169548b6c4cb877aaa6773408ca08ae2a282805b958dbc163cb19822d/behave-1.2.6-py2.py3-none-any.whl", hash = "sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c", size = 136779, upload-time = "2018-02-25T20:06:34.436Z" },
]
[[package]]
name = "cfgv"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coverage"
version = "7.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/8f/6ac7fbb29e35645065f7be835bfe3e0cce567f80390de2f3db65d83cb5e3/coverage-7.10.0.tar.gz", hash = "sha256:2768885aef484b5dcde56262cbdfba559b770bfc46994fe9485dc3614c7a5867", size = 819816, upload-time = "2025-07-24T16:53:00.896Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/f6/b2366476b1f48134757f2a42aaf00e7ce8e734eea5f3cf022df113116174/coverage-7.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbd823f7ea5286c26406ad9e54268544d82f3d1cadb6d4f3b85e9877f0cab1ef", size = 214813, upload-time = "2025-07-24T16:50:18.937Z" },
{ url = "https://files.pythonhosted.org/packages/19/d1/7e26bb4c41ed1b9aca4550187ca42557d79c70d318414a703d814858eacb/coverage-7.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab3f7a5dbaab937df0b9e9e8ec6eab235ba9a6f29d71fd3b24335affaed886cc", size = 215206, upload-time = "2025-07-24T16:50:21.788Z" },
{ url = "https://files.pythonhosted.org/packages/df/71/d5ae128557c8d0ce0156eb1e980e5c6e6f7e54ef3e998c87ab4b3679ff45/coverage-7.10.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8c63aaf850523d8cbe3f5f1a5c78f689b223797bef902635f2493ab43498f36c", size = 242171, upload-time = "2025-07-24T16:50:23.483Z" },
{ url = "https://files.pythonhosted.org/packages/af/87/d586a627e3b61cfe631ebcf3d8a38bf9085142800d2ac434bc20f3699880/coverage-7.10.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c3133ce3fa84023f7c6921c4dca711be0b658784c5a51a797168229eae26172", size = 243431, upload-time = "2025-07-24T16:50:24.913Z" },
{ url = "https://files.pythonhosted.org/packages/55/cc/ff5c6f4f99a987ebd18a3350194377c7cefee9ddd6e532ede83a0a1f332c/coverage-7.10.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3747d1d0af85b17d3a156cd30e4bbacf893815e846dc6c07050e9769da2b138e", size = 245288, upload-time = "2025-07-24T16:50:26.673Z" },
{ url = "https://files.pythonhosted.org/packages/94/d9/2758e73d7fe496c04dd715af8bb8856354a1ad4cc11553d9096c4b35dc86/coverage-7.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:241923b350437f6a7cb343d9df72998305ef940c3c40009f06e05029a047677c", size = 243235, upload-time = "2025-07-24T16:50:28.505Z" },
{ url = "https://files.pythonhosted.org/packages/9c/9b/3c273dde651d83484992d7e7bcd9cd84a363f01026caf69716390bd79e0d/coverage-7.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13e82e499309307104d58ac66f9eed237f7aaceab4325416645be34064d9a2be", size = 241909, upload-time = "2025-07-24T16:50:30.38Z" },
{ url = "https://files.pythonhosted.org/packages/5b/7c/006d9f66035c4d414ea642d990854a30c23145551315bd0b38100daee168/coverage-7.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bf73cdde4f6c9cd4457b00bf1696236796ac3a241f859a55e0f84a4c58326a7f", size = 242202, upload-time = "2025-07-24T16:50:32.199Z" },
{ url = "https://files.pythonhosted.org/packages/8a/42/80d8747f77c63593a2114c7299df52f7568168e4fd882d7d5ebe8181564f/coverage-7.10.0-cp310-cp310-win32.whl", hash = "sha256:2396e13275b37870a3345f58bce8b15a7e0a985771d13a4b16ce9129954e07d6", size = 217311, upload-time = "2025-07-24T16:50:33.598Z" },
{ url = "https://files.pythonhosted.org/packages/e3/8b/fe04c3851e5d290524f563a8a564c7e5dcd6b5ca35ed689ce662346de230/coverage-7.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d45c7c71fb3d2da92ab893602e3f28f2d1560cec765a27e1824a6e0f7e92cfd", size = 218199, upload-time = "2025-07-24T16:50:36.751Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5d/0d1ee021439e3b8b1e86ba92465f5a8d8e15b0222dcdd705606ef089f4fe/coverage-7.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4abc01843581a6f9dd72d4d15761861190973a2305416639435ef509288f7a04", size = 214934, upload-time = "2025-07-24T16:50:38.173Z" },
{ url = "https://files.pythonhosted.org/packages/f2/b2/1e0727327e473aa1a68ca1c9922818a06061d05d44e0c5330109d091b525/coverage-7.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2093297773111d7d748fe4a99b68747e57994531fb5c57bbe439af17c11c169", size = 215320, upload-time = "2025-07-24T16:50:39.617Z" },
{ url = "https://files.pythonhosted.org/packages/84/17/d231e37236863ae3bed7c51615af6b6fc89639c88adf35766d2880dcd7c7/coverage-7.10.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58240e27815bf105bd975c2fd42e700839f93d5aad034ef976411193ca32dbfd", size = 245321, upload-time = "2025-07-24T16:50:41.544Z" },
{ url = "https://files.pythonhosted.org/packages/95/77/a285aba35bf6ec12c466474931410ef0e6fa85542169009443868e98820a/coverage-7.10.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d019eac999b40ad48521ea057958b07a9f549c0c6d257a20e5c7c4ba91af8d1c", size = 247155, upload-time = "2025-07-24T16:50:43.358Z" },
{ url = "https://files.pythonhosted.org/packages/7b/82/50512eafdd5938a7aa1550014e37fa1c2ca85516bfd85ffeb2f03eff052a/coverage-7.10.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35e0a1f5454bc80faf4ceab10d1d48f025f92046c9c0f3bec2e1a9dda55137f8", size = 249320, upload-time = "2025-07-24T16:50:44.98Z" },
{ url = "https://files.pythonhosted.org/packages/de/7b/0ec1dc75c8f4d940d03d477b1e07269b4804dcab74ad1e294d40310aba47/coverage-7.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a93dd7759c416dd1cc754123b926d065055cb9a33b6699e64a1e5bdfae1ff459", size = 247047, upload-time = "2025-07-24T16:50:46.482Z" },
{ url = "https://files.pythonhosted.org/packages/d9/5b/40f9b78ae98c2f511a2b062660906e126aadcd35870b9190a4f10f2820ae/coverage-7.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7b3d737266048368a6ffd68f1ecd662c54de56535c82eb8f98a55ac216a72cbd", size = 245078, upload-time = "2025-07-24T16:50:47.904Z" },
{ url = "https://files.pythonhosted.org/packages/d6/f6/672c2a728e77846be7fcc4baaa003e0df86a2174aeb8921d132c14c333d4/coverage-7.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:93227c2707cb0effd9163cd0d8f0d9ab628982f7a3e915d6d64c7107867b9a07", size = 245686, upload-time = "2025-07-24T16:50:49.461Z" },
{ url = "https://files.pythonhosted.org/packages/a1/f3/fa078f0bfae7f0e6b14c426f9cb095f4809314d926c89b9a2641fb4ca482/coverage-7.10.0-cp311-cp311-win32.whl", hash = "sha256:69270af3014ab3058ad6108c6d0e218166f568b5a7a070dc3d62c0a63aca1c4d", size = 217350, upload-time = "2025-07-24T16:50:50.884Z" },
{ url = "https://files.pythonhosted.org/packages/1a/40/eefc3ebb9e458e3dc5db00e6b838969375577a09a8a39986d79cfa283175/coverage-7.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c16bbb661a7b4dafac0ab69e44d6dbcc6a64c4d93aefd89edc6f8911b6ab4a", size = 218235, upload-time = "2025-07-24T16:50:52.369Z" },
{ url = "https://files.pythonhosted.org/packages/e5/b8/3b53890c3ad52279eaea594a86bceaf04fcc0aed16856ff81531f75735f4/coverage-7.10.0-cp311-cp311-win_arm64.whl", hash = "sha256:14e7c23fcb74ed808efb4eb48fcd25a759f0e20f685f83266d1df174860e4733", size = 216668, upload-time = "2025-07-24T16:50:53.937Z" },
{ url = "https://files.pythonhosted.org/packages/b6/b4/7b419bb368c9f0b88889cb24805164f6e5550d7183fb59524f6173e0cf0b/coverage-7.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a2adcfdaf3b4d69b0c64ad024fe9dd6996782b52790fb6033d90f36f39e287df", size = 215124, upload-time = "2025-07-24T16:50:55.46Z" },
{ url = "https://files.pythonhosted.org/packages/f4/15/d862a806734c7e50fd5350cef18e22832ba3cdad282ca5660d6fd49def92/coverage-7.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d7b27c2c0840e8eeff3f1963782bd9d3bc767488d2e67a31de18d724327f9f6", size = 215364, upload-time = "2025-07-24T16:50:57.849Z" },
{ url = "https://files.pythonhosted.org/packages/a6/93/4671ca5b2f3650c961a01252cbad96cb41f7c0c2b85c6062f27740a66b06/coverage-7.10.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0ed50429786e935517570b08576a661fd79032e6060985ab492b9d39ba8e66ee", size = 246369, upload-time = "2025-07-24T16:50:59.505Z" },
{ url = "https://files.pythonhosted.org/packages/64/79/2ca676c712d0540df0d7957a4266232980b60858a7a654846af1878cfde0/coverage-7.10.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7171c139ab6571d70460ecf788b1dcaf376bfc75a42e1946b8c031d062bbbad4", size = 248798, upload-time = "2025-07-24T16:51:01.105Z" },
{ url = "https://files.pythonhosted.org/packages/82/c5/67e000b03ba5291f915ddd6ba7c3333e4fdee9ba003b914c8f8f2d966dfe/coverage-7.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a726aac7e6e406e403cdee4c443a13aed3ea3d67d856414c5beacac2e70c04e", size = 250260, upload-time = "2025-07-24T16:51:02.761Z" },
{ url = "https://files.pythonhosted.org/packages/9d/76/196783c425b5633db5c789b02a023858377bd73e4db4c805c2503cc42bbf/coverage-7.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2886257481a14e953e96861a00c0fe7151117a523f0470a51e392f00640bba03", size = 248171, upload-time = "2025-07-24T16:51:04.651Z" },
{ url = "https://files.pythonhosted.org/packages/83/1f/bf86c75f42de3641b4bbeab9712ec2815a3a8f5939768077245a492fad9f/coverage-7.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:536578b79521e59c385a2e0a14a5dc2a8edd58761a966d79368413e339fc9535", size = 246368, upload-time = "2025-07-24T16:51:06.16Z" },
{ url = "https://files.pythonhosted.org/packages/2d/95/bfc9a3abef0b160404438e82ec778a0f38660c66a4b0ed94d0417d4d2290/coverage-7.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77fae95558f7804a9ceefabf3c38ad41af1da92b39781b87197c6440dcaaa967", size = 247578, upload-time = "2025-07-24T16:51:07.632Z" },
{ url = "https://files.pythonhosted.org/packages/c6/7e/4fb2a284d56fe2a3ba0c76806923014854a64e503dc8ce21e5a2e6497eea/coverage-7.10.0-cp312-cp312-win32.whl", hash = "sha256:97803e14736493eb029558e1502fe507bd6a08af277a5c8eeccf05c3e970cb84", size = 217521, upload-time = "2025-07-24T16:51:09.56Z" },
{ url = "https://files.pythonhosted.org/packages/f7/30/3ab51058b75e9931fc48594d79888396cf009910fabebe12a6a636ab7f9e/coverage-7.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:4c73ab554e54ffd38d114d6bc4a7115fb0c840cf6d8622211bee3da26e4bd25d", size = 218308, upload-time = "2025-07-24T16:51:11.115Z" },
{ url = "https://files.pythonhosted.org/packages/b0/34/2adc74fd132eaa1873b1688acb906b477216074ed8a37e90426eca6d2900/coverage-7.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:3ae95d5a9aedab853641026b71b2ddd01983a0a7e9bf870a20ef3c8f5d904699", size = 216706, upload-time = "2025-07-24T16:51:12.632Z" },
{ url = "https://files.pythonhosted.org/packages/fc/a7/a47f64718c2229b7860a334edd4e6ff41ec8513f3d3f4246284610344392/coverage-7.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d883fee92b9245c0120fa25b5d36de71ccd4cfc29735906a448271e935d8d86d", size = 215143, upload-time = "2025-07-24T16:51:14.105Z" },
{ url = "https://files.pythonhosted.org/packages/ea/86/14d76a409e9ffab10d5aece73ac159dbd102fc56627e203413bfc6d53b24/coverage-7.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c87e59e88268d30e33d3665ede4fbb77b513981a2df0059e7c106ca3de537586", size = 215401, upload-time = "2025-07-24T16:51:15.978Z" },
{ url = "https://files.pythonhosted.org/packages/f4/b3/fb5c28148a19035a3877fac4e40b044a4c97b24658c980bcf7dff18bfab8/coverage-7.10.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f669d969f669a11d6ceee0b733e491d9a50573eb92a71ffab13b15f3aa2665d4", size = 245949, upload-time = "2025-07-24T16:51:17.628Z" },
{ url = "https://files.pythonhosted.org/packages/6d/95/357559ecfe73970d2023845797361e6c2e6c2c05f970073fff186fe19dd7/coverage-7.10.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9582bd6c6771300a847d328c1c4204e751dbc339a9e249eecdc48cada41f72e6", size = 248295, upload-time = "2025-07-24T16:51:19.46Z" },
{ url = "https://files.pythonhosted.org/packages/7e/58/bac5bc43085712af201f76a24733895331c475e5ddda88ac36c1332a65e6/coverage-7.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91f97e9637dc7977842776fdb7ad142075d6fa40bc1b91cb73685265e0d31d32", size = 249733, upload-time = "2025-07-24T16:51:21.518Z" },
{ url = "https://files.pythonhosted.org/packages/b2/db/104b713b3b74752ee365346677fb104765923982ae7bd93b95ca41fe256b/coverage-7.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae4fa92b6601a62367c6c9967ad32ad4e28a89af54b6bb37d740946b0e0534dd", size = 247943, upload-time = "2025-07-24T16:51:23.194Z" },
{ url = "https://files.pythonhosted.org/packages/32/4f/bef25c797c9496cf31ae9cfa93ce96b4414cacf13688e4a6000982772fd5/coverage-7.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3a5cc8b97473e7b3623dd17a42d2194a2b49de8afecf8d7d03c8987237a9552c", size = 245914, upload-time = "2025-07-24T16:51:24.766Z" },
{ url = "https://files.pythonhosted.org/packages/36/6b/b3efa0b506dbb9a37830d6dc862438fe3ad2833c5f889152bce24d9577cf/coverage-7.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc1cbb7f623250e047c32bd7aa1bb62ebc62608d5004d74df095e1059141ac88", size = 247296, upload-time = "2025-07-24T16:51:26.361Z" },
{ url = "https://files.pythonhosted.org/packages/1f/aa/95a845266aeacab4c57b08e0f4e0e2899b07809a18fd0c1ddef2ac2c9138/coverage-7.10.0-cp313-cp313-win32.whl", hash = "sha256:1380cc5666d778e77f1587cd88cc317158111f44d54c0dd3975f0936993284e0", size = 217566, upload-time = "2025-07-24T16:51:28.961Z" },
{ url = "https://files.pythonhosted.org/packages/a0/d1/27b6e5073a8026b9e0f4224f1ac53217ce589a4cdab1bee878f23bff64f0/coverage-7.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:bf03cf176af098ee578b754a03add4690b82bdfe070adfb5d192d0b1cd15cf82", size = 218337, upload-time = "2025-07-24T16:51:31.45Z" },
{ url = "https://files.pythonhosted.org/packages/c7/06/0e3ba498b11e2245fd96bd7e8dcdf90e1dd36d57f49f308aa650ff0561b8/coverage-7.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:8041c78cd145088116db2329b2fb6e89dc338116c962fbe654b7e9f5d72ab957", size = 216740, upload-time = "2025-07-24T16:51:33.317Z" },
{ url = "https://files.pythonhosted.org/packages/44/8b/11529debbe3e6b39ef6e7c8912554724adc6dc10adbb617a855ecfd387eb/coverage-7.10.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:37cc2c06052771f48651160c080a86431884db9cd62ba622cab71049b90a95b3", size = 215866, upload-time = "2025-07-24T16:51:35.339Z" },
{ url = "https://files.pythonhosted.org/packages/9c/6d/d8981310879e395f39af66536665b75135b1bc88dd21c7764e3340e9ce69/coverage-7.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:91f37270b16178b05fa107d85713d29bf21606e37b652d38646eef5f2dfbd458", size = 216083, upload-time = "2025-07-24T16:51:36.932Z" },
{ url = "https://files.pythonhosted.org/packages/c3/84/93295402de002de8b8c953bf6a1f19687174c4db7d44c1e85ffc153a772d/coverage-7.10.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f9b0b0168864d09bcb9a3837548f75121645c4cfd0efce0eb994c221955c5b10", size = 257320, upload-time = "2025-07-24T16:51:38.734Z" },
{ url = "https://files.pythonhosted.org/packages/02/5c/d0540db4869954dac0f69ad709adcd51f3a73ab11fcc9435ee76c518944a/coverage-7.10.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0be435d3b616e7d3ee3f9ebbc0d784a213986fe5dff9c6f1042ee7cfd30157", size = 259182, upload-time = "2025-07-24T16:51:40.463Z" },
{ url = "https://files.pythonhosted.org/packages/59/b2/d7d57a41a15ca4b47290862efd6b596d0a185bfd26f15d04db9f238aa56c/coverage-7.10.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35e9aba1c4434b837b1d567a533feba5ce205e8e91179c97974b28a14c23d3a0", size = 261322, upload-time = "2025-07-24T16:51:42.44Z" },
{ url = "https://files.pythonhosted.org/packages/16/92/fd828ae411b3da63673305617b6fbeccc09feb7dfe397d164f55a65cd880/coverage-7.10.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a0b0c481e74dfad631bdc2c883e57d8b058e5c90ba8ef087600995daf7bbec18", size = 258914, upload-time = "2025-07-24T16:51:44.115Z" },
{ url = "https://files.pythonhosted.org/packages/28/49/4aa5f5464b2e1215640c0400c5b007e7f5cdade8bf39c55c33b02f3a8c7f/coverage-7.10.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8aec1b7c8922808a433c13cd44ace6fceac0609f4587773f6c8217a06102674b", size = 257051, upload-time = "2025-07-24T16:51:45.75Z" },
{ url = "https://files.pythonhosted.org/packages/1e/5a/ded2346098c7f48ff6e135b5005b97de4cd9daec5c39adb4ecf3a60967da/coverage-7.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:04ec59ceb3a594af0927f2e0d810e1221212abd9a2e6b5b917769ff48760b460", size = 257869, upload-time = "2025-07-24T16:51:47.41Z" },
{ url = "https://files.pythonhosted.org/packages/46/66/e06cedb8fc7d1c96630b2f549b8cdc084e2623dcc70c900cb3b705a36a60/coverage-7.10.0-cp313-cp313t-win32.whl", hash = "sha256:b6871e62d29646eb9b3f5f92def59e7575daea1587db21f99e2b19561187abda", size = 218243, upload-time = "2025-07-24T16:51:49.136Z" },
{ url = "https://files.pythonhosted.org/packages/e7/1e/e84dd5ff35ed066bd6150e5c26fe0061ded2c59c209fd4f18db0650766c0/coverage-7.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff99cff2be44f78920b76803f782e91ffb46ccc7fa89eccccc0da3ca94285b64", size = 219334, upload-time = "2025-07-24T16:51:50.789Z" },
{ url = "https://files.pythonhosted.org/packages/b7/e0/b7b60b5dbc4e88eac0a0e9d5b4762409a59b29bf4e772b3509c8543ccaba/coverage-7.10.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3246b63501348fe47299d12c47a27cfc221cfbffa1c2d857bcc8151323a4ae4f", size = 217196, upload-time = "2025-07-24T16:51:52.599Z" },
{ url = "https://files.pythonhosted.org/packages/15/c1/597b4fa7d6c0861d4916c4fe5c45bf30c11b31a3b07fedffed23dec5f765/coverage-7.10.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:1f628d91f941a375b4503cb486148dbeeffb48e17bc080e0f0adfee729361574", size = 215139, upload-time = "2025-07-24T16:51:54.381Z" },
{ url = "https://files.pythonhosted.org/packages/18/47/07973dcad0161355cf01ff0023ab34466b735deb460a178f37163d7c800e/coverage-7.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a0e101d5af952d233557e445f42ebace20b06b4ceb615581595ced5386caa78", size = 215419, upload-time = "2025-07-24T16:51:56.341Z" },
{ url = "https://files.pythonhosted.org/packages/f6/f8/c65127782da312084ef909c1531226c869bfe22dac8b92d9c609d8150131/coverage-7.10.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ec4c1abbcc53f9f650acb14ea71725d88246a9e14ed42f8dd1b4e1b694e9d842", size = 245917, upload-time = "2025-07-24T16:51:58.045Z" },
{ url = "https://files.pythonhosted.org/packages/05/97/a7f2fe79b6ae759ccc8740608cf9686ae406cc5e5591947ebbf1d679a325/coverage-7.10.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9c95f3a7f041b4cc68a8e3fecfa6366170c13ac773841049f1cd19c8650094e0", size = 248225, upload-time = "2025-07-24T16:51:59.745Z" },
{ url = "https://files.pythonhosted.org/packages/7f/d3/d2e1496d7ac3340356c5de582e08e14b02933e254924f79d18e9749269d8/coverage-7.10.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a2cd597b69c16d24e310611f2ed6fcfb8f09429316038c03a57e7b4f5345244", size = 249844, upload-time = "2025-07-24T16:52:01.799Z" },
{ url = "https://files.pythonhosted.org/packages/e5/7e/e26d966c9cae62500e5924107974ede2e985f7d119d10ed44d102998e509/coverage-7.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5e18591906a40c2b3609196c9879136aa4a47c5405052ca6b065ab10cb0b71d0", size = 247871, upload-time = "2025-07-24T16:52:03.797Z" },
{ url = "https://files.pythonhosted.org/packages/59/95/6a372a292dfb9d6e2cc019fc50878f7a6a5fbe704604018d7c5c1dbffb2d/coverage-7.10.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:485c55744252ed3f300cc1a0f5f365e684a0f2651a7aed301f7a67125906b80e", size = 245714, upload-time = "2025-07-24T16:52:05.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/7f/63da22b7bc4e82e2c1df7755223291fc94fb01942cfe75e19f2bed96129e/coverage-7.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4dabea1516e5b0e9577282b149c8015e4dceeb606da66fb8d9d75932d5799bf5", size = 247131, upload-time = "2025-07-24T16:52:07.661Z" },
{ url = "https://files.pythonhosted.org/packages/3d/af/883272555e34872879f48daea4207489cb36df249e3069e6a8a664dc6ba6/coverage-7.10.0-cp314-cp314-win32.whl", hash = "sha256:ac455f0537af22333fdc23b824cff81110dff2d47300bb2490f947b7c9a16017", size = 217804, upload-time = "2025-07-24T16:52:09.328Z" },
{ url = "https://files.pythonhosted.org/packages/90/f6/7afc3439994b7f7311d858438d49eef8b06eadbf2322502d921a110fae1e/coverage-7.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:b3c94b532f52f95f36fbfde3e178510a4d04eea640b484b2fe8f1491338dc653", size = 218596, upload-time = "2025-07-24T16:52:11.038Z" },
{ url = "https://files.pythonhosted.org/packages/0b/99/7c715cfa155609ee3e71bc81b4d1265e1a9b79ad00cc3d19917ea736cbac/coverage-7.10.0-cp314-cp314-win_arm64.whl", hash = "sha256:2f807f2c3a9da99c80dfa73f09ef5fc3bd21e70c73ba1c538f23396a3a772252", size = 216960, upload-time = "2025-07-24T16:52:12.77Z" },
{ url = "https://files.pythonhosted.org/packages/59/18/5cb476346d3842f2e42cd92614a91921ebad38aa97aba63f2aab51919e35/coverage-7.10.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0a889ef25215990f65073c32cadf37483363a6a22914186dedc15a6b1a597d50", size = 215881, upload-time = "2025-07-24T16:52:14.492Z" },
{ url = "https://files.pythonhosted.org/packages/80/1b/c066d6836f4c1940a8df14894a5ec99db362838fdd9eee9fb7efe0e561d2/coverage-7.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39c638ecf3123805bacbf71aff8091e93af490c676fca10ab4e442375076e483", size = 216087, upload-time = "2025-07-24T16:52:16.216Z" },
{ url = "https://files.pythonhosted.org/packages/1d/57/f0996fd468e70d4d24d69eba10ecc2b913c2e85d9f3c1bb2075ad7554c05/coverage-7.10.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f2f2c0df0cbcf7dffa14f88a99c530cdef3f4fcfe935fa4f95d28be2e7ebc570", size = 257408, upload-time = "2025-07-24T16:52:18.136Z" },
{ url = "https://files.pythonhosted.org/packages/36/78/c9f308b2b986cc685d4964a3b829b053817a07d7ba14ff124cf06154402e/coverage-7.10.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:048d19a5d641a2296745ab59f34a27b89a08c48d6d432685f22aac0ec1ea447f", size = 259373, upload-time = "2025-07-24T16:52:20.923Z" },
{ url = "https://files.pythonhosted.org/packages/99/13/192827b71da71255d3554cb7dc289bce561cb281bda27e1b0dd19d88e47d/coverage-7.10.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1209b65d302d7a762004be37ab9396cbd8c99525ed572bdf455477e3a9449e06", size = 261495, upload-time = "2025-07-24T16:52:23.018Z" },
{ url = "https://files.pythonhosted.org/packages/0d/5c/cf4694353405abbb440a94468df8e5c4dbf884635da1f056b43be7284d28/coverage-7.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e44aa79a36a7a0aec6ea109905a4a7c28552d90f34e5941b36217ae9556657d5", size = 258970, upload-time = "2025-07-24T16:52:25.685Z" },
{ url = "https://files.pythonhosted.org/packages/c7/83/fb45dac65c42eff6ce4153fe51b9f2a9fdc832ce57b7902ab9ff216c3faa/coverage-7.10.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96124be864b89395770c9a14652afcddbcdafb99466f53a9281c51d1466fb741", size = 257046, upload-time = "2025-07-24T16:52:27.778Z" },
{ url = "https://files.pythonhosted.org/packages/60/95/577dc757c01f493a1951157475dd44561c82084387f12635974fb62e848c/coverage-7.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aad222e841f94b42bd1d6be71737fade66943853f0807cf87887c88f70883a2a", size = 257946, upload-time = "2025-07-24T16:52:29.931Z" },
{ url = "https://files.pythonhosted.org/packages/da/5a/14b1be12e3a71fcf4031464ae285dab7df0939976236d0462c4c5382d317/coverage-7.10.0-cp314-cp314t-win32.whl", hash = "sha256:0eed5354d28caa5c8ad60e07e938f253e4b2810ea7dd56784339b6ce98b6f104", size = 218602, upload-time = "2025-07-24T16:52:32.074Z" },
{ url = "https://files.pythonhosted.org/packages/a0/8d/c32890c0f4f7f71b8d4a1074ef8e9ef28e9b9c2f9fd0e2896f2cc32593bf/coverage-7.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3da35f9980058acb960b2644527cc3911f1e00f94d309d704b309fa984029109", size = 219720, upload-time = "2025-07-24T16:52:34.745Z" },
{ url = "https://files.pythonhosted.org/packages/22/f7/e5cc13338aa5e2780b6226fb50e9bd8f3f88da85a4b2951447b4b51109a4/coverage-7.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cb9e138dfa8a4b5c52c92a537651e2ca4f2ca48d8cb1bc01a2cbe7a5773c2426", size = 217374, upload-time = "2025-07-24T16:52:36.974Z" },
{ url = "https://files.pythonhosted.org/packages/45/fb/ace937cb8faf4d723bfc6058fee39b6756d888cf7524559885e437d06d71/coverage-7.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cf283ec9c6878826291b17442eb5c32d3d252dc77d25e082b460b2d2ea67ba3c", size = 214811, upload-time = "2025-07-24T16:52:38.826Z" },
{ url = "https://files.pythonhosted.org/packages/ff/76/cbacf622916d4d3e1c5dbe07cacfdf19c80dfab9e5f65fa62d8fa0dbab31/coverage-7.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8a83488c9fc6fff487f2ab551f9b64c70672357b8949f0951b0cd778b3ed8165", size = 215190, upload-time = "2025-07-24T16:52:40.577Z" },
{ url = "https://files.pythonhosted.org/packages/7a/24/794bebf18d9b6eb83defcc33b54c3af9ae781d2584aa07539631de2a4975/coverage-7.10.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b86df3a7494d12338c11e59f210a0498d6109bbc3a4037f44de517ebb30a9c6b", size = 241262, upload-time = "2025-07-24T16:52:42.37Z" },
{ url = "https://files.pythonhosted.org/packages/19/49/674dfe9a00de71576d21825fb4c608db18ad69bec3e1184bf0b4d6e440c0/coverage-7.10.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6de9b460809e5e4787b742e786a36ae2346a53982e2be317cdcb7a33c56412fb", size = 243159, upload-time = "2025-07-24T16:52:44.165Z" },
{ url = "https://files.pythonhosted.org/packages/73/0c/ff37bcbae61f0e7783a2b58019e757e368754819f24428beebb31a9589e9/coverage-7.10.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de5ef8a5954d63fa26a6aaa4600e48f885ce70fe495e8fce2c43aa9241fc9434", size = 244727, upload-time = "2025-07-24T16:52:46.942Z" },
{ url = "https://files.pythonhosted.org/packages/d2/d6/a42496f920770374a4116ccd01349d112e01969aeb03ba6eb3af74d5b7a0/coverage-7.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f178fe5e96f1e057527d5d0b20ab76b8616e0410169c33716cc226118eaf2c4f", size = 242662, upload-time = "2025-07-24T16:52:49.365Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f0/518341fbed44ada9660d92bb7001d848d6901d606f157d1d9009b36bfe1b/coverage-7.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4a38c42f0182a012fa9ec25bc6057e51114c1ba125be304f3f776d6d283cb303", size = 240896, upload-time = "2025-07-24T16:52:51.223Z" },
{ url = "https://files.pythonhosted.org/packages/f6/08/fbe01e9a7394e11215ec3c67d51c66947abb4a02c9076cd04e8ccd454fa5/coverage-7.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bf09beb5c1785cb36aad042455c0afab561399b74bb8cdaf6e82b7d77322df99", size = 241848, upload-time = "2025-07-24T16:52:53.388Z" },
{ url = "https://files.pythonhosted.org/packages/d2/d4/edf9d75080254d969e2a8c8b4f4a5391865a3097de493a2ad3c938c8c9d3/coverage-7.10.0-cp39-cp39-win32.whl", hash = "sha256:cb8dfbb5d3016cb8d1940444c0c69b40cdc6c8bde724b07716ee5ea47b5273c6", size = 217320, upload-time = "2025-07-24T16:52:55.258Z" },
{ url = "https://files.pythonhosted.org/packages/91/bb/4ffaec3b62fa24faf4c462cbdb0145a395f532aacc85f2e51a571d54a74f/coverage-7.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:58ff22653cd93d563110d1ff2aef958f5f21be9e917762f8124d0e36f80f172a", size = 218215, upload-time = "2025-07-24T16:52:57.118Z" },
{ url = "https://files.pythonhosted.org/packages/09/df/7c34bada8ace39f688b3bd5bc411459a20a3204ccb0984c90169a80a9366/coverage-7.10.0-py3-none-any.whl", hash = "sha256:310a786330bb0463775c21d68e26e79973839b66d29e065c5787122b8dd4489f", size = 206777, upload-time = "2025-07-24T16:52:59.009Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "distlib"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
]
[[package]]
name = "filelock"
version = "3.18.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
]
[[package]]
name = "identify"
version = "2.6.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]]
name = "nodeenv"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
]
[[package]]
name = "openfeature-sdk"
version = "0.8.1"
source = { editable = "." }
[package.dev-dependencies]
dev = [
{ name = "behave" },
{ name = "coverage", extra = ["toml"] },
{ name = "pre-commit" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
]
[package.metadata]
[package.metadata.requires-dev]
dev = [
{ name = "behave" },
{ name = "coverage", extras = ["toml"], specifier = ">=6.5" },
{ name = "pre-commit" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "parse"
version = "1.20.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" },
]
[[package]]
name = "parse-type"
version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "parse" },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/17/e9/a3b2ae5f8a852542788ac1f1865dcea0c549cc40af243f42cabfa0acf24d/parse_type-0.6.4.tar.gz", hash = "sha256:5e1ec10440b000c3f818006033372939e693a9ec0176f446d9303e4db88489a6", size = 96480, upload-time = "2024-10-03T11:51:00.353Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/b3/f6cc950042bfdbe98672e7c834d930f85920fb7d3359f59096e8d2799617/parse_type-0.6.4-py2.py3-none-any.whl", hash = "sha256:83d41144a82d6b8541127bf212dd76c7f01baff680b498ce8a4d052a7a5bce4c", size = 27442, upload-time = "2024-10-03T11:50:58.519Z" },
]
[[package]]
name = "platformdirs"
version = "4.3.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pre-commit"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cfgv" },
{ name = "identify" },
{ name = "nodeenv" },
{ name = "pyyaml" },
{ name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "8.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
]
[[package]]
name = "pytest-asyncio"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
{ name = "pytest" },
{ name = "typing-extensions", marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" },
{ url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" },
{ url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" },
{ url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" },
{ url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" },
{ url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" },
{ url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" },
{ url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" },
{ url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" },
{ url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" },
{ url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" },
{ url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" },
{ url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" },
{ url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" },
{ url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" },
{ url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" },
{ url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
{ url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" },
{ url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" },
{ url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" },
{ url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" },
{ url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" },
{ url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" },
{ url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" },
{ url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" },
{ url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "tomli"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
]
[[package]]
name = "typing-extensions"
version = "4.14.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
]
[[package]]
name = "virtualenv"
version = "20.32.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/96/0834f30fa08dca3738614e6a9d42752b6420ee94e58971d702118f7cfd30/virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0", size = 6076970, upload-time = "2025-07-21T04:09:50.985Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/c6/f8f28009920a736d0df434b52e9feebfb4d702ba942f15338cb4a83eafc1/virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56", size = 6057761, upload-time = "2025-07-21T04:09:48.059Z" },
]