Compare commits

...

86 Commits
v0.7.5 ... main

Author SHA1 Message Date
Anton Grübel 32fdec1781
chore: support Python 3.14 (#530)
support Python 3.14

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2025-08-02 17:12:04 +02:00
renovate[bot] 9d0cbe8d4a
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.17.1 (#528)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 13:35:26 +02:00
OpenFeature Bot 1802022994
chore(main): release 0.8.2 (#485)
Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>
2025-07-30 17:29:08 -04:00
Anton Grübel a3698902b5
chore: switch build backend to uv (#527)
Signed-off-by: gruebel <anton.gruebel@gmail.com>
2025-07-30 17:08:54 -04:00
Konstantinos Koukopoulos a5b3aa9c52
fix: merge transaction context into hook context evaluation context (#521) (#523)
Signed-off-by: Konstantinos Koukopoulos <koukopoulos@gmail.com>
2025-07-23 14:27:15 +02:00
renovate[bot] 5652c0c457
chore(deps): update astral-sh/setup-uv digest to 7edac99 (#524)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 23:47:48 +02:00
renovate[bot] 00cab65315
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.17.0 (#526)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Simon Schrottner <simon.schrottner@dynatrace.com>
2025-07-21 17:33:07 +02:00
renovate[bot] 90a193d22c
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.12.4 (#525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 17:31:59 +02:00
renovate[bot] 288bd6bb34
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.12.2 (#518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 22:09:42 +00:00
renovate[bot] a04e52c022
chore(deps): update github/codeql-action digest to 181d5ee (#517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 20:46:31 +00:00
renovate[bot] 21ef53a156
chore(deps): update github/codeql-action digest to 39edc49 (#515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 23:33:05 +00:00
renovate[bot] 6da7890ac6
chore(deps): pin astral-sh/setup-uv action to bd01e18 (#514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 11:20:53 +02:00
Leo fb47cbb2a5
feat: starting migration to uv (#512)
* starting migration to uv

Signed-off-by: leohoare <leo@insight.co>

* single command for test

Signed-off-by: leohoare <leo@insight.co>

* uv-lock in pre-commit, manually run pre-commit instead of action, add mypy and pre-commit to dev dependencies

Signed-off-by: leohoare <leo@insight.co>

* ignore nothing to cache

Signed-off-by: leohoare <leo@insight.co>

* cleanup and remove comment

Signed-off-by: leohoare <leo@insight.co>

* Update .github/workflows/release.yml

Co-authored-by: Simon Schrottner <simon.schrottner@dynatrace.com>
Signed-off-by: Leo  <37860104+leohoare@users.noreply.github.com>
Signed-off-by: leohoare <leo@insight.co>

* cloeset thing to hatch scripts using project scripts

Signed-off-by: leohoare <leo@insight.co>

* ignore errors (not ideal)

Signed-off-by: leohoare <leo@insight.co>

* move pre-commit into scripts

Signed-off-by: leohoare <leo@insight.co>

* update dependency group, add frozen, remove uv-lock precommit, v6 uv, revert precommit

Signed-off-by: leohoare <leo@insight.co>

* add comment to github issue

Signed-off-by: leohoare <leo@insight.co>

---------

Signed-off-by: leohoare <leo@insight.co>
Signed-off-by: Leo  <37860104+leohoare@users.noreply.github.com>
Co-authored-by: Simon Schrottner <simon.schrottner@dynatrace.com>
Co-authored-by: Anton Grübel <anton.gruebel@gmail.com>
2025-06-27 09:17:52 +02:00
renovate[bot] 7783a8b6c7
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.12.1 (#513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 02:43:29 +00:00
renovate[bot] d21d9db90a
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.12.0 (#510)
* chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.12.0

* skip PLC0415

Signed-off-by: gruebel <anton.gruebel@gmail.com>

---------

Signed-off-by: gruebel <anton.gruebel@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: gruebel <anton.gruebel@gmail.com>
2025-06-19 17:32:18 +00:00
renovate[bot] ac95c7a5b7
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.16.1 (#509)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 13:26:01 +02:00
renovate[bot] 4628c24f5c
chore(deps): update github/codeql-action digest to ce28f5b (#508)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 21:35:29 +00:00
renovate[bot] a21413bd50
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.13 (#507)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 02:55:07 +00:00
renovate[bot] 347517a7cc
chore(deps): update github/codeql-action digest to fca7ace (#505)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-03 21:29:53 +00:00
Anton Grübel f95b27a25a
refactor: refine typing.Any type hints (#504)
* refine typing.Any type hints

Signed-off-by: gruebel <anton.gruebel@gmail.com>

* exclude TYPE_CHECKING from coverage

Signed-off-by: gruebel <anton.gruebel@gmail.com>

---------

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2025-06-03 19:52:16 +02:00
renovate[bot] 87e448593d
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.16.0 (#503)
* chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.16.0

* fix typing

Signed-off-by: gruebel <anton.gruebel@gmail.com>

---------

Signed-off-by: gruebel <anton.gruebel@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: gruebel <anton.gruebel@gmail.com>
2025-05-30 16:35:39 +02:00
renovate[bot] 8dfa88cf8a
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.12 (#501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-29 18:06:49 +00:00
renovate[bot] abb3137779
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.11 (#499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-23 07:31:12 +00:00
renovate[bot] c722cf0239
chore(deps): update github/codeql-action digest to ff0a06e (#498)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-16 17:01:35 +00:00
renovate[bot] 7bb0f5e499
chore(deps): update codecov/codecov-action action to v5.4.3 (#497)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-16 03:11:30 +00:00
renovate[bot] 1dd8b29493
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.10 (#496)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-15 20:08:12 +00:00
renovate[bot] 42fed6b200
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.9 (#493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-09 22:07:55 +00:00
renovate[bot] 8aedfe81ef
chore(deps): update github/codeql-action digest to 60168ef (#492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-02 15:58:40 +00:00
renovate[bot] 1f169551e3
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.8 (#491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-01 19:04:19 +00:00
renovate[bot] f4f9a12081
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.7 (#490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-25 03:51:50 +00:00
renovate[bot] ad69f2c55f
chore(deps): update actions/setup-python digest to a26af69 (#489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-24 13:22:29 +02:00
renovate[bot] e0de4b2faa
chore(deps): update github/codeql-action digest to 28deaed (#488)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-23 23:01:54 +00:00
renovate[bot] 7fe752d8fd
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.6 (#487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 18:38:47 +00:00
Todd Baert d54d239a2d
chore: use publish env
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-04-16 13:49:13 -04:00
renovate[bot] 798ac8ded0
chore(deps): update codecov/codecov-action action to v5.4.2 (#486)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 02:54:15 +00:00
renovate[bot] 95be943d33
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.5 (#484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-10 23:27:28 +00:00
OpenFeature Bot 4006df768c
chore(main): release 0.8.1 (#445)
Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>
2025-04-10 08:47:46 +02:00
Anton Grübel 3636a0d75f
fix: fix cycle dependency between api and client (#480)
* fix cycle dependency between api and client

Signed-off-by: gruebel <anton.gruebel@gmail.com>

* remove comment

Signed-off-by: gruebel <anton.gruebel@gmail.com>

---------

Signed-off-by: gruebel <anton.gruebel@gmail.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-04-09 08:39:26 +02:00
Anton Grübel e61b69bb50
refactor: replace exception raising with error flag resolution (#474)
* replace exception raising with error flag resolution

Signed-off-by: gruebel <anton.gruebel@gmail.com>

* revert spec to commit 0cd553d

Signed-off-by: gruebel <anton.gruebel@gmail.com>

---------

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2025-04-08 19:54:41 +02:00
renovate[bot] 5a2825b00d
chore(deps): update github/codeql-action digest to 45775bd (#483)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-08 02:13:02 +00:00
renovate[bot] 8acc883288
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.4 (#476)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-07 19:00:06 +00:00
Simon Schrottner 7a30ef914b
chore: add codeowner file to be consistent with the rest of openfeature (#477)
All the other sdks use codeowner files to ensure proper approvals, we should also utilize this within python-sdk

Signed-off-by: Simon Schrottner <simon.schrottner@dynatrace.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-04-07 16:29:13 +02:00
renovate[bot] 1ae9fc2361
chore(deps): update github/codeql-action digest to fc7e4a0 (#481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-07 13:25:19 +00:00
Anton Grübel 0ebec538db
chore: revert spec to commit 0cd553d (#479)
Signed-off-by: gruebel <anton.gruebel@gmail.com>
2025-04-07 15:21:55 +02:00
renovate[bot] 2be2c06569
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.3 (#475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-03 16:18:36 +00:00
renovate[bot] a1359112e9
chore(deps): update actions/setup-python digest to 8d9ed9a (#473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-25 06:01:28 +00:00
renovate[bot] 490cd06853
chore(deps): update spec digest to 27e4461 (#472)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-25 02:34:26 +00:00
renovate[bot] 9ced6bf2d1
chore(deps): update spec digest to 130df3e (#471)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 19:50:16 +00:00
renovate[bot] 4eeab3b691
chore(deps): update github/codeql-action digest to 1b549b9 (#470)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 19:48:43 +00:00
renovate[bot] 95e87c71fc
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.2 (#469)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-21 15:16:51 +00:00
renovate[bot] c07d3d6467
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.1 (#468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-20 15:57:03 +00:00
renovate[bot] d69b7594a9
chore(deps): update github/codeql-action digest to 5f8171a (#467)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-19 15:08:59 +00:00
renovate[bot] d1eb3a08a8
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.0 (#465)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 18:11:34 +00:00
renovate[bot] d15388b542
chore(deps): update spec digest to aad6193 (#464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 18:10:18 +00:00
renovate[bot] 5fede4d4f0
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.10.0 (#463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-13 21:36:48 +00:00
renovate[bot] 0396592586
chore(deps): update github/codeql-action digest to 6bb031a (#462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 17:59:00 +00:00
renovate[bot] 9057c6b3df
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.10 (#461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 16:00:57 +00:00
renovate[bot] 547781fbd8
chore(deps): update spec digest to 09aef37 (#460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-06 10:44:45 +00:00
renovate[bot] 40cbd82dda
chore(deps): update spec digest to 25c57ee (#459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-06 06:15:26 +00:00
renovate[bot] 9ce51ebff5
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.9 (#458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-28 12:08:52 +00:00
renovate[bot] 0c1a388ca1
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.8 (#457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 14:41:09 +00:00
renovate[bot] a666227f55
chore(deps): update codecov/codecov-action action to v5.4.0 (#456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 03:09:18 +00:00
renovate[bot] fe99f08e94
chore(deps): update spec digest to 0cd553d (#455)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 16:04:34 +00:00
Anton Grübel 2d1ba85c93
feat: add OTel utility function (#451)
add OTel utility function

Signed-off-by: gruebel <anton.gruebel@gmail.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-02-22 16:50:27 +01:00
renovate[bot] 613388ddde
chore(deps): update github/codeql-action digest to b56ba49 (#454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 17:45:32 +00:00
renovate[bot] a5cb27b678
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.7 (#453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 19:04:06 +00:00
Anton Grübel 088409ea5c
fix: add passthrough init to abstract provider (#450)
Signed-off-by: gruebel <anton.gruebel@gmail.com>
2025-02-18 08:45:12 -05:00
Michael Beemer 11987280ba
docs: fix linting issue on the readme
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-02-18 08:29:49 -05:00
renovate[bot] 95b33b39e6
chore(deps): update spec digest to a69f748 (#452)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 02:38:44 +00:00
Anton Grübel 31afa6490f
chore: improve resolve details callable type hints (#449)
improve resolve details callable type hints

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2025-02-13 21:52:41 +01:00
Leo f29c4506a6
chore: use keyword arguments, validate test (#446)
Signed-off-by: leohoare <leo@insight.co>
2025-02-13 12:06:54 -05:00
renovate[bot] f907855966
chore(deps): update spec digest to 54952f3 (#447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-12 23:27:56 +00:00
Michael Beemer 5ae8571ccd
chore: use existing submodule version for e2e tests (#444)
* chore: use existing submodule version for e2e tests

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>

* reset submoduels

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>

---------

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-02-12 08:21:06 +01:00
OpenFeature Bot 2951eb2982
chore(main): release 0.8.0 (#431)
Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>
2025-02-11 22:20:08 +01:00
Anton Grübel bcd1a3807e
chore!: drop Python 3.8 support (#441)
* drop Python 3.8 support

Signed-off-by: gruebel <anton.gruebel@gmail.com>

* pin mypy python version to 3.9

Signed-off-by: gruebel <anton.gruebel@gmail.com>

---------

Signed-off-by: gruebel <anton.gruebel@gmail.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-02-11 22:17:37 +01:00
chrfwow d4f53b4de2
test: Implement gherkin hooks tests (#442)
* Implement gherkin hooks tests

Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>

* fixup! Implement gherkin hooks tests

Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>

* fix tests and lint

Signed-off-by: gruebel <anton.gruebel@gmail.com>

* lint

Signed-off-by: gruebel <anton.gruebel@gmail.com>

---------

Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
Signed-off-by: gruebel <anton.gruebel@gmail.com>
Co-authored-by: gruebel <anton.gruebel@gmail.com>
2025-02-11 22:12:46 +01:00
renovate[bot] 37296dc0b5
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.6 (#443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 13:38:19 +00:00
renovate[bot] ba0213e701
chore(deps): update github/codeql-action digest to 9e8d078 (#440)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-07 12:42:14 +00:00
renovate[bot] 75b41dd020
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.5 (#439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-06 21:39:03 +00:00
Leo 86e7c07112
feat: Add async functionality to providers (#413)
Signed-off-by: leohoare <leo@insight.co>
2025-02-06 12:30:54 -05:00
Maxim 154d8345e7
docs: fix eval context link (#438)
Signed-off-by: Maxim <therb1@mail.com>
2025-02-05 16:24:48 -05:00
renovate[bot] 38d13fa454
chore(deps): pin dependencies (#435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-05 15:05:28 -05:00
Michael Beemer e705af47b1
chore: fix renovate syntax issue
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-02-05 15:01:22 -05:00
Michael Beemer ff521630a1
chore: use centralized renovate config, downgrade release please (#433)
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-02-05 14:42:16 -05:00
Maxim 49edce2269
docs: fix links to the openfeature ecosystem page (#432)
Signed-off-by: Maxim <therb1@mail.com>
2025-02-05 13:58:41 -05:00
renovate[bot] fe0fea1f73
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.15.0 (#430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-05 10:27:38 +00:00
43 changed files with 2605 additions and 492 deletions

View File

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

View File

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: googleapis/release-please-action@v4 - uses: googleapis/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3
id: release id: release
with: with:
command: manifest command: manifest
@ -34,7 +34,7 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: release environment: publish
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
@ -44,18 +44,16 @@ jobs:
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5 - name: Install uv and set the python version
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6
with: with:
python-version: '3.13' python-version: "3.13"
- name: Upgrade pip - name: Install dependencies
run: pip install --upgrade pip run: uv sync --frozen
- name: Install hatch
run: pip install hatch
- name: Build a binary wheel and a source tarball - name: Build a binary wheel and a source tarball
run: hatch build run: uv build
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1

View File

@ -1,9 +1,9 @@
default_stages: [commit] default_stages: [pre-commit]
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.4 rev: v0.12.4
hooks: hooks:
- id: ruff - id: ruff-check
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.14.1 rev: v1.17.1
hooks: hooks:
- id: mypy - id: mypy
files: openfeature files: openfeature

View File

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

View File

@ -1,5 +1,143 @@
# 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) ## [0.7.5](https://github.com/open-feature/python-sdk/compare/v0.7.4...v0.7.5) (2025-01-31)

1
CODEOWNERS Normal file
View File

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

View File

@ -4,27 +4,59 @@
### System Requirements ### System Requirements
Python 3.8 and above are required. Python 3.9 and above are required.
### Target version(s) ### Target version(s)
Python 3.8 and above are supported by the SDK. Python 3.9 and above are supported by the SDK.
### Installation and Dependencies ### Installation and Dependencies
We use [Hatch](https://hatch.pypa.io/) to manage the project. We use [uv](https://github.com/astral-sh/uv) for fast Python package management and dependency resolution.
To install Hatch, just run `pip install hatch`. To install uv, follow the [installation guide](https://docs.astral.sh/uv/getting-started/installation/).
You will also need to set up the `pre-commit` hooks. ### Setup Development Environment
Run `pre-commit install` in the root directory of the repository.
If you don't have `pre-commit` installed, you can install it with `pip install pre-commit`. 1. **Clone the repository:**
```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 with `hatch run test`. Run tests:
```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
@ -59,7 +91,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
hatch run test uv run test --frozen
``` ```
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.7.5"> <a href="https://github.com/open-feature/python-sdk/releases/tag/v0.8.2">
<img alt="Latest version" src="https://img.shields.io/static/v1?label=release&message=v0.7.5&color=blue&style=for-the-badge" /> <img alt="Latest version" src="https://img.shields.io/static/v1?label=release&message=v0.8.2&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.8-blue.svg" /> <img alt="Min python version" src="https://img.shields.io/badge/python->=3.9-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.8+ - Python 3.9+
### Install ### Install
@ -60,13 +60,13 @@
#### Pip install #### Pip install
```bash ```bash
pip install openfeature-sdk==0.7.5 pip install openfeature-sdk==0.8.2
``` ```
#### requirements.txt #### requirements.txt
```bash ```bash
openfeature-sdk==0.7.5 openfeature-sdk==0.8.2
``` ```
```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](/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](https://openfeature.dev/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,6 +316,25 @@ 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.
@ -390,6 +409,57 @@ 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, Dict, List from typing import TYPE_CHECKING
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 from openfeature.api import get_client # noqa: PLC0415
_run_immediate_handler(get_client(), event, handler) _run_immediate_handler(get_client(), event, handler)

View File

@ -2,19 +2,22 @@ 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 EvaluationContext from openfeature.evaluation_context import (
get_evaluation_context,
set_evaluation_context,
)
from openfeature.event import ( from openfeature.event import (
EventHandler, EventHandler,
ProviderEvent, ProviderEvent,
) )
from openfeature.exception import GeneralError from openfeature.hook import add_hooks, clear_hooks, get_hooks
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 TransactionContextPropagator from openfeature.transaction_context import (
from openfeature.transaction_context.no_op_transaction_context_propagator import ( get_transaction_context,
NoOpTransactionContextPropagator, set_transaction_context,
set_transaction_context_propagator,
) )
__all__ = [ __all__ = [
@ -35,13 +38,6 @@ __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
@ -67,49 +63,6 @@ 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,9 +1,10 @@
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, api from openfeature import _event_support
from openfeature.evaluation_context import EvaluationContext from openfeature.evaluation_context import EvaluationContext, get_evaluation_context
from openfeature.event import EventHandler, ProviderEvent from openfeature.event import EventHandler, ProviderEvent
from openfeature.exception import ( from openfeature.exception import (
ErrorCode, ErrorCode,
@ -18,9 +19,10 @@ from openfeature.flag_evaluation import (
FlagEvaluationOptions, FlagEvaluationOptions,
FlagResolutionDetails, FlagResolutionDetails,
FlagType, FlagType,
FlagValueType,
Reason, Reason,
) )
from openfeature.hook import Hook, HookContext from openfeature.hook import Hook, HookContext, HookHints, get_hooks
from openfeature.hook._hook_support import ( from openfeature.hook._hook_support import (
after_all_hooks, after_all_hooks,
after_hooks, after_hooks,
@ -29,6 +31,7 @@ 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",
@ -37,35 +40,37 @@ __all__ = [
logger = logging.getLogger("openfeature") logger = logging.getLogger("openfeature")
GetDetailCallable = typing.Union[ TypeMap = dict[
typing.Callable[
[str, bool, typing.Optional[EvaluationContext]], FlagResolutionDetails[bool]
],
typing.Callable[
[str, int, typing.Optional[EvaluationContext]], FlagResolutionDetails[int]
],
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, FlagType,
typing.Union[ typing.Union[
typing.Type[bool], type[bool],
typing.Type[int], type[int],
typing.Type[float], type[float],
typing.Type[str], type[str],
typing.Tuple[typing.Type[dict], typing.Type[list]], tuple[type[dict], 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
class ClientMetadata: class ClientMetadata:
@ -79,7 +84,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[typing.List[Hook]] = None, hooks: typing.Optional[list[Hook]] = None,
) -> None: ) -> None:
self.domain = domain self.domain = domain
self.version = version self.version = version
@ -96,7 +101,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: typing.List[Hook]) -> None: def add_hooks(self, hooks: list[Hook]) -> None:
self.hooks = self.hooks + hooks self.hooks = self.hooks + hooks
def get_boolean_value( def get_boolean_value(
@ -113,6 +118,21 @@ 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,
@ -128,6 +148,21 @@ 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,
@ -142,6 +177,21 @@ 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,
@ -157,6 +207,21 @@ 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,
@ -171,6 +236,21 @@ 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,
@ -186,6 +266,21 @@ 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,
@ -200,6 +295,21 @@ 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,
@ -215,13 +325,30 @@ 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[dict, list], default_value: typing.Union[
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[dict, list]: ) -> typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]:
return self.get_object_details( return self.get_object_details(
flag_key, flag_key,
default_value, default_value,
@ -229,13 +356,34 @@ 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[dict, list], default_value: typing.Union[
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[typing.Union[dict, list]]: ) -> FlagEvaluationDetails[
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,
@ -244,14 +392,354 @@ class OpenFeatureClient:
flag_evaluation_options, flag_evaluation_options,
) )
def evaluate_flag_details( # noqa: PLR0915 async def get_object_details_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,
) -> 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: typing.Any, default_value: FlagValueType,
evaluation_context: typing.Optional[EvaluationContext],
flag_evaluation_options: typing.Optional[FlagEvaluationOptions],
) -> tuple[
FeatureProvider,
HookContext,
HookHints,
list[Hook],
list[Hook],
]:
if evaluation_context is None:
evaluation_context = EvaluationContext()
if flag_evaluation_options is None:
flag_evaluation_options = FlagEvaluationOptions()
provider = self.provider # call this once to maintain a consistent reference
evaluation_hooks = flag_evaluation_options.hooks
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(
flag_key=flag_key,
flag_type=flag_type,
default_value=default_value,
evaluation_context=merged_eval_context,
client_metadata=self.get_metadata(),
provider_metadata=provider.get_metadata(),
)
# Hooks need to be handled in different orders at different stages
# in the flag evaluation
# before: API, Client, Invocation, Provider
merged_hooks = (
get_hooks() + self.hooks + evaluation_hooks + provider.get_provider_hooks()
)
# after, error, finally: Provider, Invocation, Client, API
reversed_merged_hooks = merged_hooks[:]
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()
if status == ProviderStatus.NOT_READY:
return ProviderNotReadyError()
if status == ProviderStatus.FATAL:
return ProviderFatalError()
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, evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[typing.Any]: ) -> 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,
hook_context,
merged_hooks,
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,
hook_hints,
)
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,
value=default_value,
reason=Reason.ERROR,
error_code=err.error_code,
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. Evaluate the flag requested by the user from the clients provider.
@ -263,45 +751,22 @@ 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
""" """
provider, hook_context, hook_hints, merged_hooks, reversed_merged_hooks = (
if evaluation_context is None: self._establish_hooks_and_provider(
evaluation_context = EvaluationContext() flag_type,
flag_key,
if flag_evaluation_options is None: default_value,
flag_evaluation_options = FlagEvaluationOptions() evaluation_context,
flag_evaluation_options,
provider = self.provider # call this once to maintain a consistent reference )
evaluation_hooks = flag_evaluation_options.hooks
hook_hints = flag_evaluation_options.hook_hints
hook_context = HookContext(
flag_key=flag_key,
flag_type=flag_type,
default_value=default_value,
evaluation_context=evaluation_context,
client_metadata=self.get_metadata(),
provider_metadata=provider.get_metadata(),
) )
# Hooks need to be handled in different orders at different stages
# in the flag evaluation
# before: API, Client, Invocation, Provider
merged_hooks = (
api.get_hooks()
+ self.hooks
+ evaluation_hooks
+ provider.get_provider_hooks()
)
# after, error, finally: Provider, Invocation, Client, API
reversed_merged_hooks = merged_hooks[:]
reversed_merged_hooks.reverse()
try: try:
status = self.get_provider_status() if provider_err := self._assert_provider_status():
if status == ProviderStatus.NOT_READY:
error_hooks( error_hooks(
flag_type, flag_type,
hook_context, hook_context,
ProviderNotReadyError(), provider_err,
reversed_merged_hooks, reversed_merged_hooks,
hook_hints, hook_hints,
) )
@ -309,40 +774,17 @@ class OpenFeatureClient:
flag_key=flag_key, flag_key=flag_key,
value=default_value, value=default_value,
reason=Reason.ERROR, reason=Reason.ERROR,
error_code=ErrorCode.PROVIDER_NOT_READY, error_code=provider_err.error_code,
) error_message=provider_err.error_message,
return flag_evaluation
if status == ProviderStatus.FATAL:
error_hooks(
flag_type,
hook_context,
ProviderFatalError(),
reversed_merged_hooks,
hook_hints,
)
flag_evaluation = FlagEvaluationDetails(
flag_key=flag_key,
value=default_value,
reason=Reason.ERROR,
error_code=ErrorCode.PROVIDER_FATAL,
) )
return flag_evaluation return flag_evaluation
# https://github.com/open-feature/spec/blob/main/specification/sections/03-evaluation-context.md merged_context = self._run_before_hooks_and_update_context(
# 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 merged_hooks,
invocation_context = before_hooks( hook_hints,
flag_type, hook_context, merged_hooks, hook_hints evaluation_context,
)
invocation_context = invocation_context.merge(ctx2=evaluation_context)
# Requirement 3.2.2 merge: API.context->transaction.context->client.context->invocation.context
merged_context = (
api.get_evaluation_context()
.merge(api.get_transaction_context())
.merge(self.context)
.merge(invocation_context)
) )
flag_evaluation = self._create_provider_evaluation( flag_evaluation = self._create_provider_evaluation(
@ -352,6 +794,12 @@ 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,
@ -402,14 +850,61 @@ class OpenFeatureClient:
hook_hints, 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: typing.Any, default_value: FlagValueType,
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagEvaluationDetails[typing.Any]: ) -> FlagEvaluationDetails[FlagValueType]:
""" """
Encapsulated method to create a FlagEvaluationDetail from a specific provider. Encapsulated method to create a FlagEvaluationDetail from a specific provider.
@ -420,13 +915,7 @@ 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
""" """
args = ( get_details_callables: typing.Mapping[FlagType, ResolveDetailsCallable] = {
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,
@ -436,23 +925,33 @@ 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:
raise GeneralError(error_message="Unknown flag type") return FlagEvaluationDetails(
flag_key=flag_key,
value=default_value,
reason=Reason.ERROR,
error_code=ErrorCode.GENERAL,
error_message="Unknown flag type",
)
resolution = get_details_callable(*args) resolution = get_details_callable(
resolution.raise_for_error() 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. # we need to check the get_args to be compatible with union types.
_typecheck_flag_value(resolution.value, flag_type) 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 FlagEvaluationDetails( return resolution.to_flag_evaluation_details(flag_key)
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)
@ -461,7 +960,9 @@ class OpenFeatureClient:
_event_support.remove_client_handler(self, event, handler) _event_support.remove_client_handler(self, event, handler)
def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None: def _typecheck_flag_value(
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,
@ -471,6 +972,7 @@ def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None:
} }
_type = type_map.get(flag_type) _type = type_map.get(flag_type)
if not _type: if not _type:
raise GeneralError(error_message="Unknown flag type") return GeneralError(error_message="Unknown flag type")
if not isinstance(value, _type): if not isinstance(value, _type):
raise TypeMismatchError(f"Expected type {_type} but got {type(value)}") return TypeMismatchError(f"Expected type {_type} but got {type(value)}")
return None

View File

@ -1,19 +0,0 @@
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

@ -0,0 +1,54 @@
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, Dict, List, Optional, Union from typing import Callable, 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,7 +2,8 @@ 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",
@ -163,7 +164,7 @@ class InvalidContextError(OpenFeatureError):
super().__init__(ErrorCode.INVALID_CONTEXT, error_message) super().__init__(ErrorCode.INVALID_CONTEXT, error_message)
class ErrorCode(Enum): class ErrorCode(StrEnum):
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,10 +1,11 @@
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 from openfeature.exception import ErrorCode, OpenFeatureError
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
@ -34,13 +35,22 @@ class Reason(StrEnum):
DEFAULT = "DEFAULT" DEFAULT = "DEFAULT"
DISABLED = "DISABLED" DISABLED = "DISABLED"
ERROR = "ERROR" ERROR = "ERROR"
STATIC = "STATIC"
SPLIT = "SPLIT" SPLIT = "SPLIT"
STATIC = "STATIC"
STALE = "STALE"
TARGETING_MATCH = "TARGETING_MATCH" TARGETING_MATCH = "TARGETING_MATCH"
UNKNOWN = "UNKNOWN" UNKNOWN = "UNKNOWN"
FlagMetadata = typing.Mapping[str, typing.Any] FlagMetadata = typing.Mapping[str, typing.Union[bool, int, float, str]]
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)
@ -55,10 +65,15 @@ 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: typing.List[Hook] = field(default_factory=list) hooks: list[Hook] = field(default_factory=list)
hook_hints: HookHints = field(default_factory=dict) hook_hints: HookHints = field(default_factory=dict)
@ -78,3 +93,14 @@ 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,18 +1,29 @@
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 from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType, FlagValueType
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__ = ["Hook", "HookContext", "HookHints", "HookType"] __all__ = [
"Hook",
"HookContext",
"HookHints",
"HookType",
"add_hooks",
"clear_hooks",
"get_hooks",
]
_hooks: list[Hook] = []
class HookType(Enum): class HookType(Enum):
@ -27,7 +38,7 @@ class HookContext:
self, self,
flag_key: str, flag_key: str,
flag_type: FlagType, flag_type: FlagType,
default_value: typing.Any, default_value: FlagValueType,
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,
@ -60,8 +71,8 @@ HookHints = typing.Mapping[
float, float,
str, str,
datetime, datetime,
typing.List[typing.Any], Sequence["HookHints"],
typing.Dict[str, typing.Any], typing.Mapping[str, "HookHints"],
], ],
] ]
@ -84,7 +95,7 @@ class Hook:
def after( def after(
self, self,
hook_context: HookContext, hook_context: HookContext,
details: FlagEvaluationDetails[typing.Any], details: FlagEvaluationDetails[FlagValueType],
hints: HookHints, hints: HookHints,
) -> None: ) -> None:
""" """
@ -112,7 +123,7 @@ class Hook:
def finally_after( def finally_after(
self, self,
hook_context: HookContext, hook_context: HookContext,
details: FlagEvaluationDetails[typing.Any], details: FlagEvaluationDetails[FlagValueType],
hints: HookHints, hints: HookHints,
) -> None: ) -> None:
""" """
@ -133,3 +144,17 @@ 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: typing.List[Hook], hooks: 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}
@ -26,7 +26,7 @@ def after_all_hooks(
flag_type: FlagType, flag_type: FlagType,
hook_context: HookContext, hook_context: HookContext,
details: FlagEvaluationDetails[typing.Any], 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, "details": details, "hints": hints}
@ -39,7 +39,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: 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, "details": details, "hints": hints}
@ -51,7 +51,7 @@ def after_hooks(
def before_hooks( def before_hooks(
flag_type: FlagType, flag_type: FlagType,
hook_context: HookContext, hook_context: HookContext,
hooks: typing.List[Hook], hooks: 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 +68,7 @@ def before_hooks(
def _execute_hooks( def _execute_hooks(
flag_type: FlagType, flag_type: FlagType,
hooks: typing.List[Hook], hooks: list[Hook],
hook_method: HookType, hook_method: HookType,
**kwargs: typing.Any, **kwargs: typing.Any,
) -> list: ) -> list:
@ -91,10 +91,10 @@ def _execute_hooks(
def _execute_hooks_unchecked( def _execute_hooks_unchecked(
flag_type: FlagType, flag_type: FlagType,
hooks: typing.List[Hook], hooks: list[Hook],
hook_method: HookType, hook_method: HookType,
**kwargs: typing.Any, **kwargs: typing.Any,
) -> typing.List[typing.Optional[EvaluationContext]]: ) -> 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 # type:ignore[assignment] update = _immutable
setdefault = _immutable # type:ignore[assignment] setdefault = _immutable
pop = _immutable # type:ignore[assignment] pop = _immutable
popitem = _immutable popitem = _immutable

View File

@ -2,6 +2,7 @@ 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
@ -11,6 +12,9 @@ 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"]
@ -38,7 +42,7 @@ class FeatureProvider(typing.Protocol): # pragma: no cover
def get_metadata(self) -> Metadata: ... def get_metadata(self) -> Metadata: ...
def get_provider_hooks(self) -> typing.List[Hook]: ... def get_provider_hooks(self) -> list[Hook]: ...
def resolve_boolean_details( def resolve_boolean_details(
self, self,
@ -47,6 +51,13 @@ 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,
@ -54,6 +65,13 @@ 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,
@ -61,6 +79,13 @@ 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,
@ -68,15 +93,41 @@ 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[dict, list], default_value: typing.Union[
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[typing.Union[dict, list]]: ... ) -> FlagResolutionDetails[
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[
@ -99,7 +150,7 @@ class AbstractProvider(FeatureProvider):
def get_metadata(self) -> Metadata: def get_metadata(self) -> Metadata:
pass pass
def get_provider_hooks(self) -> typing.List[Hook]: def get_provider_hooks(self) -> list[Hook]:
return [] return []
@abstractmethod @abstractmethod
@ -111,6 +162,14 @@ 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,
@ -120,6 +179,14 @@ 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,
@ -129,6 +196,14 @@ 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,
@ -138,15 +213,39 @@ 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[dict, list], default_value: typing.Union[
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[typing.Union[dict, list]]: ) -> FlagResolutionDetails[
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 from openfeature.evaluation_context import EvaluationContext, get_evaluation_context
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: typing.Dict[str, FeatureProvider] _providers: dict[str, FeatureProvider]
_provider_status: typing.Dict[FeatureProvider, ProviderStatus] _provider_status: dict[FeatureProvider, ProviderStatus]
def __init__(self) -> None: def __init__(self) -> None:
self._default_provider = NoOpProvider() self._default_provider = NoOpProvider()
@ -65,9 +65,6 @@ 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,13 +1,19 @@
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 FlagNotFoundError from openfeature.exception import ErrorCode
from openfeature.flag_evaluation import FlagMetadata, FlagResolutionDetails, Reason from openfeature.flag_evaluation import 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"
@ -26,12 +32,12 @@ class InMemoryFlag(typing.Generic[T_co]):
DISABLED = "DISABLED" DISABLED = "DISABLED"
default_variant: str default_variant: str
variants: typing.Dict[str, T_co] variants: 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
@ -51,7 +57,7 @@ class InMemoryFlag(typing.Generic[T_co]):
) )
FlagStorage = typing.Dict[str, InMemoryFlag[typing.Any]] FlagStorage = dict[str, InMemoryFlag[typing.Any]]
V = typing.TypeVar("V") V = typing.TypeVar("V")
@ -65,7 +71,7 @@ class InMemoryProvider(AbstractProvider):
def get_metadata(self) -> Metadata: def get_metadata(self) -> Metadata:
return InMemoryMetadata() return InMemoryMetadata()
def get_provider_hooks(self) -> typing.List[Hook]: def get_provider_hooks(self) -> list[Hook]:
return [] return []
def resolve_boolean_details( def resolve_boolean_details(
@ -74,7 +80,15 @@ 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, evaluation_context) return self._resolve(flag_key, default_value, 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,
@ -82,7 +96,15 @@ 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, evaluation_context) return self._resolve(flag_key, default_value, 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,
@ -90,7 +112,15 @@ 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, evaluation_context) return self._resolve(flag_key, default_value, 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,
@ -98,22 +128,60 @@ 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, evaluation_context) return self._resolve(flag_key, default_value, 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[dict, list], default_value: typing.Union[
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[typing.Union[dict, list]]: ) -> FlagResolutionDetails[
return self._resolve(flag_key, evaluation_context) typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]
]:
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:
raise FlagNotFoundError(f"Flag '{flag_key}' not found") return FlagResolutionDetails(
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,11 +1,18 @@
import typing from __future__ import annotations
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.hook import Hook from openfeature.provider import AbstractProvider
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"
@ -13,7 +20,7 @@ class NoOpProvider(AbstractProvider):
def get_metadata(self) -> Metadata: def get_metadata(self) -> Metadata:
return NoOpMetadata() return NoOpMetadata()
def get_provider_hooks(self) -> typing.List[Hook]: def get_provider_hooks(self) -> list[Hook]:
return [] return []
def resolve_boolean_details( def resolve_boolean_details(
@ -67,9 +74,13 @@ class NoOpProvider(AbstractProvider):
def resolve_object_details( def resolve_object_details(
self, self,
flag_key: str, flag_key: str,
default_value: typing.Union[dict, list], default_value: typing.Union[
Sequence[FlagValueType], typing.Mapping[str, FlagValueType]
],
evaluation_context: typing.Optional[EvaluationContext] = None, evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[typing.Union[dict, list]]: ) -> FlagResolutionDetails[
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

@ -0,0 +1,75 @@
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

@ -0,0 +1,19 @@
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

@ -0,0 +1,11 @@
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

@ -0,0 +1,13 @@
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,6 +1,10 @@
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,
) )
@ -8,4 +12,29 @@ 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.7.5" __version__ = "0.8.2"

View File

@ -1,11 +1,11 @@
# pyproject.toml # pyproject.toml
[build-system] [build-system]
requires = ["hatchling"] requires = ["uv_build~=0.8.0"]
build-backend = "hatchling.build" build-backend = "uv_build"
[project] [project]
name = "openfeature_sdk" name = "openfeature_sdk"
version = "0.7.5" version = "0.8.2"
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,50 +22,32 @@ keywords = [
"toggles", "toggles",
] ]
dependencies = [] dependencies = []
requires-python = ">=3.8" requires-python = ">=3.9"
[project.urls] [project.urls]
Homepage = "https://github.com/open-feature/python-sdk" Homepage = "https://github.com/open-feature/python-sdk"
[tool.hatch] [dependency-groups]
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.hatch.envs.default.scripts] [tool.uv]
test = "pytest {args:tests}" required-version = "~=0.8.0"
test-cov = "coverage run -m pytest {args:tests}"
cov-report = [
"coverage xml",
]
cov = [
"test-cov",
"cov-report",
]
e2e = [
"git submodule add --force https://github.com/open-feature/spec.git spec",
"cp spec/specification/assets/gherkin/* tests/features/",
"behave tests/features/",
"rm tests/features/*.feature",
]
[tool.hatch.build.targets.sdist] [tool.uv.build-backend]
exclude = [ module-name = "openfeature"
".gitignore", module-root = ""
"test-harness", namespace = true
"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
@ -73,6 +55,15 @@ 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",
@ -80,7 +71,7 @@ exclude = [
"__pycache__", "__pycache__",
"venv", "venv",
] ]
target-version = "py38" target-version = "py39"
[tool.ruff.lint] [tool.ruff.lint]
select = [ select = [
@ -123,3 +114,13 @@ 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,30 +1,15 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": [
"config:recommended" "github>open-feature/community-tooling"
], ],
"semanticCommits": "enabled",
"pep621": { "pep621": {
"enabled": true "enabled": true
}, },
"pre-commit": { "pre-commit": {
"enabled": true "enabled": true
}, },
"packageRules": [ "lockFileMaintenance": {
{ "enabled": true
"description": "Automerge non-major updates", }
"matchUpdateTypes": [
"minor",
"patch"
],
"matchCurrentVersion": "!/^0/",
"automerge": true
},
{
"matchManagers": [
"github-actions"
],
"automerge": true
}
]
} }

38
scripts/scripts.py Normal file
View File

@ -0,0 +1,38 @@
# 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 Submodule

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

View File

@ -1,8 +1,17 @@
import contextlib
from behave import given, when from behave import given, when
@given('a {flag_type}-flag with key "{flag_key}" and a default value "{default_value}"') @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): 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) context.flag = (flag_type, flag_key, default_value)

View File

@ -1,12 +1,13 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from behave import then, when from behave import given, then
from openfeature.exception import ErrorCode from openfeature.exception import ErrorCode
from openfeature.flag_evaluation import Reason
from openfeature.hook import Hook from openfeature.hook import Hook
@when("a hook is added to the client") @given("a client with added hook")
def step_impl_add_hook(context): def step_impl_add_hook(context):
hook = MagicMock(spec=Hook) hook = MagicMock(spec=Hook)
hook.before = MagicMock() hook.before = MagicMock()
@ -17,18 +18,23 @@ def step_impl_add_hook(context):
context.client.add_hooks([hook]) context.client.add_hooks([hook])
@then("error hooks should be called") @then('the "{hook_name}" hook should have been executed')
def step_impl_call_error(context): def step_impl_should_called(context, hook_name):
assert context.hook.before.called hook = get_hook_from_name(context, hook_name)
assert context.hook.error.called assert hook.called
assert context.hook.finally_after.called
@then("non-error hooks should be called") @then('the "{hook_names}" hooks should be called with evaluation details')
def step_impl_call_non_error(context): def step_impl_should_have_eval_details(context, hook_names):
assert context.hook.before.called for hook_name in hook_names.split(", "):
assert context.hook.after.called hook = get_hook_from_name(context, hook_name)
assert context.hook.finally_after.called 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): def get_hook_from_name(context, hook_name):
@ -44,8 +50,8 @@ def get_hook_from_name(context, hook_name):
raise ValueError(str(hook_name) + " is not a valid hook name") raise ValueError(str(hook_name) + " is not a valid hook name")
def convert_value_from_flag_type(value, flag_type): def convert_value_from_key_and_flag_type(value, key, flag_type):
if value == "None": if value in ("None", "null"):
return None return None
if flag_type.lower() == "boolean": if flag_type.lower() == "boolean":
return bool(value) return bool(value)
@ -53,20 +59,8 @@ def convert_value_from_flag_type(value, flag_type):
return int(value) return int(value)
elif flag_type.lower() == "float": elif flag_type.lower() == "float":
return float(value) return float(value)
elif key == "reason":
return Reason(value)
elif key == "error_code":
return ErrorCode(value)
return value return value
@then('"{hook_names}" hooks should have 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_flag_type(value, flag_type)
actual = hook.call_args[1]["details"].__dict__[key]
if isinstance(actual, ErrorCode):
actual = str(actual)
assert actual == value

View File

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

View File

@ -0,0 +1,197 @@
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

View File

@ -0,0 +1,101 @@
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,3 +1,4 @@
import inspect
import time import time
import uuid import uuid
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@ -7,11 +8,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 from openfeature.client import OpenFeatureClient, _typecheck_flag_value
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, Reason from openfeature.flag_evaluation import FlagResolutionDetails, FlagType, 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
@ -23,9 +24,13 @@ 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,
{ {
@ -35,21 +40,38 @@ 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",
),
), ),
) )
def test_should_get_flag_value_based_on_method_type( @pytest.mark.asyncio
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
flag = getattr(no_op_provider_client, get_method)( method = getattr(no_op_provider_client, get_method)
flag_key="Key", default_value=default_value if inspect.iscoroutinefunction(method):
) 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
@ -60,9 +82,13 @@ 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,
{ {
@ -72,38 +98,62 @@ 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",
),
), ),
) )
def test_should_get_flag_detail_based_on_method_type( @pytest.mark.asyncio
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
flag = getattr(no_op_provider_client, get_method)( method = getattr(no_op_provider_client, get_method)
flag_key="Key", default_value=default_value if inspect.iscoroutinefunction(method):
) 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)
def test_should_raise_exception_when_invalid_flag_type_provided(no_op_provider_client): @pytest.mark.asyncio
async def test_should_raise_exception_when_invalid_flag_type_provided(
no_op_provider_client,
):
# Given # Given
# When # When
flag = no_op_provider_client.evaluate_flag_details( flag_sync = 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
assert flag.value for flag in [flag_sync, flag_async]:
assert flag.error_message == "Unknown flag type" assert flag.value
assert flag.error_code == ErrorCode.GENERAL assert flag.error_message == "Unknown flag type"
assert flag.reason == Reason.ERROR assert flag.error_code == ErrorCode.GENERAL
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():
@ -202,7 +252,8 @@ def test_should_define_a_provider_status_accessor(no_op_provider_client):
# Requirement 1.7.6 # Requirement 1.7.6
def test_should_shortcircuit_if_provider_is_not_ready( @pytest.mark.asyncio
async def test_should_shortcircuit_if_provider_is_not_ready(
no_op_provider_client, monkeypatch no_op_provider_client, monkeypatch
): ):
# Given # Given
@ -212,20 +263,27 @@ 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 = no_op_provider_client.get_boolean_details( flag_details_sync = 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
assert flag_details is not None for flag_details in [flag_details_sync, flag_details_async]:
assert flag_details.value assert flag_details is not None
assert flag_details.reason == Reason.ERROR assert flag_details.value
assert flag_details.error_code == ErrorCode.PROVIDER_NOT_READY assert flag_details.reason == Reason.ERROR
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() spy_hook.finally_after.assert_called_once()
# Requirement 1.7.7 # Requirement 1.7.7
def test_should_shortcircuit_if_provider_is_in_irrecoverable_error_state( @pytest.mark.asyncio
async def test_should_shortcircuit_if_provider_is_in_irrecoverable_error_state(
no_op_provider_client, monkeypatch no_op_provider_client, monkeypatch
): ):
# Given # Given
@ -235,40 +293,98 @@ 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 = no_op_provider_client.get_boolean_details( flag_details_sync = 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
assert flag_details is not None for flag_details in [flag_details_sync, flag_details_async]:
assert flag_details.value assert flag_details is not None
assert flag_details.reason == Reason.ERROR assert flag_details.value
assert flag_details.error_code == ErrorCode.PROVIDER_FATAL 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.finally_after.assert_called_once() spy_hook.finally_after.assert_called_once()
def test_should_run_error_hooks_if_provider_returns_resolution_with_error_code(): @pytest.mark.asyncio
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 = []
provider.resolve_boolean_details.return_value = FlagResolutionDetails( mock_resolution = 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 = client.get_boolean_details(flag_key="Key", default_value=True) flag_details_sync = 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():
@ -422,12 +538,20 @@ 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"}
) )
client.get_boolean_details("flag", False, invocation_context) flag_input = "flag"
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 = args flag_key, default_value, context = (
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

@ -0,0 +1,51 @@
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 Normal file
View File

@ -0,0 +1,456 @@
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" },
]