Compare commits

...

297 Commits
v0.2.0 ... 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
OpenFeature Bot c2d1402641
chore(main): release 0.7.5 (#399)
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-01-31 11:35:08 -05:00
renovate[bot] 99905d57f8
chore(deps): update googleapis/release-please-action action to v4 (#428)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 16:24:56 +00:00
Michael Beemer 384c119d2e
ci: lower release please version to v3 (#427)
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-01-31 11:24:11 -05:00
renovate[bot] f72670689d
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.4 (#426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-30 18:52:31 +00:00
chrfwow 9e9bb5c626
feat: Add evaluation details to finally hook stage #403 (#423)
Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
Co-authored-by: Anton Grübel <anton.gruebel@gmail.com>
2025-01-30 13:51:19 -05:00
chrfwow 8f2cabaa32
fix: Finally hooks do not get called when the provider is not ready #424 (#425)
Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
2025-01-29 10:57:44 -05:00
renovate[bot] 9c2ed71c6e
chore(deps): update actions/setup-python digest to 4237552 (#422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-28 05:24:00 +00:00
renovate[bot] e99e481524
chore(deps): update codecov/codecov-action action to v5.3.1 (#421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-24 16:47:38 +00:00
renovate[bot] 8f9cc7ca96
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.3 (#419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 22:34:42 +00:00
renovate[bot] 6af37b1c2b
chore(deps): update codecov/codecov-action action to v5.3.0 (#420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 22:33:30 +00:00
chrfwow 192f7c40bd
feat: Update test harness (copy test files) #1467 (#416)
* feat: Update test harness (copy test files) #1467

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

* fixup! feat: Update test harness (copy test files) #1467

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

* fixup! feat: Update test harness (copy test files) #1467

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

* fixup! feat: Update test harness (copy test files) #1467

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

* fixup! feat: Update test harness (copy test files) #1467

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

* fixup! feat: Update test harness (copy test files) #1467

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

* fixup! feat: Update test harness (copy test files) #1467

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

* fixup! feat: Update test harness (copy test files) #1467

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

* fixup! feat: Update test harness (copy test files) #1467

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

* fixup! feat: Update test harness (copy test files) #1467

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

---------

Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
Co-authored-by: Anton Grübel <anton.gruebel@gmail.com>
2025-01-23 23:32:36 +01:00
renovate[bot] b69e81a636
chore(deps): update codecov/codecov-action action to v5.2.0 (#418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 16:47:23 +00:00
Michael Beemer 995e8b0394
ci: update release process (#417)
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-01-22 10:08:42 -05:00
chrfwow f559d1b27a
feat: Update test harness (add assertions) #1467 (#415)
* Update test harness (add assertions) #1467

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

* fixup! Update test harness (add assertions) #1467

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

* fixup! Update test harness (add assertions) #1467

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

* fixup! Update test harness (add assertions) #1467

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

* fixup! Update test harness (add assertions) #1467

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

* fixup! Update test harness (add assertions) #1467

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

* fixup! Update test harness (add assertions) #1467

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

---------

Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
Co-authored-by: Anton Grübel <anton.gruebel@gmail.com>
2025-01-21 15:11:10 +01:00
renovate[bot] 9304292ea9
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.2 (#414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 16:33:48 +00:00
renovate[bot] cbace6a24c
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.1 (#412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-10 21:24:06 +00:00
renovate[bot] bc6e333215
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.0 (#411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 19:23:15 +00:00
renovate[bot] 7f9d422497
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.8.6 (#410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 02:33:20 +00:00
renovate[bot] 2c1840c87d
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.14.1 (#409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-31 09:39:55 +00:00
renovate[bot] 26bc964227
chore(config): migrate renovate config (#408)
chore(config): migrate config renovate.json

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 10:03:00 +01:00
renovate[bot] 89d6997b1f
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.14.0 (#407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-21 10:26:40 +00:00
renovate[bot] 3296d3b229
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.8.4 (#406)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-19 17:12:10 +00:00
renovate[bot] 1c564804af
chore(deps): update codecov/codecov-action action to v5.1.2 (#405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 19:30:13 +00:00
renovate[bot] 01ec388d2d
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.8.3 (#404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-12 16:49:40 +00:00
renovate[bot] a6907d610e
chore(deps): update codecov/codecov-action action to v5.1.1 (#402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 22:01:03 +00:00
renovate[bot] 0459330cb9
chore(deps): update codecov/codecov-action action to v5.1.0 (#401)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 19:02:20 +00:00
renovate[bot] 2b6e210bc9
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.8.2 (#400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 14:23:45 +00:00
renovate[bot] 043385a836
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.8.1 (#398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-29 06:52:17 +00:00
OpenFeature Bot e6ada0f413
chore(main): release 0.7.4 (#397) 2024-11-25 10:29:26 +01:00
renovate[bot] cd737a9a6a
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.8.0 (#395)
* chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.8.0

* fix ruff issues

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>
2024-11-25 09:26:52 +00:00
Anton Grübel 70acd1dd42
build: pin pypi release GHA to v1.11 (#396)
pin pypi release GHA to v1.11

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-11-25 10:02:32 +01:00
OpenFeature Bot 398dcb04a0
chore(main): release 0.7.3 (#382) 2024-11-25 09:39:03 +01:00
Lukas Reining 9b97130908
feat: implement transaction context (#389)
* feat: implement transaction context

Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>

* fix: lint issues

Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>

* feat: add tests for context merging

Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>

* feat: fix pre-commit checks

Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>

* feat: use elipsis instead of pass

Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>

* Update openfeature/transaction_context/no_op_transaction_context_propagator.py

Co-authored-by: Anton Grübel <anton.gruebel@gmail.com>
Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>

* feat: pr feedback

Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>

---------

Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>
Co-authored-by: Anton Grübel <anton.gruebel@gmail.com>
2024-11-24 17:54:42 +01:00
renovate[bot] f024a6f340
chore(deps): update codecov/codecov-action action to v5.0.7 (#394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-21 04:42:17 +00:00
renovate[bot] 24970b5d09
chore(deps): update codecov/codecov-action action to v5.0.6 (#393)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 23:17:46 +00:00
renovate[bot] a652004fde
chore(deps): update codecov/codecov-action action to v5.0.5 (#392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 16:49:21 +00:00
renovate[bot] 9759bfaa21
chore(deps): update codecov/codecov-action action to v5.0.4 (#391)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 05:11:05 +00:00
renovate[bot] 3fb7cbc3dd
chore(deps): update codecov/codecov-action action to v5.0.3 (#390)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-19 22:18:49 +00:00
renovate[bot] e72e329a2a
chore(deps): update codecov/codecov-action action to v5.0.2 (#388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 17:19:38 +00:00
renovate[bot] 3d77f24107
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.7.4 (#387)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 13:15:19 +00:00
renovate[bot] 6cd570f64c
chore(deps): update codecov/codecov-action action to v5 (#386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 18:24:06 +00:00
renovate[bot] 1eb6f4c655
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.7.3 (#384)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 13:43:38 +00:00
renovate[bot] 2d8fadf40a
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.7.2 (#381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-01 20:01:05 +00:00
OpenFeature Bot 05dc3be00f
chore(main): release 0.7.2 (#358) 2024-10-25 09:50:22 -04:00
renovate[bot] c3d70ec457
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.7.1 (#380)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 17:40:30 +00:00
renovate[bot] f5aaac0525
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.13.0 (#379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 10:12:30 +00:00
renovate[bot] 091656b653
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.12.1 (#378)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-20 10:53:19 +00:00
renovate[bot] 157e1baddb
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.7.0 (#377)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 20:47:46 +00:00
renovate[bot] 00f026d4f1
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.12.0 (#376)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 09:53:15 +00:00
renovate[bot] 7a6b3c78cc
chore(deps): update pre-commit hook pre-commit/pre-commit-hooks to v5 (#373)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-08 07:34:34 +02:00
renovate[bot] 977cd6d4b7
chore(deps): update python docker tag to v3.13 (#375)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-08 03:06:40 +00:00
renovate[bot] d2c6b401cd
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.6.9 (#372)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 15:17:25 +00:00
renovate[bot] 45d2a6daca
chore(deps): update codecov/codecov-action action to v4.6.0 (#371)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 15:46:39 +00:00
renovate[bot] dcdb56c43f
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.6.8 (#370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-26 14:08:16 +00:00
renovate[bot] 65b0f1a374
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.6.7 (#369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-21 19:34:57 +00:00
renovate[bot] a334f20251
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.6.6 (#367)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-20 05:00:40 +00:00
renovate[bot] 4959144a50
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.6.5 (#366)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-13 17:19:51 +00:00
Anton Grübel ca4d589456
chore: add Python 3.13 (#364)
Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-09-09 12:19:44 -04:00
renovate[bot] e57ff78306
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.6.4 (#365)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-05 18:19:00 +00:00
renovate[bot] 6e316a216a
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.6.3 (#363)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-29 20:19:52 +02:00
renovate[bot] f38ff919bd
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.11.2 (#362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-25 15:48:10 +02:00
renovate[bot] 8e6a530040
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.6.2 (#361)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 17:07:25 +00:00
renovate[bot] 90d417dbd3
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.6.1 (#360)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-16 23:31:19 +02:00
renovate[bot] e538f86e76
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.6.0 (#359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 17:42:33 +00:00
renovate[bot] a6914808b8
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.5.7 (#357)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-08 18:31:33 +00:00
github-actions[bot] 26b7114f4b
chore(main): release 0.7.1 (#328)
Co-authored-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>
2024-08-02 13:33:58 -04:00
renovate[bot] 261aa4168e
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.5.6 (#356)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-02 19:24:54 +02:00
renovate[bot] 62c4b672f6
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.11.1 (#355)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-31 15:58:20 +02:00
Michael Beemer d618fabe78
ci: run release please as OpenFeature bot (#354)
* Update release.yml

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

* Update release-please-config.json

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

---------

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2024-07-30 21:04:40 -04:00
renovate[bot] 6d46d957bd
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.5.5 (#353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-25 20:02:34 +02:00
renovate[bot] c29468941b
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.5.4 (#352)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-20 23:22:54 +02:00
renovate[bot] fe63b64d8f
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.5.3 (#350)
* chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.5.3

* fix linting

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>
2024-07-20 14:42:21 +02:00
renovate[bot] 931e0cb3a8
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.11.0 (#351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-20 13:01:32 +02:00
renovate[bot] 299a4f4630
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.5.2 (#349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 20:03:38 +02:00
renovate[bot] 5dff1e89b2
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.5.1 (#348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-05 15:07:35 +00:00
Michael Beemer 0ed625f186
fix: remove exception logging during evaluation (#347)
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2024-07-02 11:37:39 -04:00
renovate[bot] 5c7bd14b41
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.5.0 (#346)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-27 18:03:14 +00:00
renovate[bot] b553dfa607
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.10.1 (#345)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 11:53:16 +02:00
renovate[bot] 2a45af895c
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.10 (#344)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-20 21:12:12 +02:00
renovate[bot] f3982dc8c6
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.9 (#342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-14 17:54:00 +02:00
renovate[bot] e6a353e475
chore(deps): update codecov/codecov-action action to v4.5.0 (#341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-13 12:27:56 +00:00
Anton Grübel 5abcf3b157
fix: make global hooks thread safe (#331)
Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-06-07 10:53:51 -04:00
Anton Grübel f2389da024
ci: update release please to new GHA (#340)
update release please to new GHA

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-06-07 00:22:49 +02:00
renovate[bot] 44b07879b0
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.8 (#339)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-05 18:39:37 +00:00
renovate[bot] 1bf4682b46
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.7 (#338)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-01 03:18:19 +00:00
renovate[bot] cf61e5b682
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.6 (#337)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-29 01:00:56 +00:00
renovate[bot] 2f93524063
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.5 (#336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-23 07:22:34 +02:00
renovate[bot] fa677092f8
chore(deps): update codecov/codecov-action action to v4.4.1 (#335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-20 22:51:58 +02:00
renovate[bot] 6acbef94e6
chore(deps): update codecov/codecov-action action to v4.4.0 (#334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 00:13:57 +00:00
renovate[bot] bd0bc1e2b7
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.4 (#333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-09 21:58:13 +02:00
renovate[bot] f8544ffaf6
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.3 (#330)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-04 10:47:16 +10:00
Federico Bond 3217575f4f
fix: event handler methods are not thread-safe (#329)
The _client_handlers dictionary allowed modifications during iteration
without proper concurrency control. I added some reentrant locks to manage
concurrent access to the _global_handlers and _client_handlers data
structures.

See #326

Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-05-03 00:42:43 +10:00
Federico Bond c3ad697a80
refactor: bind providers explicitly to a registry with attach/detach (#324)
* test: make sure provider is registered in events test

Signed-off-by: Federico Bond <federicobond@gmail.com>

* refactor: bind providers explicitly to a registry with attach/detach

Signed-off-by: Federico Bond <federicobond@gmail.com>

---------

Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-05-02 09:13:30 +10:00
renovate[bot] f352045055
chore(deps): update codecov/codecov-action action to v4.3.1 (#327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-01 11:43:21 -04:00
github-actions[bot] 3b967a9a3e
chore(main): release 0.7.0 (#308)
chore(main): release 0.7.0

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-04-30 11:08:50 +10:00
renovate[bot] f109df671c
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.2 (#323)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-25 23:17:11 +02:00
renovate[bot] 6dedd275cf
chore(deps): update pre-commit hook pre-commit/mirrors-mypy to v1.10.0 (#322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-25 20:27:33 +10:00
renovate[bot] 44f12239ff
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.1 (#321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-19 21:27:05 +02:00
renovate[bot] abb14f5ed9
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.4.0 (#320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-19 00:15:20 +00:00
renovate[bot] 563662054c
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.3.7 (#318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-12 15:42:09 +10:00
Federico Bond 96ba7938de
refactor!: move AbstractProvider to openfeature.provider (#314)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-04-12 11:19:21 +10:00
Federico Bond cd605c4f5d
chore: update codecov/codecov-action action to v4 (#317)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-04-12 09:53:57 +09:00
renovate[bot] 49f0948e51
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.3.6 (#316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-11 21:45:00 +00:00
Federico Bond 34ac91c707
fix!: restrict exported names with __all__ (#306)
* fix!: restrict exported names with __all__

Signed-off-by: Federico Bond <federicobond@gmail.com>

* restrict codecov upload to Python 3.11

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

* disable codecov ci fail on error

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

---------

Signed-off-by: Federico Bond <federicobond@gmail.com>
Signed-off-by: gruebel <anton.gruebel@gmail.com>
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
Co-authored-by: gruebel <anton.gruebel@gmail.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2024-04-09 11:15:30 +09:00
Federico Bond 9966c14e16
feat: update provider status when provider emits events (#309)
* refactor: move registry singleton to the registry module

Signed-off-by: Federico Bond <federicobond@gmail.com>

* refactor: make openfeature.provider.registry a private module

Signed-off-by: Federico Bond <federicobond@gmail.com>

* feat: update provider status when provider emits events

Signed-off-by: Federico Bond <federicobond@gmail.com>

* refactor: avoid duplicate code

Signed-off-by: Federico Bond <federicobond@gmail.com>

* fix: fix provider event dispatch on initialize/shutdown

Signed-off-by: Federico Bond <federicobond@gmail.com>

* refactor: rename default_registry to provider_registry

Signed-off-by: Federico Bond <federicobond@gmail.com>

---------

Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-04-07 22:59:02 +10:00
renovate[bot] faf02a9888
chore(deps): update pre-commit hook pre-commit/pre-commit-hooks to v4.6.0 (#312)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-06 21:34:15 +00:00
Anton Grübel 9ba82e3b63
ci: switch to hatch (#297)
switch to hatch

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-04-05 18:08:16 +11:00
renovate[bot] 47ae16c167
chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.3.5 (#311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-05 12:48:51 +11:00
Federico Bond ff626374ae
chore: update renovate config (#310)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-04-05 12:46:02 +11:00
Federico Bond de36b214dc
fix: remove ProviderEvent.PROVIDER_FATAL (#307)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-03-28 16:54:00 +11:00
github-actions[bot] f00bc89caa
chore(main): release 0.6.1 (#304) 2024-03-28 00:10:46 +11:00
Federico Bond 05d0da2e3d
chore: add keywords to pyproject.toml (#305)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-03-26 07:31:57 +01:00
Federico Bond 78ea3b9914
feat: populate provider and client metadata in HookContext (#302)
* feat: populate provider and client metadata in HookContext

Signed-off-by: Federico Bond <federicobond@gmail.com>

* fix: ensure provider consistency during flag evaluation

Signed-off-by: Federico Bond <federicobond@gmail.com>

---------

Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-03-25 18:15:26 +11:00
Federico Bond 4a323b0f96
refactor: mark hook_support module as private/internal (#303)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-03-25 18:02:36 +11:00
github-actions[bot] 2c23c9e971
chore(main): release 0.6.0 (#283)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-03-22 15:30:50 +01:00
Federico Bond 58d27c4011
docs: update spec version to 0.8.0 (#299)
Release-As: 0.6.0

Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-03-22 08:06:08 +01:00
renovate[bot] a70ae0cb2e
chore(deps): update dependency pytest-mock to v3.14.0 (#300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-22 13:25:00 +11:00
Federico Bond 679409fad2
feat: implement provider events (#278)
* feat: implement provider events

Signed-off-by: Federico Bond <federicobond@gmail.com>

* feat: add error_code field to EventDetails and ProviderEventDetails

Signed-off-by: Federico Bond <federicobond@gmail.com>

* fix: replace strings with postponed evaluation of annotations

Signed-off-by: Federico Bond <federicobond@gmail.com>

* feat: run handlers immediately if provider already in associated state

Signed-off-by: Federico Bond <federicobond@gmail.com>

* feat: remove unused _provider from openfeature.api

Signed-off-by: Federico Bond <federicobond@gmail.com>

* test: add some comments to test cases

Signed-off-by: Federico Bond <federicobond@gmail.com>

* test: add provider event late binding test cases

Signed-off-by: Federico Bond <federicobond@gmail.com>

* fix: fix status handlers running immediately if provider already in associated state

Signed-off-by: Federico Bond <federicobond@gmail.com>

* refactor: reuse provider property in OpenFeatureClient

Signed-off-by: Federico Bond <federicobond@gmail.com>

* refactor: move _provider_status_to_event to ProviderEvent.from_provider_status

Signed-off-by: Federico Bond <federicobond@gmail.com>

* refactor: move EventSupport class to an internal module

Signed-off-by: Federico Bond <federicobond@gmail.com>

* refactor: replace EventSupport class with module-level functions

Signed-off-by: Federico Bond <federicobond@gmail.com>

* style: fix code style

---------

Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-03-22 07:45:00 +11:00
renovate[bot] 04b4009dbf
chore(deps): update dependency pytest-mock to v3.13.0 (#298)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-22 07:22:24 +11:00
Anton Grübel 6e4eebce20
chore: update mypy and ruff (#296)
update mypy and ruff

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-03-19 06:16:16 +11:00
renovate[bot] f5987ef8f4
chore(deps): update dependency coverage to v7.4.4 (#293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-15 07:41:41 +11:00
Federico Bond e7475441bd
fix: run error hooks if provider returns FlagResolutionDetails with non-empty error_code (#291)
* fix: run error hooks if provider returns FlagResolutionDetails with non-empty error_code

Signed-off-by: Federico Bond <federicobond@gmail.com>

* refactor: extract error code to exception mapping to class variable

Signed-off-by: Federico Bond <federicobond@gmail.com>

---------

Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-03-14 08:31:46 +11:00
renovate[bot] 3f336b3a24
chore(deps): update dependency pytest to v8.1.1 (#289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-09 16:47:21 +01:00
Federico Bond 789e6e0f5f
feat: implement provider status (#288)
* feat: implement provider status

Signed-off-by: Federico Bond <federicobond@gmail.com>

* feat: set provider status to fatal if initialize raises PROVIDER_FATAL error

Signed-off-by: Federico Bond <federicobond@gmail.com>

* feat: add a provider status accessor to clients

Signed-off-by: Federico Bond <federicobond@gmail.com>

* feat: short circuit flag resolution when provider is not ready

Signed-off-by: Federico Bond <federicobond@gmail.com>

---------

Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-03-08 09:00:32 +11:00
renovate[bot] 7ba7d6146f
chore(deps): update dependency pytest to v8.1.0 (#287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-03 23:06:47 +01:00
Zhiwei ae26217328
docs: add Missing Imports in Provider Dev Example in README (#286)
docs: add missing imports in provider dev example in README

Signed-off-by: Zhiwei <zhi.wei.liang@outlook.com>
2024-03-03 12:04:45 -03:00
Anton Grübel 5acd6a6598
refactor: improve Hook Hints typing (#285)
* improve Hook Hints typing

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

* ignore lint issue for this line

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

* exclude TYPE_CHECKING from coverage report

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

---------

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-03-03 00:38:14 -03:00
Todd Baert 141858d235
chore: add changelog sections (#282)
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2024-02-26 20:56:00 -03:00
renovate[bot] b2594a567c
chore(deps): update dependency pytest to v8.0.2 (#281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-25 00:45:32 -03:00
renovate[bot] bafa427a0d
chore(deps): update dependency coverage to v7.4.3 (#280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-24 00:25:56 +01:00
github-actions[bot] 035d0ad679
chore: release 0.5.0 (#277)
chore(main): release 0.5.0

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-21 16:23:33 -05:00
Federico Bond ed6a42f264
feat!: add support for domains (#271)
* feat: add support for domains

Signed-off-by: Federico Bond <federicobond@gmail.com>

* docs: update README.md

Signed-off-by: Federico Bond <federicobond@gmail.com>

* feat: add clear_providers function to api

Signed-off-by: Federico Bond <federicobond@gmail.com>

* feat: make _get_provider function private

Signed-off-by: Federico Bond <federicobond@gmail.com>

* fix: shutdown all providers on api.shutdown

Signed-off-by: Federico Bond <federicobond@gmail.com>

* refactor: move provider dict to a ProviderRegistry class

Signed-off-by: Federico Bond <federicobond@gmail.com>

* feat: reset default provider on clear_providers and add tests

Signed-off-by: Federico Bond <federicobond@gmail.com>

* docs: update README.md

Signed-off-by: Federico Bond <federicobond@gmail.com>

---------

Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-02-20 19:31:22 -03:00
renovate[bot] 0ec2b69d1e
chore(deps): update dependency coverage to v7.4.2 (#276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-20 21:48:09 +00:00
renovate[bot] 2b177e6ab5
chore(deps): update dependency pytest to v8.0.1 (#275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-17 04:24:21 +00:00
Federico Bond 77fbae7b1e
refactor: remove abstractmethod decorator from get_provider_hooks (#274)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-02-09 18:50:28 -03:00
Federico Bond f9833ba753
chore: update ruff version to 0.2.1 and remove preview flag (#272)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-02-07 20:08:59 -03:00
renovate[bot] 915cabe5b1
chore(deps): update pre-commit/action action to v3.0.1 (#273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-07 19:52:15 +00:00
github-actions[bot] 522d425a06
chore(main): release 0.4.2 (#238)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-06 07:37:38 -08:00
Federico Bond cb1677b0a8
feat: make return value not optional in provider API functions (#270)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-02-06 11:15:49 -03:00
Federico Bond 1282bab31e
docs: update supported spec version (#269)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-02-05 20:49:20 -08:00
Federico Bond caa7f36c30
feat: add FeatureProvider protocol (#268)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-02-03 11:26:18 -03:00
Anton Grübel 3b89760d41
feat: make specific fields in HookContext immutable (#266)
make specific fields in HookContext immutable

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-01-29 13:16:10 -03:00
Matthew M. Keeler 5ef6ca1263
fix: Allow string values for `FlagEvaluationDetails.reason` and `FlagResolutionDetails.reason` (#264)
* fix: Allow string values for `FlagEvaluationDetails.reason` and ``FlagResolutionDetails.reason`

Signed-off-by: Matthew Keeler <mkeeler@launchdarkly.com>

* Remove useless test

Signed-off-by: Matthew Keeler <mkeeler@launchdarkly.com>

---------

Signed-off-by: Matthew Keeler <mkeeler@launchdarkly.com>
2024-01-29 12:42:37 -03:00
renovate[bot] f1b0839d16
chore(deps): update dependency pytest to v8 (#265)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-29 10:37:07 -03:00
Federico Bond ccbff2c5e4
feat: improve logging setup (#261)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2024-01-26 23:52:33 -03:00
renovate[bot] 15ce8f9b56
chore(deps): update dependency coverage to v7.4.1 (#263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-26 22:44:44 +00:00
renovate[bot] d1f27e3278
chore(deps): update actions/cache action to v4 (#260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-17 18:26:28 -03:00
Anton Grübel a1c25e241b
ci: change codecov GHA version to v3 (#259)
change codecov GHA version to v3

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-01-16 17:13:13 -03:00
Anton Grübel 1722848651
ci: use pypi trusted publishing (#258)
use pypi trusted publishing

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-01-11 16:12:47 -05:00
Michael Beemer 4883ab47d8
ci: fix missing CodeQL workflow permissions
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2024-01-09 13:36:55 -05:00
Anton Grübel af9d3da336
chore: enable mypy strict mode (#257)
Signed-off-by: gruebel <anton.gruebel@gmail.com>
Co-authored-by: Federico Bond <federicobond@gmail.com>
2024-01-08 20:09:53 -03:00
Anton Grübel 30f4e692d8
ci: split lint job into lint and sast (#256)
split lint job into lint and sast

Signed-off-by: gruebel <anton.gruebel@gmail.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2024-01-08 19:56:12 -03:00
Anton Grübel b3c67b6ab3
ci: add mypy type checking and fix/exclude minor issues (#255)
* add mypy type checking and fix/exclude minor issues

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

* enable explicit_package_bases for mypy

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

---------

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-01-07 12:56:32 -03:00
Anton Grübel a853b85514
chore: remove excluded ruff rules and fix issues (#254)
remove excluded ruff rules and fix issues

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-01-06 14:25:44 -03:00
Anton Grübel 49aae786fb
ci: ignore tests in CodeQL scan and add missing permission restrictions (#253)
ignore tests in CodeQL scan and add missing permission restrictions

Signed-off-by: gruebel <anton.gruebel@gmail.com>
2024-01-06 11:21:40 -03:00
Anton Grübel 3b6204daec
chore: replace black, fake8 and isort with ruff and ruff-format (#249)
* replace black, fake8 and isort with ruff and ruff-format

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

* fix workflow

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

* use full version tag for pre-commit/action action

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

---------

Signed-off-by: gruebel <anton.gruebel@gmail.com>
Signed-off-by: Anton Grübel <anton.gruebel@gmail.com>
2024-01-05 19:45:26 -05:00
renovate[bot] ab50012460
chore(deps): update dependency pytest to v7.4.4 (#250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-31 15:18:16 +00:00
renovate[bot] 3176be5327
chore(deps): update dependency coverage to v7.4.0 (#248)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-28 03:11:54 +00:00
renovate[bot] c9a7bb15e3
chore(deps): update dependency black to v23.12.1 (#246)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-23 03:25:32 +00:00
renovate[bot] fb11280b66
chore(deps): update dependency coverage to v7.3.4 (#245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-20 22:48:55 +00:00
renovate[bot] 270a34c6b8
chore(deps): update dependency coverage to v7.3.3 (#244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-14 23:22:17 +00:00
renovate[bot] 4c416b434c
chore(deps): update github/codeql-action action to v3 (#243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-13 21:19:13 +00:00
renovate[bot] 48bb2a383c
chore(deps): update dependency pylint to v3.0.3 (#241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-12 12:35:46 +00:00
renovate[bot] b5a6a70134
chore(deps): update dependency black to v23.12.0 (#242)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-12 09:31:14 +00:00
renovate[bot] 4b59b65ebe
chore(deps): update actions/setup-python action to v5 (#240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-06 16:44:28 +00:00
renovate[bot] 56284e36fa
chore(deps): update google-github-actions/release-please-action action to v4 (#239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-02 02:21:11 +00:00
Federico Bond 95d69e27b3
docs: document shutdown function (#237)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2023-11-13 21:34:09 -06:00
github-actions[bot] e24a6347a1
chore(main): release 0.4.1 (#235)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-08 06:17:29 -06:00
renovate[bot] 3001cf9b0f
chore(deps): update dependency black to v23.11.0 (#236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 11:21:38 +00:00
Federico Bond 963b01e66d
fix: replace str with enum value in InMemoryFlag definition (#234)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2023-11-07 09:51:24 -06:00
Federico Bond 4bdd384544
fix: fix types for HookContext.{client,provider}_metadata (#233)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2023-11-07 09:44:54 -06:00
Federico Bond db504946d1
fix: add PEP 561 py.typed marker file (#232)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2023-11-07 09:33:22 -06:00
github-actions[bot] d558bb1366
chore(main): release 0.4.0 (#230)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2023-11-01 15:50:12 -04:00
Michael Beemer 107663a9d1
chore: update readme link in pyproject.toml
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2023-11-01 15:27:42 -04:00
Michael Beemer a25af4854a
chore: update release please extra files config
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2023-11-01 15:05:44 -04:00
Todd Baert 1864a3fa57
chore: update readme based on latest template (#227)
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
Co-authored-by: Federico Bond <federicobond@gmail.com>
2023-11-01 15:02:46 -04:00
Michael Beemer 0c314ab77c
fix!: raise error if the flag wasn't found using the in-memory provider (#228)
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2023-11-01 14:56:48 -04:00
Federico Bond f74eda06bd
chore: optimize GitHub CI workflow (#220)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2023-10-24 19:39:06 -04:00
renovate[bot] 291b4ae7d4
chore(deps): update dependency pytest to v7.4.3 (#224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-24 18:38:11 -03:00
renovate[bot] 1a804ee675
chore(deps): update dependency black to v23.10.1 (#223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-23 21:45:19 -03:00
renovate[bot] 0b889b3a81
chore(deps): update dependency pytest-mock to v3.12.0 (#221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-22 19:42:03 +00:00
renovate[bot] 1d3db126ac
chore(deps): update dependency pylint to v3.0.2 (#222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-22 13:13:41 -03:00
Federico Bond 64f57fdcd4
refactor: use if clauses in list comprehensions to make code more pythonic (#215)
Signed-off-by: Federico Bond <federicobond@gmail.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2023-10-19 17:38:11 -03:00
Federico Bond c661ab20a4
fix: Hook methods should have default non-abstract implementations (#216)
* fix: Hook methods should have default non-abstract implementations

Signed-off-by: Federico Bond <federicobond@gmail.com>

* fix: use correct return type for Hook.before method

Signed-off-by: Federico Bond <federicobond@gmail.com>

* feat: make EvaluationContext a dataclass

Signed-off-by: Federico Bond <federicobond@gmail.com>

* test: add unit test for evaluation context merging in before_hooks

Signed-off-by: Federico Bond <federicobond@gmail.com>

---------

Signed-off-by: Federico Bond <federicobond@gmail.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2023-10-18 12:30:29 -03:00
Federico Bond 84af1aec01
feat: implement initialize/shutdown on provider registration (#213)
Signed-off-by: Federico Bond <federicobond@gmail.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2023-10-18 12:04:09 -03:00
Federico Bond 88a204dc27
feat: pass flag_metadata from resolution to evaluation details (#212)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2023-10-18 10:36:30 -04:00
renovate[bot] d41cea270e
chore(deps): update dependency pylint to v3 (#206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 01:17:15 -03:00
renovate[bot] 54c018ff85
chore(deps): update dependency black to v23.10.0 (#211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 01:01:44 -03:00
Federico Bond 4314ef7003
chore: simplify build and dependencies configuration (#208)
* chore: simplify build and dependencies configuration

Signed-off-by: Federico Bond <federicobond@gmail.com>

* chore: remove unused .env.template

Signed-off-by: Federico Bond <federicobond@gmail.com>

* chore: remove duplicate hook_support.py module

Signed-off-by: Federico Bond <federicobond@gmail.com>

* chore: update pre-commit config and fix format issues

Signed-off-by: Federico Bond <federicobond@gmail.com>

* chore: update github workflows

Signed-off-by: Federico Bond <federicobond@gmail.com>

* chore: update dependencies

Signed-off-by: Federico Bond <federicobond@gmail.com>

* chore: replace virtualenv with built-in venv everywhere

Signed-off-by: Federico Bond <federicobond@gmail.com>

---------

Signed-off-by: Federico Bond <federicobond@gmail.com>
2023-10-18 00:41:54 -03:00
renovate[bot] 059b54a298
chore(deps): update dependency pre-commit to v3.5.0 (#210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-13 19:56:31 +00:00
renovate[bot] af4955dbde
chore(deps): update python docker tag to v3.12 (#207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-13 08:08:47 +00:00
renovate[bot] a53b4a84be
chore(deps): update dependency pycodestyle to v2.11.1 (#209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-13 04:36:09 +00:00
renovate[bot] 6fde2a0087
chore(deps): update dependency platformdirs to v3.11.0 (#204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-03 00:34:42 +00:00
renovate[bot] 054d674deb
chore(deps): update dependency coverage to v7.3.2 (#205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-02 22:11:26 +00:00
renovate[bot] c15e3a0f5b
chore(deps): update dependency packaging to v23.2 (#203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-01 18:46:40 +00:00
renovate[bot] 2f0617b379
chore(deps): update dependency pylint to v2.17.7 (#202)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-01 01:12:45 +00:00
renovate[bot] 78bc5a85de
chore(deps): update dependency identify to v2.5.30 (#201)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-30 21:57:26 +00:00
github-actions[bot] 544ae0e0ff
chore(main): release 0.3.1 (#200)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-09-27 22:42:36 -04:00
Federico Bond c544918d65
feat: make openfeature an implicit namespace package (#199)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2023-09-27 22:40:25 -04:00
renovate[bot] 6e87a36995
chore(deps): update dependency astroid to v2.15.8 (#197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-26 19:10:27 +00:00
github-actions[bot] 5237e935d5
chore(main): release 0.3.0 (#194)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-09-25 13:45:04 -04:00
renovate[bot] eef4e159ed
chore(deps): update dependency pylint to v2.17.6 (#196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-25 15:55:55 +00:00
renovate[bot] 43274eb6d0
chore(deps): update dependency astroid to v2.15.7 (#195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-24 00:02:19 +00:00
Federico Bond 51f0d260f0
refactor!: rename top-level package to openfeature (#192)
Signed-off-by: Federico Bond <federicobond@gmail.com>
2023-09-20 13:22:31 -03:00
Manuel Schönlaub 25f6a87bd2
chore(deps): update dependency pyflakes to v3.1.0 (#184)
This updates pyflakes and associated libraries

chore(deps): update dependency pycodestyle to v2.11.0

chore(deps): update dependency flake8 to v6.1.0

Signed-off-by: Manuel Schönlaub <manuel.schonlaub@prodigygame.com>
2023-09-19 11:26:26 -04:00
renovate[bot] cb8c671d70
chore(deps): update dependency identify to v2.5.29 (#193)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-16 01:31:14 +00:00
renovate[bot] 81c7429deb
chore(deps): update dependency filelock to v3.12.4 (#191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-13 18:45:37 +00:00
renovate[bot] d7b43bccd5
chore(deps): update codecov/codecov-action digest to c9e0f0b (#189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-13 04:32:59 +00:00
renovate[bot] 1c5cca29f1
chore(deps): update codecov/codecov-action digest to 8e29a53 (#188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 21:03:20 +00:00
renovate[bot] 8fad269b30
chore(deps): update dependency identify to v2.5.28 (#187)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-11 20:23:05 -06:00
renovate[bot] d6b4c3dc5d
chore(deps): update dependency black to v23.9.1 (#186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-11 06:42:48 +00:00
86 changed files with 5944 additions and 1831 deletions

View File

@ -1,3 +0,0 @@
[flake8]
max-line-length = 88
exclude = .venv/*,venv/*,.git,__pycache__

4
.github/codeql-config.yml vendored Normal file
View File

@ -0,0 +1,4 @@
name: "CodeQL config"
paths-ignore:
- tests

92
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,92 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: "Build, lint, and test"
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
submodules: recursive
- name: Install uv and set the python version
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --frozen
- name: Test with pytest
run: uv run cov --frozen
- name: Run E2E tests with behave
run: uv run e2e --frozen
- if: matrix.python-version == '3.13'
name: Upload coverage to Codecov
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
with:
flags: unittests # optional
name: coverage # optional
fail_ci_if_error: true # optional (default = false)
verbose: true # optional (default = false)
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install uv and set the python version
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen
- name: Run pre-commit
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
sast:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install uv and set the python version
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6
with:
python-version: "3.13"
ignore-nothing-to-cache: true
- name: Initialize CodeQL
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3
with:
languages: python
config-file: ./.github/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3

View File

@ -12,11 +12,14 @@ on:
- edited
- synchronize
permissions:
pull-requests: read
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,75 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Merge
on:
push:
branches: [ master, main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
container: [ "python:3.8", "python:3.9", "python:3.10" ]
container:
image: ${{ matrix.container }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Cache virtualenvironment
uses: actions/cache@v3
with:
path: ~/.venv
key: ${{ hashFiles('requirements.txt', 'requirements-dev.txt') }}
- name: Upgrade pip
run: pip install --upgrade pip
- name: Create and activate Virtualenv
run: |
pip install virtualenv
[ ! -d ".venv" ] && virtualenv .venv
. .venv/bin/activate
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: python
- name: Install dependencies
run: pip install -r requirements-dev.txt
- name: Run black formatter check
run: black --check .
- name: Run flake8 formatter check
run: flake8 .
- name: Run isort formatter check
run: isort .
- name: Test with pytest
run: coverage run --omit="*/test*" -m pytest
- name: Upload coverage to Codecov
uses: codecov/codecov-action@398b9de041a7e69750d45077b10c5912201a3466
with:
flags: unittests # optional
name: coverage # optional
fail_ci_if_error: true # optional (default = false)
verbose: true # optional (default = false)
- name: Run E2E tests with behave
run: |
cp test-harness/features/evaluation.feature tests/features/
behave tests/features/
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@ -1,78 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: PR
on:
pull_request:
branches: [ master, main ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
container: [ "python:3.8", "python:3.9", "python:3.10", "python:3.11" ]
container:
image: ${{ matrix.container }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Cache virtualenvironment
uses: actions/cache@v3
with:
path: ~/.venv
key: ${{ hashFiles('requirements.txt', 'requirements-dev.txt') }}
- name: Upgrade pip
run: pip install --upgrade pip
- name: Create and activate Virtualenv
run: |
pip install virtualenv
[ ! -d ".venv" ] && virtualenv .venv
. .venv/bin/activate
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: python
- name: Install dependencies
run: pip install -r requirements-dev.txt
- name: Run black formatter check
run: black --check .
- name: Run flake8 formatter check
run: flake8 .
- name: Run isort formatter check
run: isort .
- name: Test with pytest
run: coverage run --omit="*/test*" -m pytest
- name: Upload coverage to Codecov
uses: codecov/codecov-action@398b9de041a7e69750d45077b10c5912201a3466
with:
flags: unittests # optional
name: coverage # optional
fail_ci_if_error: true # optional (default = false)
verbose: true # optional (default = false)
- name: Run E2E tests with behave
run: |
cp test-harness/features/evaluation.feature tests/features/
behave tests/features/
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@ -10,72 +10,50 @@ on:
branches:
- main
permissions: # added using https://github.com/step-security/secure-workflows
permissions: # added using https://github.com/step-security/secure-workflows
contents: read
jobs:
release-please:
permissions:
contents: write # for google-github-actions/release-please-action to create release commit
pull-requests: write # for google-github-actions/release-please-action to create release PR
contents: write # for googleapis/release-please-action to create release commit
pull-requests: write # for googleapis/release-please-action to create release PR
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v3
- uses: googleapis/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3
id: release
with:
command: manifest
token: ${{secrets.GITHUB_TOKEN}}
token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}}
default-branch: main
signoff: "OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>"
outputs:
release_created: ${{ steps.release.outputs.release_created }}
release_tag_name: ${{ steps.release.outputs.tag_name }}
release:
runs-on: ubuntu-latest
environment: publish
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing to pypi
id-token: write
needs: release-please
if: ${{ needs.release-please.outputs.release_created }}
container:
image: "python:3.11"
if: ${{ fromJSON(needs.release-please.outputs.release_created || false) }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Cache virtualenvironment
uses: actions/cache@v3
- name: Install uv and set the python version
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6
with:
path: ~/.venv
key: ${{ hashFiles('requirements.txt', 'requirements-dev.txt') }}
- name: Upgrade pip
run: pip install --upgrade pip
- name: Create and activate Virtualenv
run: |
pip install virtualenv
[ ! -d ".venv" ] && virtualenv .venv
. .venv/bin/activate
python-version: "3.13"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
run: uv sync --frozen
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
run: uv build
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}

6
.gitmodules vendored
View File

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

View File

@ -1,27 +1,22 @@
default_stages: [ commit ]
default_stages: [pre-commit]
repos:
- repo: https://github.com/pycqa/isort
rev: 5.9.2
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.4
hooks:
- id: isort
args: ["--profile", "black", "--filter-files"]
- repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- id: black
language_version: python3.9
- id: ruff-check
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
rev: v5.0.0
hooks:
- id: check-toml
- id: check-yaml
- id: trailing-whitespace
- id: check-merge-conflict
- id: debug-statements
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.17.1
hooks:
- id: flake8
additional_dependencies: [ "flake8-print", "flake8-builtins", "flake8-functions==0.0.4" ]
- id: mypy
files: openfeature

View File

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

View File

@ -1,5 +1,454 @@
# Changelog
## [0.8.2](https://github.com/open-feature/python-sdk/compare/v0.8.1...v0.8.2) (2025-07-30)
### 🐛 Bug Fixes
* merge transaction context into hook context evaluation context ([#521](https://github.com/open-feature/python-sdk/issues/521)) ([#523](https://github.com/open-feature/python-sdk/issues/523)) ([a5b3aa9](https://github.com/open-feature/python-sdk/commit/a5b3aa9c5213dda311068695f9209282f5faaff5))
### ✨ New Features
* starting migration to uv ([#512](https://github.com/open-feature/python-sdk/issues/512)) ([fb47cbb](https://github.com/open-feature/python-sdk/commit/fb47cbb2a51da9154adf977aad0b16575d227c33))
### 🧹 Chore
* **deps:** pin astral-sh/setup-uv action to bd01e18 ([#514](https://github.com/open-feature/python-sdk/issues/514)) ([6da7890](https://github.com/open-feature/python-sdk/commit/6da7890ac6488bccf640f74bdc530fa9ce8bbec3))
* **deps:** update actions/setup-python digest to a26af69 ([#489](https://github.com/open-feature/python-sdk/issues/489)) ([ad69f2c](https://github.com/open-feature/python-sdk/commit/ad69f2c55f3c8170a8a53981238130eb106207ba))
* **deps:** update astral-sh/setup-uv digest to 7edac99 ([#524](https://github.com/open-feature/python-sdk/issues/524)) ([5652c0c](https://github.com/open-feature/python-sdk/commit/5652c0c457cc5a524e91405f3b229cf245ae4531))
* **deps:** update codecov/codecov-action action to v5.4.2 ([#486](https://github.com/open-feature/python-sdk/issues/486)) ([798ac8d](https://github.com/open-feature/python-sdk/commit/798ac8ded00b8509068003367f36e6c04c574cbc))
* **deps:** update codecov/codecov-action action to v5.4.3 ([#497](https://github.com/open-feature/python-sdk/issues/497)) ([7bb0f5e](https://github.com/open-feature/python-sdk/commit/7bb0f5e499ff8e0985b24696d0680251c90af32b))
* **deps:** update github/codeql-action digest to 181d5ee ([#517](https://github.com/open-feature/python-sdk/issues/517)) ([a04e52c](https://github.com/open-feature/python-sdk/commit/a04e52c0224a6c1c269218df050ce7a56076211d))
* **deps:** update github/codeql-action digest to 28deaed ([#488](https://github.com/open-feature/python-sdk/issues/488)) ([e0de4b2](https://github.com/open-feature/python-sdk/commit/e0de4b2faa109454a8079b934320f1c2b2b2b06e))
* **deps:** update github/codeql-action digest to 39edc49 ([#515](https://github.com/open-feature/python-sdk/issues/515)) ([21ef53a](https://github.com/open-feature/python-sdk/commit/21ef53a156b17ce24db79c75b5bbfeaf2bd77f01))
* **deps:** update github/codeql-action digest to 60168ef ([#492](https://github.com/open-feature/python-sdk/issues/492)) ([8aedfe8](https://github.com/open-feature/python-sdk/commit/8aedfe81ef67af3210ea9921e6b364fdd21ef8ac))
* **deps:** update github/codeql-action digest to ce28f5b ([#508](https://github.com/open-feature/python-sdk/issues/508)) ([4628c24](https://github.com/open-feature/python-sdk/commit/4628c24f5c94821aecb06388703173bd5a8efc30))
* **deps:** update github/codeql-action digest to fca7ace ([#505](https://github.com/open-feature/python-sdk/issues/505)) ([347517a](https://github.com/open-feature/python-sdk/commit/347517a7ccaf145a940fc6e2a37a8d1df621f3a3))
* **deps:** update github/codeql-action digest to ff0a06e ([#498](https://github.com/open-feature/python-sdk/issues/498)) ([c722cf0](https://github.com/open-feature/python-sdk/commit/c722cf0239f2b9b95a1214b99447a9316c2c73d8))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.10 ([#496](https://github.com/open-feature/python-sdk/issues/496)) ([1dd8b29](https://github.com/open-feature/python-sdk/commit/1dd8b294930ee94e9c27a7bad4e43fda76bb1f9d))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.11 ([#499](https://github.com/open-feature/python-sdk/issues/499)) ([abb3137](https://github.com/open-feature/python-sdk/commit/abb31377790f95e4900e92f57050a4a12dc9f311))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.12 ([#501](https://github.com/open-feature/python-sdk/issues/501)) ([8dfa88c](https://github.com/open-feature/python-sdk/commit/8dfa88cf8aacada8b8b20803a51667b586e2182a))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.13 ([#507](https://github.com/open-feature/python-sdk/issues/507)) ([a21413b](https://github.com/open-feature/python-sdk/commit/a21413bd5069a9fd32378e197ae7709b34f001a5))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.5 ([#484](https://github.com/open-feature/python-sdk/issues/484)) ([95be943](https://github.com/open-feature/python-sdk/commit/95be943d33b9cfca137f94e5a1ad52bc562e4b4a))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.6 ([#487](https://github.com/open-feature/python-sdk/issues/487)) ([7fe752d](https://github.com/open-feature/python-sdk/commit/7fe752d8fd6148a38d53e5c7dfc071a6c8121c85))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.7 ([#490](https://github.com/open-feature/python-sdk/issues/490)) ([f4f9a12](https://github.com/open-feature/python-sdk/commit/f4f9a12081871f4797dc0e649219dc7be58d4116))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.8 ([#491](https://github.com/open-feature/python-sdk/issues/491)) ([1f16955](https://github.com/open-feature/python-sdk/commit/1f169551e37f286597400e5cb2c171a60a38436e))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.9 ([#493](https://github.com/open-feature/python-sdk/issues/493)) ([42fed6b](https://github.com/open-feature/python-sdk/commit/42fed6b2001b9056ed9bc7a390213206829a7b99))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.12.0 ([#510](https://github.com/open-feature/python-sdk/issues/510)) ([d21d9db](https://github.com/open-feature/python-sdk/commit/d21d9db90ae6f5f15f1aaf25a4e5d0669dbe1d96))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.12.1 ([#513](https://github.com/open-feature/python-sdk/issues/513)) ([7783a8b](https://github.com/open-feature/python-sdk/commit/7783a8b6c798246fc861fcb83e24427ea0f43e5f))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.12.2 ([#518](https://github.com/open-feature/python-sdk/issues/518)) ([288bd6b](https://github.com/open-feature/python-sdk/commit/288bd6bb34f2d5e857ae5b2b17f02f79276049c5))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.12.4 ([#525](https://github.com/open-feature/python-sdk/issues/525)) ([90a193d](https://github.com/open-feature/python-sdk/commit/90a193d22c64b8b708300dd436a16b8c7d632686))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.16.0 ([#503](https://github.com/open-feature/python-sdk/issues/503)) ([87e4485](https://github.com/open-feature/python-sdk/commit/87e448593d723b8239244a198e54e6cb056d3f95))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.16.1 ([#509](https://github.com/open-feature/python-sdk/issues/509)) ([ac95c7a](https://github.com/open-feature/python-sdk/commit/ac95c7a5b72d56e78dcb8e3411178967b00c04d9))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.17.0 ([#526](https://github.com/open-feature/python-sdk/issues/526)) ([00cab65](https://github.com/open-feature/python-sdk/commit/00cab65315ff3bc38f26b7281d8b258e6ec0f47d))
* switch build backend to uv ([#527](https://github.com/open-feature/python-sdk/issues/527)) ([a369890](https://github.com/open-feature/python-sdk/commit/a3698902b55a1c9d53d4a7db2e6b18e4f2e77c70))
* use publish env ([d54d239](https://github.com/open-feature/python-sdk/commit/d54d239a2d55359d9b49aa534a4b53339bf26571))
### 🔄 Refactoring
* refine typing.Any type hints ([#504](https://github.com/open-feature/python-sdk/issues/504)) ([f95b27a](https://github.com/open-feature/python-sdk/commit/f95b27a25ae1dda7281c2039ec9060363de2703e))
## [0.8.1](https://github.com/open-feature/python-sdk/compare/v0.8.0...v0.8.1) (2025-04-09)
### 🐛 Bug Fixes
* add passthrough init to abstract provider ([#450](https://github.com/open-feature/python-sdk/issues/450)) ([088409e](https://github.com/open-feature/python-sdk/commit/088409ea5cdefef33f28fc4f45026fabac52377a))
* fix cycle dependency between api and client ([#480](https://github.com/open-feature/python-sdk/issues/480)) ([3636a0d](https://github.com/open-feature/python-sdk/commit/3636a0d75f69712844a768cbc6c2f80fdcf6eb84))
### ✨ New Features
* add OTel utility function ([#451](https://github.com/open-feature/python-sdk/issues/451)) ([2d1ba85](https://github.com/open-feature/python-sdk/commit/2d1ba85c93cdd954f539d2872783b21683bd8b07))
### 🧹 Chore
* add codeowner file to be consistent with the rest of openfeature ([#477](https://github.com/open-feature/python-sdk/issues/477)) ([7a30ef9](https://github.com/open-feature/python-sdk/commit/7a30ef914b3180fc72be9a1d2072a8a288e8b54d))
* **deps:** update actions/setup-python digest to 8d9ed9a ([#473](https://github.com/open-feature/python-sdk/issues/473)) ([a135911](https://github.com/open-feature/python-sdk/commit/a1359112e9c1d740bcca501cbb5aadd9da3602b6))
* **deps:** update codecov/codecov-action action to v5.4.0 ([#456](https://github.com/open-feature/python-sdk/issues/456)) ([a666227](https://github.com/open-feature/python-sdk/commit/a666227f55b14d4d2b6e43b6487ac643b6893739))
* **deps:** update github/codeql-action digest to 1b549b9 ([#470](https://github.com/open-feature/python-sdk/issues/470)) ([4eeab3b](https://github.com/open-feature/python-sdk/commit/4eeab3b6914bd947a63f8d3c5bb89b85b7c2ced1))
* **deps:** update github/codeql-action digest to 45775bd ([#483](https://github.com/open-feature/python-sdk/issues/483)) ([5a2825b](https://github.com/open-feature/python-sdk/commit/5a2825b00db0653c6d0496ec7f4703f9125cbed7))
* **deps:** update github/codeql-action digest to 5f8171a ([#467](https://github.com/open-feature/python-sdk/issues/467)) ([d69b759](https://github.com/open-feature/python-sdk/commit/d69b7594a956a49385ef3030c212624d628aec74))
* **deps:** update github/codeql-action digest to 6bb031a ([#462](https://github.com/open-feature/python-sdk/issues/462)) ([0396592](https://github.com/open-feature/python-sdk/commit/0396592586b6f721754c18a46b6d2fee3c2f80e8))
* **deps:** update github/codeql-action digest to b56ba49 ([#454](https://github.com/open-feature/python-sdk/issues/454)) ([613388d](https://github.com/open-feature/python-sdk/commit/613388ddde33b6ce5ff3a39760970297dfa83255))
* **deps:** update github/codeql-action digest to fc7e4a0 ([#481](https://github.com/open-feature/python-sdk/issues/481)) ([1ae9fc2](https://github.com/open-feature/python-sdk/commit/1ae9fc2361f1671cee8c794f02c01eb6ca0b77a6))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.10.0 ([#463](https://github.com/open-feature/python-sdk/issues/463)) ([5fede4d](https://github.com/open-feature/python-sdk/commit/5fede4d4f0cb6e39f84e85c72c9a2dd13434bc78))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.0 ([#465](https://github.com/open-feature/python-sdk/issues/465)) ([d1eb3a0](https://github.com/open-feature/python-sdk/commit/d1eb3a08a8da75022788cc4b9ea7b7d95aec4e69))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.1 ([#468](https://github.com/open-feature/python-sdk/issues/468)) ([c07d3d6](https://github.com/open-feature/python-sdk/commit/c07d3d64677c2ce475b098580b5eba1dd7f95a2e))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.2 ([#469](https://github.com/open-feature/python-sdk/issues/469)) ([95e87c7](https://github.com/open-feature/python-sdk/commit/95e87c71fc835cde7f7528e974509438ab8f2dc3))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.3 ([#475](https://github.com/open-feature/python-sdk/issues/475)) ([2be2c06](https://github.com/open-feature/python-sdk/commit/2be2c06569d89309a70793bb14a82be91d2ccf20))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.4 ([#476](https://github.com/open-feature/python-sdk/issues/476)) ([8acc883](https://github.com/open-feature/python-sdk/commit/8acc88328836c70f168ca87b71f4c49a6dba9381))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.10 ([#461](https://github.com/open-feature/python-sdk/issues/461)) ([9057c6b](https://github.com/open-feature/python-sdk/commit/9057c6b3df6ca5dc9e429db231eb4427cce031ea))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.7 ([#453](https://github.com/open-feature/python-sdk/issues/453)) ([a5cb27b](https://github.com/open-feature/python-sdk/commit/a5cb27b67839d60ea631001759478b2e74b75f28))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.8 ([#457](https://github.com/open-feature/python-sdk/issues/457)) ([0c1a388](https://github.com/open-feature/python-sdk/commit/0c1a388ca121e232f5c36b4b7a550d541ae34e5b))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.9 ([#458](https://github.com/open-feature/python-sdk/issues/458)) ([9ce51eb](https://github.com/open-feature/python-sdk/commit/9ce51ebff5a896b818e241fd8e3c2dea2fee610c))
* **deps:** update spec digest to 09aef37 ([#460](https://github.com/open-feature/python-sdk/issues/460)) ([547781f](https://github.com/open-feature/python-sdk/commit/547781fbd82d1e2ee8a17988d11da3875d6a73dd))
* **deps:** update spec digest to 0cd553d ([#455](https://github.com/open-feature/python-sdk/issues/455)) ([fe99f08](https://github.com/open-feature/python-sdk/commit/fe99f08e9465e8d35dd2b187d8ac01eae98432b7))
* **deps:** update spec digest to 130df3e ([#471](https://github.com/open-feature/python-sdk/issues/471)) ([9ced6bf](https://github.com/open-feature/python-sdk/commit/9ced6bf2d1c7e3b0f01d062564ee63e49254af00))
* **deps:** update spec digest to 25c57ee ([#459](https://github.com/open-feature/python-sdk/issues/459)) ([40cbd82](https://github.com/open-feature/python-sdk/commit/40cbd82dda20604a7a7be00e6913710d4a1ab56f))
* **deps:** update spec digest to 27e4461 ([#472](https://github.com/open-feature/python-sdk/issues/472)) ([490cd06](https://github.com/open-feature/python-sdk/commit/490cd068533bb5ad702adf71915b6e0ac49706d8))
* **deps:** update spec digest to 54952f3 ([#447](https://github.com/open-feature/python-sdk/issues/447)) ([f907855](https://github.com/open-feature/python-sdk/commit/f907855966cf788a3522e7626c76bd050de59a7e))
* **deps:** update spec digest to a69f748 ([#452](https://github.com/open-feature/python-sdk/issues/452)) ([95b33b3](https://github.com/open-feature/python-sdk/commit/95b33b39e6ef472264002322162e83665054d71b))
* **deps:** update spec digest to aad6193 ([#464](https://github.com/open-feature/python-sdk/issues/464)) ([d15388b](https://github.com/open-feature/python-sdk/commit/d15388b542798f7703578927dc5013863a83efa1))
* improve resolve details callable type hints ([#449](https://github.com/open-feature/python-sdk/issues/449)) ([31afa64](https://github.com/open-feature/python-sdk/commit/31afa6490f7c2fc7a553b69c56840d494a520836))
* revert spec to commit 0cd553d ([#479](https://github.com/open-feature/python-sdk/issues/479)) ([0ebec53](https://github.com/open-feature/python-sdk/commit/0ebec538db4d1180bad05e89bb62db23ca606a27))
* use existing submodule version for e2e tests ([#444](https://github.com/open-feature/python-sdk/issues/444)) ([5ae8571](https://github.com/open-feature/python-sdk/commit/5ae8571ccd5f30c0aef87b0bc7f1a08a65254df0))
* use keyword arguments, validate test ([#446](https://github.com/open-feature/python-sdk/issues/446)) ([f29c450](https://github.com/open-feature/python-sdk/commit/f29c4506a6a13307ba95a9b450a1b19c328975b3))
### 📚 Documentation
* fix linting issue on the readme ([1198728](https://github.com/open-feature/python-sdk/commit/11987280ba53ba087b1792316acc920a81434630))
### 🔄 Refactoring
* replace exception raising with error flag resolution ([#474](https://github.com/open-feature/python-sdk/issues/474)) ([e61b69b](https://github.com/open-feature/python-sdk/commit/e61b69bb5079547c62a3ad51499326057db69e7a))
## [0.8.0](https://github.com/open-feature/python-sdk/compare/v0.7.5...v0.8.0) (2025-02-11)
### ⚠ BREAKING CHANGES
* drop Python 3.8 support ([#441](https://github.com/open-feature/python-sdk/issues/441))
### ✨ New Features
* Add async functionality to providers ([#413](https://github.com/open-feature/python-sdk/issues/413)) ([86e7c07](https://github.com/open-feature/python-sdk/commit/86e7c07112cfa9fa6bec15cb7a47f8a675034b8b))
### 🧹 Chore
* **deps:** pin dependencies ([#435](https://github.com/open-feature/python-sdk/issues/435)) ([38d13fa](https://github.com/open-feature/python-sdk/commit/38d13fa454e8b7d5a55a8e4e12dcbe4c37f70706))
* **deps:** update github/codeql-action digest to 9e8d078 ([#440](https://github.com/open-feature/python-sdk/issues/440)) ([ba0213e](https://github.com/open-feature/python-sdk/commit/ba0213e701958a9962676646bec267a5c530184c))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.5 ([#439](https://github.com/open-feature/python-sdk/issues/439)) ([75b41dd](https://github.com/open-feature/python-sdk/commit/75b41dd0202e9651801d2144ceec2c16ebe4989f))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.6 ([#443](https://github.com/open-feature/python-sdk/issues/443)) ([37296dc](https://github.com/open-feature/python-sdk/commit/37296dc0b5b7450815b3b63d7877968fe07f06be))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.15.0 ([#430](https://github.com/open-feature/python-sdk/issues/430)) ([fe0fea1](https://github.com/open-feature/python-sdk/commit/fe0fea1f7328bb97c6985628be2f5c11bae13f22))
* drop Python 3.8 support ([#441](https://github.com/open-feature/python-sdk/issues/441)) ([bcd1a38](https://github.com/open-feature/python-sdk/commit/bcd1a3807e635dcd80a7894859ae14d54a3dc485))
* fix renovate syntax issue ([e705af4](https://github.com/open-feature/python-sdk/commit/e705af47b1b44705f0f0cca1846ccb97e820f042))
* use centralized renovate config, downgrade release please ([#433](https://github.com/open-feature/python-sdk/issues/433)) ([ff52163](https://github.com/open-feature/python-sdk/commit/ff521630a1962f73a1d3f8e3fc65c8cdc691f54b))
### 📚 Documentation
* fix eval context link ([#438](https://github.com/open-feature/python-sdk/issues/438)) ([154d834](https://github.com/open-feature/python-sdk/commit/154d8345e7a65f3409c168a87d157df583fc8aa8))
* fix links to the openfeature ecosystem page ([#432](https://github.com/open-feature/python-sdk/issues/432)) ([49edce2](https://github.com/open-feature/python-sdk/commit/49edce226996d7d27a6dd64a1ae45e0def9e9b29))
## [0.7.5](https://github.com/open-feature/python-sdk/compare/v0.7.4...v0.7.5) (2025-01-31)
### ⚠ BREAKING CHANGES
The signature of the `finally_after` hook stage has been changed. The signature now includes the `evaluation details`, as per the [OpenFeature specification](https://openfeature.dev/specification/sections/hooks#requirement-438). To migrate, update any hook that implements the `finally_after` stage to accept `evaluation details` as the second argument.
* Add evaluation details to finally hook stage [#403](https://github.com/open-feature/python-sdk/issues/403) ([#423](https://github.com/open-feature/python-sdk/issues/423)) ([9e9bb5c](https://github.com/open-feature/python-sdk/commit/9e9bb5c6269cfa5d9c9ffc7141c6dc63e399cdca))
### 🐛 Bug Fixes
* Finally hooks do not get called when the provider is not ready [#424](https://github.com/open-feature/python-sdk/issues/424) ([#425](https://github.com/open-feature/python-sdk/issues/425)) ([8f2caba](https://github.com/open-feature/python-sdk/commit/8f2cabaa32f595304ecd6964b6ae21909672ef4a))
### ✨ New Features
* Add evaluation details to finally hook stage [#403](https://github.com/open-feature/python-sdk/issues/403) ([#423](https://github.com/open-feature/python-sdk/issues/423)) ([9e9bb5c](https://github.com/open-feature/python-sdk/commit/9e9bb5c6269cfa5d9c9ffc7141c6dc63e399cdca))
* Update test harness (add assertions) [#1467](https://github.com/open-feature/python-sdk/issues/1467) ([#415](https://github.com/open-feature/python-sdk/issues/415)) ([f559d1b](https://github.com/open-feature/python-sdk/commit/f559d1b27a096c585bd81f32f6472039c9ce5e03))
* Update test harness (copy test files) [#1467](https://github.com/open-feature/python-sdk/issues/1467) ([#416](https://github.com/open-feature/python-sdk/issues/416)) ([192f7c4](https://github.com/open-feature/python-sdk/commit/192f7c40bd07616030e86ff2aba7e993390d4af4))
### 🧹 Chore
* **config:** migrate config renovate.json ([26bc964](https://github.com/open-feature/python-sdk/commit/26bc9642270f9371170ce8ab2d9b938a0fca187e))
* **config:** migrate renovate config ([#408](https://github.com/open-feature/python-sdk/issues/408)) ([26bc964](https://github.com/open-feature/python-sdk/commit/26bc9642270f9371170ce8ab2d9b938a0fca187e))
* **deps:** update actions/setup-python digest to 4237552 ([#422](https://github.com/open-feature/python-sdk/issues/422)) ([9c2ed71](https://github.com/open-feature/python-sdk/commit/9c2ed71c6efdc2ece9ae89cf91f3b019c44b0033))
* **deps:** update codecov/codecov-action action to v5.1.0 ([#401](https://github.com/open-feature/python-sdk/issues/401)) ([0459330](https://github.com/open-feature/python-sdk/commit/0459330cb91e9b28a15bdd380aec4c56c3b5d8df))
* **deps:** update codecov/codecov-action action to v5.1.1 ([#402](https://github.com/open-feature/python-sdk/issues/402)) ([a6907d6](https://github.com/open-feature/python-sdk/commit/a6907d610e6dde1eecef56f25f3cc6a569b6eee4))
* **deps:** update codecov/codecov-action action to v5.1.2 ([#405](https://github.com/open-feature/python-sdk/issues/405)) ([1c56480](https://github.com/open-feature/python-sdk/commit/1c564804afad474151489d695af7fa0409a768c6))
* **deps:** update codecov/codecov-action action to v5.2.0 ([#418](https://github.com/open-feature/python-sdk/issues/418)) ([b69e81a](https://github.com/open-feature/python-sdk/commit/b69e81a63676240ef2abb98e96d2954a50f0c20a))
* **deps:** update codecov/codecov-action action to v5.3.0 ([#420](https://github.com/open-feature/python-sdk/issues/420)) ([6af37b1](https://github.com/open-feature/python-sdk/commit/6af37b1c2bc2e10161673af5726932129ed02506))
* **deps:** update codecov/codecov-action action to v5.3.1 ([#421](https://github.com/open-feature/python-sdk/issues/421)) ([e99e481](https://github.com/open-feature/python-sdk/commit/e99e481524ccfbea5bc7554d531f9c883dce6b5f))
* **deps:** update googleapis/release-please-action action to v4 ([#428](https://github.com/open-feature/python-sdk/issues/428)) ([99905d5](https://github.com/open-feature/python-sdk/commit/99905d57f8b21a640f8f64f177679eee127ebba6))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.1 ([#398](https://github.com/open-feature/python-sdk/issues/398)) ([043385a](https://github.com/open-feature/python-sdk/commit/043385a8369e253a5e0ad1e184e980f8e8d7e5c7))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.2 ([#400](https://github.com/open-feature/python-sdk/issues/400)) ([2b6e210](https://github.com/open-feature/python-sdk/commit/2b6e210bc9dda72335e646fc60cde79b5bdd76c1))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.3 ([#404](https://github.com/open-feature/python-sdk/issues/404)) ([01ec388](https://github.com/open-feature/python-sdk/commit/01ec388d2d93fc87a4f2eca856cf507cbee35785))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.4 ([#406](https://github.com/open-feature/python-sdk/issues/406)) ([3296d3b](https://github.com/open-feature/python-sdk/commit/3296d3b229d41c7f879adfb7ab36e19de36617e4))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.6 ([#410](https://github.com/open-feature/python-sdk/issues/410)) ([7f9d422](https://github.com/open-feature/python-sdk/commit/7f9d422497a6d8392eee18ccfd12050eef7a2338))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.0 ([#411](https://github.com/open-feature/python-sdk/issues/411)) ([bc6e333](https://github.com/open-feature/python-sdk/commit/bc6e3332157788ec3f8037f68b9087e26a4634b5))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.1 ([#412](https://github.com/open-feature/python-sdk/issues/412)) ([cbace6a](https://github.com/open-feature/python-sdk/commit/cbace6a24c3fa091242aedd4f7c2e8de4332e463))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.2 ([#414](https://github.com/open-feature/python-sdk/issues/414)) ([9304292](https://github.com/open-feature/python-sdk/commit/9304292ea91580cf8cbfee7dbde922caa818189d))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.3 ([#419](https://github.com/open-feature/python-sdk/issues/419)) ([8f9cc7c](https://github.com/open-feature/python-sdk/commit/8f9cc7ca96a1210bab104e6342b4f7d1553bad3b))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.4 ([#426](https://github.com/open-feature/python-sdk/issues/426)) ([f726706](https://github.com/open-feature/python-sdk/commit/f72670689d25b82c8d54cacde3b1af179e0bc7e6))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.14.0 ([#407](https://github.com/open-feature/python-sdk/issues/407)) ([89d6997](https://github.com/open-feature/python-sdk/commit/89d6997b1fe04df82e0873de7e7a1ea1aca2b071))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.14.1 ([#409](https://github.com/open-feature/python-sdk/issues/409)) ([2c1840c](https://github.com/open-feature/python-sdk/commit/2c1840c87d00177d87b93135a38a7df98a9f6c0b))
## [0.7.4](https://github.com/open-feature/python-sdk/compare/v0.7.3...v0.7.4) (2024-11-25)
### 🧹 Chore
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.0 ([#395](https://github.com/open-feature/python-sdk/issues/395)) ([cd737a9](https://github.com/open-feature/python-sdk/commit/cd737a9a6aeb6ff64050f759749aab96e73a8c34))
## [0.7.3](https://github.com/open-feature/python-sdk/compare/v0.7.2...v0.7.3) (2024-11-24)
### ✨ New Features
* implement transaction context ([#389](https://github.com/open-feature/python-sdk/issues/389)) ([9b97130](https://github.com/open-feature/python-sdk/commit/9b97130908c5ec07580d66e0f2aad9adf1607f53))
### 🧹 Chore
* **deps:** update codecov/codecov-action action to v5 ([#386](https://github.com/open-feature/python-sdk/issues/386)) ([6cd570f](https://github.com/open-feature/python-sdk/commit/6cd570f64cb50473e0a046d5928e114157598d78))
* **deps:** update codecov/codecov-action action to v5.0.2 ([#388](https://github.com/open-feature/python-sdk/issues/388)) ([e72e329](https://github.com/open-feature/python-sdk/commit/e72e329a2acda0a0399ef2faa04c9b7f3b29bb65))
* **deps:** update codecov/codecov-action action to v5.0.3 ([#390](https://github.com/open-feature/python-sdk/issues/390)) ([3fb7cbc](https://github.com/open-feature/python-sdk/commit/3fb7cbc3ddcc9016ed51e890d9648f1f96284170))
* **deps:** update codecov/codecov-action action to v5.0.4 ([#391](https://github.com/open-feature/python-sdk/issues/391)) ([9759bfa](https://github.com/open-feature/python-sdk/commit/9759bfaa21ed6d6e5e84c5559304db9a49b61884))
* **deps:** update codecov/codecov-action action to v5.0.5 ([#392](https://github.com/open-feature/python-sdk/issues/392)) ([a652004](https://github.com/open-feature/python-sdk/commit/a652004fde72cb1dcf730f146db36d686589b642))
* **deps:** update codecov/codecov-action action to v5.0.6 ([#393](https://github.com/open-feature/python-sdk/issues/393)) ([24970b5](https://github.com/open-feature/python-sdk/commit/24970b5d0915f533d88e4cd0fb54bea8cd28cab0))
* **deps:** update codecov/codecov-action action to v5.0.7 ([#394](https://github.com/open-feature/python-sdk/issues/394)) ([f024a6f](https://github.com/open-feature/python-sdk/commit/f024a6f3407b3e3ce8ea16a80982a7642c9f2f20))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.7.2 ([#381](https://github.com/open-feature/python-sdk/issues/381)) ([2d8fadf](https://github.com/open-feature/python-sdk/commit/2d8fadf40afb98e2915b645278358fca367447d7))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.7.3 ([#384](https://github.com/open-feature/python-sdk/issues/384)) ([1eb6f4c](https://github.com/open-feature/python-sdk/commit/1eb6f4c6558f3dd20cb2389f9650a6476ca20fcb))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.7.4 ([#387](https://github.com/open-feature/python-sdk/issues/387)) ([3d77f24](https://github.com/open-feature/python-sdk/commit/3d77f2410751646380f58a77cad0fd5851da30b5))
## [0.7.2](https://github.com/open-feature/python-sdk/compare/v0.7.1...v0.7.2) (2024-10-24)
### 🧹 Chore
* add Python 3.13 ([#364](https://github.com/open-feature/python-sdk/issues/364)) ([ca4d589](https://github.com/open-feature/python-sdk/commit/ca4d589456a7c3ca1a8ba448592687c7909bf75d))
* **deps:** update codecov/codecov-action action to v4.6.0 ([#371](https://github.com/open-feature/python-sdk/issues/371)) ([45d2a6d](https://github.com/open-feature/python-sdk/commit/45d2a6daca1ed6073fa9510a047085230b9d99c1))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.7 ([#357](https://github.com/open-feature/python-sdk/issues/357)) ([a691480](https://github.com/open-feature/python-sdk/commit/a6914808b8ed4338d332b333a734fb441b09d3d8))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.6.0 ([#359](https://github.com/open-feature/python-sdk/issues/359)) ([e538f86](https://github.com/open-feature/python-sdk/commit/e538f86e76958a40400e41838ee3fd81a4f11dd9))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.6.1 ([#360](https://github.com/open-feature/python-sdk/issues/360)) ([90d417d](https://github.com/open-feature/python-sdk/commit/90d417dbd3940b99dc5d9fe945f351f0296a4058))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.6.2 ([#361](https://github.com/open-feature/python-sdk/issues/361)) ([8e6a530](https://github.com/open-feature/python-sdk/commit/8e6a530040f122cdd1861dce883aebb725542932))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.6.3 ([#363](https://github.com/open-feature/python-sdk/issues/363)) ([6e316a2](https://github.com/open-feature/python-sdk/commit/6e316a216a50848959154f25bfcac17953910fcd))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.6.4 ([#365](https://github.com/open-feature/python-sdk/issues/365)) ([e57ff78](https://github.com/open-feature/python-sdk/commit/e57ff783064f1097609e807d9b781b252792b922))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.6.5 ([#366](https://github.com/open-feature/python-sdk/issues/366)) ([4959144](https://github.com/open-feature/python-sdk/commit/4959144a5016f60c13791154ef8a43108a8d8568))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.6.6 ([#367](https://github.com/open-feature/python-sdk/issues/367)) ([a334f20](https://github.com/open-feature/python-sdk/commit/a334f202515949f29ea4a47912881f84cbfc9a93))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.6.7 ([#369](https://github.com/open-feature/python-sdk/issues/369)) ([65b0f1a](https://github.com/open-feature/python-sdk/commit/65b0f1a374b692a1a55635b78355776524c2ac1a))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.6.8 ([#370](https://github.com/open-feature/python-sdk/issues/370)) ([dcdb56c](https://github.com/open-feature/python-sdk/commit/dcdb56c43f493aa3fd6c0e69d82b65c74b598a6f))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.6.9 ([#372](https://github.com/open-feature/python-sdk/issues/372)) ([d2c6b40](https://github.com/open-feature/python-sdk/commit/d2c6b401cd69cd1fb17aadfd00b4c37ff9e9e57d))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.7.0 ([#377](https://github.com/open-feature/python-sdk/issues/377)) ([157e1ba](https://github.com/open-feature/python-sdk/commit/157e1baddb69a51bd49f1f55ac381cffc50f4081))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.7.1 ([#380](https://github.com/open-feature/python-sdk/issues/380)) ([c3d70ec](https://github.com/open-feature/python-sdk/commit/c3d70ec4579d1af7556ae7cb903337de66131697))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.11.2 ([#362](https://github.com/open-feature/python-sdk/issues/362)) ([f38ff91](https://github.com/open-feature/python-sdk/commit/f38ff919bde09fa76b50992b8fb05ae3f2397936))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.12.0 ([#376](https://github.com/open-feature/python-sdk/issues/376)) ([00f026d](https://github.com/open-feature/python-sdk/commit/00f026d4f17b3062c992bd03b1e714447601b406))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.12.1 ([#378](https://github.com/open-feature/python-sdk/issues/378)) ([091656b](https://github.com/open-feature/python-sdk/commit/091656b6539a3fcb7746c8fd26de7eab6f988cb3))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.13.0 ([#379](https://github.com/open-feature/python-sdk/issues/379)) ([f5aaac0](https://github.com/open-feature/python-sdk/commit/f5aaac0525587d533bb15f3641e15411f978cf00))
* **deps:** update pre-commit hook pre-commit/pre-commit-hooks to v5 ([#373](https://github.com/open-feature/python-sdk/issues/373)) ([7a6b3c7](https://github.com/open-feature/python-sdk/commit/7a6b3c78cc86e2704441c4d64974f4348e63234d))
* **deps:** update python docker tag to v3.13 ([#375](https://github.com/open-feature/python-sdk/issues/375)) ([977cd6d](https://github.com/open-feature/python-sdk/commit/977cd6d4b7c7b44755409242db1e430758b5b955))
## [0.7.1](https://github.com/open-feature/python-sdk/compare/v0.7.0...v0.7.1) (2024-08-02)
### 🐛 Bug Fixes
* event handler methods are not thread-safe ([#329](https://github.com/open-feature/python-sdk/issues/329)) ([3217575](https://github.com/open-feature/python-sdk/commit/3217575f4f87587751e47707384c344c185b684c)), closes [#326](https://github.com/open-feature/python-sdk/issues/326)
* make global hooks thread safe ([#331](https://github.com/open-feature/python-sdk/issues/331)) ([5abcf3b](https://github.com/open-feature/python-sdk/commit/5abcf3b157f0f1ef6655a64abf1229ab84ad190e))
* remove exception logging during evaluation ([#347](https://github.com/open-feature/python-sdk/issues/347)) ([0ed625f](https://github.com/open-feature/python-sdk/commit/0ed625f18617472ac0e60a88e727223381d8d735))
### 🧹 Chore
* **deps:** update codecov/codecov-action action to v4.3.1 ([#327](https://github.com/open-feature/python-sdk/issues/327)) ([f352045](https://github.com/open-feature/python-sdk/commit/f3520450557c71d8bfd7884c909114c27ba4e2e6))
* **deps:** update codecov/codecov-action action to v4.4.0 ([#334](https://github.com/open-feature/python-sdk/issues/334)) ([6acbef9](https://github.com/open-feature/python-sdk/commit/6acbef94e67fa1c5da8f764a1d581870d92729aa))
* **deps:** update codecov/codecov-action action to v4.4.1 ([#335](https://github.com/open-feature/python-sdk/issues/335)) ([fa67709](https://github.com/open-feature/python-sdk/commit/fa677092f894ed0ad00093391b799fb5a2adbab2))
* **deps:** update codecov/codecov-action action to v4.5.0 ([#341](https://github.com/open-feature/python-sdk/issues/341)) ([e6a353e](https://github.com/open-feature/python-sdk/commit/e6a353e4754aa9443f3042b820bf167b6a66c944))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.10 ([#344](https://github.com/open-feature/python-sdk/issues/344)) ([2a45af8](https://github.com/open-feature/python-sdk/commit/2a45af895cc7dc7e15f94422a9de58d2b82db92b))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.3 ([#330](https://github.com/open-feature/python-sdk/issues/330)) ([f8544ff](https://github.com/open-feature/python-sdk/commit/f8544ffaf6abdee88d38e40c2dc493b36dad2c82))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.4 ([#333](https://github.com/open-feature/python-sdk/issues/333)) ([bd0bc1e](https://github.com/open-feature/python-sdk/commit/bd0bc1e2b7a28b1f1dcd50b63b61214131968925))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.5 ([#336](https://github.com/open-feature/python-sdk/issues/336)) ([2f93524](https://github.com/open-feature/python-sdk/commit/2f9352406301d0dfb804d04bf21039e87eeb01c5))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.6 ([#337](https://github.com/open-feature/python-sdk/issues/337)) ([cf61e5b](https://github.com/open-feature/python-sdk/commit/cf61e5b682481b4350d47af928f330bbbd93d7f1))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.7 ([#338](https://github.com/open-feature/python-sdk/issues/338)) ([1bf4682](https://github.com/open-feature/python-sdk/commit/1bf4682b466b998106fb94c7bbafdaa4a5e32289))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.8 ([#339](https://github.com/open-feature/python-sdk/issues/339)) ([44b0787](https://github.com/open-feature/python-sdk/commit/44b07879b08030c1356192ad4f69bc8b58c59914))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.9 ([#342](https://github.com/open-feature/python-sdk/issues/342)) ([f3982dc](https://github.com/open-feature/python-sdk/commit/f3982dc8c6faf5de6b86a406f8ecf2056d15026b))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.0 ([#346](https://github.com/open-feature/python-sdk/issues/346)) ([5c7bd14](https://github.com/open-feature/python-sdk/commit/5c7bd14b415336e990aced2bf2b12f6d2dd64b84))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.1 ([#348](https://github.com/open-feature/python-sdk/issues/348)) ([5dff1e8](https://github.com/open-feature/python-sdk/commit/5dff1e89b21542a16d602a541f76a52f8a0dbc4f))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.2 ([#349](https://github.com/open-feature/python-sdk/issues/349)) ([299a4f4](https://github.com/open-feature/python-sdk/commit/299a4f4630c18c8fc5a5bb1a55a1bcaa9a19fd8c))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.3 ([#350](https://github.com/open-feature/python-sdk/issues/350)) ([fe63b64](https://github.com/open-feature/python-sdk/commit/fe63b64d8fe90efc1433971aa7b1701ef8ae93c9))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.4 ([#352](https://github.com/open-feature/python-sdk/issues/352)) ([c294689](https://github.com/open-feature/python-sdk/commit/c29468941b946d6b8e355c3d60bc2e1f14faa959))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.5 ([#353](https://github.com/open-feature/python-sdk/issues/353)) ([6d46d95](https://github.com/open-feature/python-sdk/commit/6d46d957bdd8dd58bf11ab47689dbc8e19e80cf6))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.6 ([#356](https://github.com/open-feature/python-sdk/issues/356)) ([261aa41](https://github.com/open-feature/python-sdk/commit/261aa4168ef7aab4d8613af4f45df1a495018f2e))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.10.1 ([#345](https://github.com/open-feature/python-sdk/issues/345)) ([b553dfa](https://github.com/open-feature/python-sdk/commit/b553dfa607ce3c22d1369180c7b8a20291895ac0))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.11.0 ([#351](https://github.com/open-feature/python-sdk/issues/351)) ([931e0cb](https://github.com/open-feature/python-sdk/commit/931e0cb3a8515dcb46c37c3eb9fa2bc08d88eed6))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.11.1 ([#355](https://github.com/open-feature/python-sdk/issues/355)) ([62c4b67](https://github.com/open-feature/python-sdk/commit/62c4b672f67f2d8c9e98c5fab902542e5d2092b2))
### 🔄 Refactoring
* bind providers explicitly to a registry with attach/detach ([#324](https://github.com/open-feature/python-sdk/issues/324)) ([c3ad697](https://github.com/open-feature/python-sdk/commit/c3ad697a80ade72fb5cdee147ac5c11c38e6533f))
## [0.7.0](https://github.com/open-feature/python-sdk/compare/v0.6.1...v0.7.0) (2024-04-25)
### ⚠ BREAKING CHANGES
* move AbstractProvider to openfeature.provider ([#314](https://github.com/open-feature/python-sdk/issues/314))
* restrict exported names with __all__ ([#306](https://github.com/open-feature/python-sdk/issues/306))
### 🐛 Bug Fixes
* remove ProviderEvent.PROVIDER_FATAL ([#307](https://github.com/open-feature/python-sdk/issues/307)) ([de36b21](https://github.com/open-feature/python-sdk/commit/de36b214dcba717d3ff72cb5d9cc3d3c8de45461))
* restrict exported names with __all__ ([#306](https://github.com/open-feature/python-sdk/issues/306)) ([34ac91c](https://github.com/open-feature/python-sdk/commit/34ac91c707103fa50e905c54148f09615c610c33))
### ✨ New Features
* update provider status when provider emits events ([#309](https://github.com/open-feature/python-sdk/issues/309)) ([9966c14](https://github.com/open-feature/python-sdk/commit/9966c14e16329f8d1e70b492b8785be497257a6b))
### 🧹 Chore
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.3.5 ([#311](https://github.com/open-feature/python-sdk/issues/311)) ([47ae16c](https://github.com/open-feature/python-sdk/commit/47ae16c167fbb4bd8f92eaca3151afabb18a6752))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.3.6 ([#316](https://github.com/open-feature/python-sdk/issues/316)) ([49f0948](https://github.com/open-feature/python-sdk/commit/49f0948e5140151f655e6d34c15810092fda3510))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.3.7 ([#318](https://github.com/open-feature/python-sdk/issues/318)) ([5636620](https://github.com/open-feature/python-sdk/commit/563662054ce0707d9c752bcd639e9d296b9bce9e))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.0 ([#320](https://github.com/open-feature/python-sdk/issues/320)) ([abb14f5](https://github.com/open-feature/python-sdk/commit/abb14f5ed9b1b5e95ea90e6df0a67c9109bd465e))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.1 ([#321](https://github.com/open-feature/python-sdk/issues/321)) ([44f1223](https://github.com/open-feature/python-sdk/commit/44f12239ffe67e6f3a78dc68640bf56fd56fe8d9))
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.2 ([#323](https://github.com/open-feature/python-sdk/issues/323)) ([f109df6](https://github.com/open-feature/python-sdk/commit/f109df671c6b446219e52a575ddff70d74792ddf))
* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.10.0 ([#322](https://github.com/open-feature/python-sdk/issues/322)) ([6dedd27](https://github.com/open-feature/python-sdk/commit/6dedd275cfe71fd76a772dea7a318312aac3d477))
* **deps:** update pre-commit hook pre-commit/pre-commit-hooks to v4.6.0 ([#312](https://github.com/open-feature/python-sdk/issues/312)) ([faf02a9](https://github.com/open-feature/python-sdk/commit/faf02a98889585e3fbe0215e2a8963a39e626ff1))
* update codecov/codecov-action action to v4 ([#317](https://github.com/open-feature/python-sdk/issues/317)) ([cd605c4](https://github.com/open-feature/python-sdk/commit/cd605c4f5d130b9823555a9bd465d6874b321701))
* update renovate config ([#310](https://github.com/open-feature/python-sdk/issues/310)) ([ff62637](https://github.com/open-feature/python-sdk/commit/ff626374ae20311f090a9344aedbde7d37fb35fd))
### 🔄 Refactoring
* move AbstractProvider to openfeature.provider ([#314](https://github.com/open-feature/python-sdk/issues/314)) ([96ba793](https://github.com/open-feature/python-sdk/commit/96ba7938de554724edfc1670d4737f4c495f98a6))
## [0.6.1](https://github.com/open-feature/python-sdk/compare/v0.6.0...v0.6.1) (2024-03-26)
### ✨ New Features
* populate provider and client metadata in HookContext ([#302](https://github.com/open-feature/python-sdk/issues/302)) ([78ea3b9](https://github.com/open-feature/python-sdk/commit/78ea3b991499231f42efda41ba6f672e20cb346c))
### 🧹 Chore
* add keywords to pyproject.toml ([#305](https://github.com/open-feature/python-sdk/issues/305)) ([05d0da2](https://github.com/open-feature/python-sdk/commit/05d0da2e3df86833986618243e18b66218425db8))
### 🔄 Refactoring
* mark hook_support module as private/internal ([#303](https://github.com/open-feature/python-sdk/issues/303)) ([4a323b0](https://github.com/open-feature/python-sdk/commit/4a323b0f9622663c9c43c292364fa25062d70715))
## [0.6.0](https://github.com/open-feature/python-sdk/compare/v0.5.0...v0.6.0) (2024-03-22)
### 🐛 Bug Fixes
* run error hooks if provider returns FlagResolutionDetails with non-empty error_code ([#291](https://github.com/open-feature/python-sdk/issues/291)) ([e747544](https://github.com/open-feature/python-sdk/commit/e7475441bd14323431fdf1850e643f5aaaa21abd))
### ✨ New Features
* implement provider events ([#278](https://github.com/open-feature/python-sdk/issues/278)) ([679409f](https://github.com/open-feature/python-sdk/commit/679409fad229d0e675be4a8ee2b3a13860f4e987))
* implement provider status ([#288](https://github.com/open-feature/python-sdk/issues/288)) ([789e6e0](https://github.com/open-feature/python-sdk/commit/789e6e0f5fcf499604261afd918ed1e8844fa0a0))
### 🧹 Chore
* add changelog sections ([#282](https://github.com/open-feature/python-sdk/issues/282)) ([141858d](https://github.com/open-feature/python-sdk/commit/141858d2359bf6bf439426b3ea4ba322f4b10421))
* **deps:** update dependency coverage to v7.4.3 ([#280](https://github.com/open-feature/python-sdk/issues/280)) ([bafa427](https://github.com/open-feature/python-sdk/commit/bafa427a0da40711d327c435ab199286f68fb6b7))
* **deps:** update dependency coverage to v7.4.4 ([#293](https://github.com/open-feature/python-sdk/issues/293)) ([f5987ef](https://github.com/open-feature/python-sdk/commit/f5987ef8f41892c9cad776d7716592ac0eac4719))
* **deps:** update dependency pytest to v8.0.2 ([#281](https://github.com/open-feature/python-sdk/issues/281)) ([b2594a5](https://github.com/open-feature/python-sdk/commit/b2594a567c31e48a1ae675b855e84300201e8132))
* **deps:** update dependency pytest to v8.1.0 ([#287](https://github.com/open-feature/python-sdk/issues/287)) ([7ba7d61](https://github.com/open-feature/python-sdk/commit/7ba7d6146f0f801cadfd7593dc6df4b7d4f488d4))
* **deps:** update dependency pytest to v8.1.1 ([#289](https://github.com/open-feature/python-sdk/issues/289)) ([3f336b3](https://github.com/open-feature/python-sdk/commit/3f336b3a248dd8e75e162870d26a4b97c61f2ff6))
* **deps:** update dependency pytest-mock to v3.13.0 ([#298](https://github.com/open-feature/python-sdk/issues/298)) ([04b4009](https://github.com/open-feature/python-sdk/commit/04b4009dbfd112307e17a6f9273e0118ad337fe1))
* **deps:** update dependency pytest-mock to v3.14.0 ([#300](https://github.com/open-feature/python-sdk/issues/300)) ([a70ae0c](https://github.com/open-feature/python-sdk/commit/a70ae0cb2e5322cc6290dbe5be12f0a665cc0e86))
* update mypy and ruff ([#296](https://github.com/open-feature/python-sdk/issues/296)) ([6e4eebc](https://github.com/open-feature/python-sdk/commit/6e4eebce2073aa792444ea9f28906b9c925ebd75))
### 📚 Documentation
* add missing imports in provider dev example in README ([ae26217](https://github.com/open-feature/python-sdk/commit/ae26217328a5ca07722c5e12b01720606259d805))
* add Missing Imports in Provider Dev Example in README ([#286](https://github.com/open-feature/python-sdk/issues/286)) ([ae26217](https://github.com/open-feature/python-sdk/commit/ae26217328a5ca07722c5e12b01720606259d805))
* update spec version to 0.8.0 ([#299](https://github.com/open-feature/python-sdk/issues/299)) ([58d27c4](https://github.com/open-feature/python-sdk/commit/58d27c4011b4f7fd96cc7d1ba10f017c7a3db958))
### 🔄 Refactoring
* improve Hook Hints typing ([#285](https://github.com/open-feature/python-sdk/issues/285)) ([5acd6a6](https://github.com/open-feature/python-sdk/commit/5acd6a6598fa45326ddafb0184d184cadea826d0))
## [0.5.0](https://github.com/open-feature/python-sdk/compare/v0.4.2...v0.5.0) (2024-02-20)
### ⚠ BREAKING CHANGES
* add support for domains ([#271](https://github.com/open-feature/python-sdk/issues/271))
### Features
* add support for domains ([#271](https://github.com/open-feature/python-sdk/issues/271)) ([ed6a42f](https://github.com/open-feature/python-sdk/commit/ed6a42f264a6efc149642181bfc4c6de0eb83ce1))
## [0.4.2](https://github.com/open-feature/python-sdk/compare/v0.4.1...v0.4.2) (2024-02-06)
### Features
* add FeatureProvider protocol ([#268](https://github.com/open-feature/python-sdk/issues/268)) ([caa7f36](https://github.com/open-feature/python-sdk/commit/caa7f36c309149bd8d91c214e85f382b026093f6))
* improve logging setup ([#261](https://github.com/open-feature/python-sdk/issues/261)) ([ccbff2c](https://github.com/open-feature/python-sdk/commit/ccbff2c5e46f69274230fc5ddc3cfb90a283d013))
* make return value not optional in provider API functions ([#270](https://github.com/open-feature/python-sdk/issues/270)) ([cb1677b](https://github.com/open-feature/python-sdk/commit/cb1677b0a826ad496f1ffa1074018f1400d84c80))
* make specific fields in HookContext immutable ([#266](https://github.com/open-feature/python-sdk/issues/266)) ([3b89760](https://github.com/open-feature/python-sdk/commit/3b89760d4127a997dadbee920d0e066b2bf08e84))
### Bug Fixes
* Allow string values for `FlagEvaluationDetails.reason` and `FlagResolutionDetails.reason` ([#264](https://github.com/open-feature/python-sdk/issues/264)) ([5ef6ca1](https://github.com/open-feature/python-sdk/commit/5ef6ca1263d2cebdc7c16177fc182eccd56bae2f))
### Documentation
* document shutdown function ([#237](https://github.com/open-feature/python-sdk/issues/237)) ([95d69e2](https://github.com/open-feature/python-sdk/commit/95d69e27b3f6b9cb9f716ae4b2d5b0879c0253e3))
* update supported spec version ([#269](https://github.com/open-feature/python-sdk/issues/269)) ([1282bab](https://github.com/open-feature/python-sdk/commit/1282bab31ea6a554911a9d37c4c4d3e14ffa5133))
## [0.4.1](https://github.com/open-feature/python-sdk/compare/v0.4.0...v0.4.1) (2023-11-08)
### Bug Fixes
* add PEP 561 py.typed marker file ([#232](https://github.com/open-feature/python-sdk/issues/232)) ([db50494](https://github.com/open-feature/python-sdk/commit/db504946d1aea7e653e5755d703cff3d52b455dd))
* fix types for HookContext.{client,provider}_metadata ([#233](https://github.com/open-feature/python-sdk/issues/233)) ([4bdd384](https://github.com/open-feature/python-sdk/commit/4bdd384544c24f5d9942c1e6261689c6b8ceb7de))
* replace str with enum value in InMemoryFlag definition ([#234](https://github.com/open-feature/python-sdk/issues/234)) ([963b01e](https://github.com/open-feature/python-sdk/commit/963b01e66d6ebe8062beaf3bfa0d034a312c037e))
## [0.4.0](https://github.com/open-feature/python-sdk/compare/v0.3.1...v0.4.0) (2023-11-01)
### ⚠ BREAKING CHANGES
* raise error if the flag wasn't found using the in-memory provider ([#228](https://github.com/open-feature/python-sdk/issues/228))
### Features
* implement initialize/shutdown on provider registration ([#213](https://github.com/open-feature/python-sdk/issues/213)) ([84af1ae](https://github.com/open-feature/python-sdk/commit/84af1aec01241842289bce2beb35486153876706))
* pass flag_metadata from resolution to evaluation details ([#212](https://github.com/open-feature/python-sdk/issues/212)) ([88a204d](https://github.com/open-feature/python-sdk/commit/88a204dc27c435f3b5faec231a07a96cb011518c))
### Bug Fixes
* Hook methods should have default non-abstract implementations ([#216](https://github.com/open-feature/python-sdk/issues/216)) ([c661ab2](https://github.com/open-feature/python-sdk/commit/c661ab20a43ff4411b7f0847c71df886af87e7ed))
* raise error if the flag wasn't found using the in-memory provider ([#228](https://github.com/open-feature/python-sdk/issues/228)) ([0c314ab](https://github.com/open-feature/python-sdk/commit/0c314ab77cd60d3347aea7f733d324a6228e8871))
## [0.3.1](https://github.com/open-feature/python-sdk/compare/v0.3.0...v0.3.1) (2023-09-28)
### Features
* make openfeature an implicit namespace package ([#199](https://github.com/open-feature/python-sdk/issues/199)) ([c544918](https://github.com/open-feature/python-sdk/commit/c544918d65c2b0af621ec9e2261784e9a715dd9d))
## [0.3.0](https://github.com/open-feature/python-sdk/compare/v0.2.0...v0.3.0) (2023-09-25)
### ⚠ BREAKING CHANGES
* rename top-level package to openfeature ([#192](https://github.com/open-feature/python-sdk/issues/192))
### Code Refactoring
* rename top-level package to openfeature ([#192](https://github.com/open-feature/python-sdk/issues/192)) ([51f0d26](https://github.com/open-feature/python-sdk/commit/51f0d260f02cce5ab673305f212770ffcfc0744f))
## [0.2.0](https://github.com/open-feature/python-sdk/compare/v0.1.0...v0.2.0) (2023-09-09)

1
CODEOWNERS Normal file
View File

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

View File

@ -4,23 +4,59 @@
### System Requirements
Python 3.8 and above are required.
Python 3.9 and above are required.
### 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
A [`Makefile`](./Makefile) has been included in the project which should make it straightforward to start the project locally. We utilize virtual environments (see [`virtualenv`](https://docs.python.org/3/tutorial/venv.html)) in order to provide isolated development environments for the project. This reduces the risk of invalid or corrupt global packages. It also integrates nicely with Make, which will detect changes in the `requirements-dev.txt` file and update the virtual environment if any occur.
We use [uv](https://github.com/astral-sh/uv) for fast Python package management and dependency resolution.
Run `make init` to initialize the project's virtual environment and install all dev dependencies.
To install uv, follow the [installation guide](https://docs.astral.sh/uv/getting-started/installation/).
### Setup Development Environment
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
Run tests with `make 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
@ -55,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
```bash
make
uv run test --frozen
```
To start working on a new feature or bugfix, create a new branch and start working on it.
@ -107,4 +143,4 @@ on each other), the owner should try to get people aligned by:
## Design Choices
As with other OpenFeature SDKs, python-sdk follows the
[openfeature-specification](https://github.com/open-feature/spec).
[openfeature-specification](https://github.com/open-feature/spec).

View File

@ -1,40 +0,0 @@
VENV = . .venv/bin/activate
.PHONY: all
all: lint test
.PHONY: init
init: .venv
.venv: requirements-dev.txt
test -d .venv || python -m virtualenv .venv
$(VENV); pip install -Ur requirements-dev.txt
.PHONY: test
test: .venv
ifdef TEST
$(VENV); pytest $(TEST)
else
$(VENV); pytest
endif
test-harness:
git submodule update --init
.PHONY: lint
lint: .venv
$(VENV); black .
$(VENV); flake8 .
$(VENV); isort .
.PHONY: clean
clean:
@rm -rf .venv
@find -iname "*.pyc" -delete
.PHONY: e2e
e2e: .venv test-harness
# NOTE: only the evaluation feature is run for now
cp test-harness/features/evaluation.feature tests/features/
$(VENV); behave tests/features/
rm tests/features/*.feature

506
README.md Normal file
View File

@ -0,0 +1,506 @@
<!-- markdownlint-disable MD033 -->
<!-- x-hide-in-docs-start -->
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/white/openfeature-horizontal-white.svg" />
<img align="center" alt="OpenFeature Logo" src="https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg" />
</picture>
</p>
<h2 align="center">OpenFeature Python SDK</h2>
<!-- x-hide-in-docs-end -->
<!-- The 'github-badges' class is used in the docs -->
<p align="center" class="github-badges">
<a href="https://github.com/open-feature/spec/releases/tag/v0.8.0">
<img alt="Specification" src="https://img.shields.io/static/v1?label=Specification&message=v0.8.0&color=red&style=for-the-badge" />
</a>
<!-- x-release-please-start-version -->
<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.8.2&color=blue&style=for-the-badge" />
</a>
<!-- x-release-please-end -->
<br/>
<a href="https://github.com/open-feature/python-sdk/actions/workflows/merge.yml">
<img alt="Build status" src="https://github.com/open-feature/python-sdk/actions/workflows/build.yml/badge.svg" />
</a>
<a href="https://codecov.io/gh/open-feature/python-sdk">
<img alt="Codecov" src="https://codecov.io/gh/open-feature/python-sdk/branch/main/graph/badge.svg?token=FQ1I444HB3" />
</a>
<a href="https://www.python.org/downloads/">
<img alt="Min python version" src="https://img.shields.io/badge/python->=3.9-blue.svg" />
</a>
<a href="https://www.repostatus.org/#wip">
<img alt="Repo status" src="https://www.repostatus.org/badges/latest/wip.svg" />
</a>
</p>
<!-- x-hide-in-docs-start -->
[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool.
<!-- x-hide-in-docs-end -->
## 🚀 Quick start
### Requirements
- Python 3.9+
### Install
<!---x-release-please-start-version-->
#### Pip install
```bash
pip install openfeature-sdk==0.8.2
```
#### requirements.txt
```bash
openfeature-sdk==0.8.2
```
```python
pip install -r requirements.txt
```
<!---x-release-please-end-->
### Usage
```python
from openfeature import api
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
# flags defined in memory
my_flags = {
"v2_enabled": InMemoryFlag("on", {"on": True, "off": False})
}
# configure a provider
api.set_provider(InMemoryProvider(my_flags))
# create a client
client = api.get_client()
# get a bool flag value
flag_value = client.get_boolean_value("v2_enabled", False)
print("Value: " + str(flag_value))
```
## 🌟 Features
| Status | Features | Description |
|--------|---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|
| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
| ✅ | [Logging](#logging) | Integrate with popular logging packages. |
| ✅ | [Domains](#domains) | Logically bind clients with providers. |
| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
| ✅ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread) |
| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>
### Providers
[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.
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:
```python
from openfeature import api
from openfeature.provider.no_op_provider import NoOpProvider
api.set_provider(NoOpProvider())
open_feature_client = api.get_client()
```
In some situations, it may be beneficial to register multiple providers in the same application.
This is possible using [domains](#domains), which is covered in more detail below.
### Targeting
Sometimes, the value of a flag must consider some dynamic criteria about the application or user, such as the user's location, IP, email address, or the server's location.
In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting).
If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context).
```python
from openfeature.api import (
get_client,
get_provider,
set_provider,
get_evaluation_context,
set_evaluation_context,
)
global_context = EvaluationContext(
targeting_key="targeting_key1", attributes={"application": "value1"}
)
request_context = EvaluationContext(
targeting_key="targeting_key2", attributes={"email": request.form['email']}
)
## set global context
set_evaluation_context(global_context)
# merge second context
client = get_client(name="No-op Provider")
client.get_string_value("email", "fallback", request_context)
```
### 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.
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.
Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level.
```python
from openfeature.api import add_hooks
from openfeature.flag_evaluation import FlagEvaluationOptions
# set global hooks at the API-level
add_hooks([MyHook()])
# or configure them in the client
client = OpenFeatureClient()
client.add_hooks([MyHook()])
# or at the invocation-level
options = FlagEvaluationOptions(hooks=[MyHook()])
client.get_boolean_flag("my-flag", False, flag_evaluation_options=options)
```
### Logging
The OpenFeature SDK logs to the `openfeature` logger using the `logging` package from the Python Standard Library.
### Domains
Clients can be assigned to a domain.
A domain is a logical identifier which can be used to associate clients with a particular provider.
If a domain has no associated provider, the global provider is used.
```python
from openfeature import api
# Registering the default provider
api.set_provider(MyProvider());
# Registering a provider to a domain
api.set_provider(MyProvider(), "my-domain");
# A client bound to the default provider
default_client = api.get_client();
# A client bound to the MyProvider provider
domain_scoped_client = api.get_client("my-domain");
```
Domains can be defined on a provider during registration.
For more details, please refer to the [providers](#providers) section.
### Eventing
Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions. Initialization events (PROVIDER_READY on success, PROVIDER_ERROR on failure) are dispatched for every provider. Some providers support additional events, such as PROVIDER_CONFIGURATION_CHANGED.
Please refer to the documentation of the provider you're using to see what events are supported.
```python
from openfeature import api
from openfeature.provider import ProviderEvent
def on_provider_ready(event_details: EventDetails):
print(f"Provider {event_details.provider_name} is ready")
api.add_handler(ProviderEvent.PROVIDER_READY, on_provider_ready)
client = api.get_client()
def on_provider_ready(event_details: EventDetails):
print(f"Provider {event_details.provider_name} is ready")
client.add_handler(ProviderEvent.PROVIDER_READY, on_provider_ready)
```
### Transaction Context Propagation
Transaction context is a container for transaction-specific evaluation context (e.g. user id, user agent, IP).
Transaction context can be set where specific data is available (e.g. an auth service or request handler) and by using the transaction context propagator it will automatically be applied to all flag evaluations within a transaction (e.g. a request or thread).
You can implement a different transaction context propagator by implementing the `TransactionContextPropagator` class exported by the OpenFeature SDK.
In most cases you can use `ContextVarsTransactionContextPropagator` as it works for `threads` and `asyncio` using [Context Variables](https://peps.python.org/pep-0567/).
The following example shows a **multithreaded** Flask application using transaction context propagation to propagate the request ip and user id into request scoped transaction context.
```python
from flask import Flask, request
from openfeature import api
from openfeature.evaluation_context import EvaluationContext
from openfeature.transaction_context import ContextVarsTransactionContextPropagator
# Initialize the Flask app
app = Flask(__name__)
# Set the transaction context propagator
api.set_transaction_context_propagator(ContextVarsTransactionContextPropagator())
# Middleware to set the transaction context
# You can call api.set_transaction_context anywhere you have information,
# you want to have available in the code-paths below the current one.
@app.before_request
def set_request_transaction_context():
ip = request.headers.get("X-Forwarded-For", request.remote_addr)
user_id = request.headers.get("User-Id") # Assuming we're getting the user ID from a header
evaluation_context = EvaluationContext(targeting_key=user_id, attributes={"ipAddress": ip})
api.set_transaction_context(evaluation_context)
def create_response() -> str:
# This method can be anywhere in our code.
# The feature flag evaluation will automatically contain the transaction context merged with other context
new_response = api.get_client().get_string_value("response-message", "Hello User!")
return f"Message from server: {new_response}"
# Example route where we use the transaction context
@app.route('/greeting')
def some_endpoint():
return create_response()
```
This also works for asyncio based implementations e.g. FastApi as seen in the following example:
```python
from fastapi import FastAPI, Request
from openfeature import api
from openfeature.evaluation_context import EvaluationContext
from openfeature.transaction_context import ContextVarsTransactionContextPropagator
# Initialize the FastAPI app
app = FastAPI()
# Set the transaction context propagator
api.set_transaction_context_propagator(ContextVarsTransactionContextPropagator())
# Middleware to set the transaction context
@app.middleware("http")
async def set_request_transaction_context(request: Request, call_next):
ip = request.headers.get("X-Forwarded-For", request.client.host)
user_id = request.headers.get("User-Id") # Assuming we're getting the user ID from a header
evaluation_context = EvaluationContext(targeting_key=user_id, attributes={"ipAddress": ip})
api.set_transaction_context(evaluation_context)
response = await call_next(request)
return response
def create_response() -> str:
# This method can be located anywhere in our code.
# The feature flag evaluation will automatically include the transaction context merged with other context.
new_response = api.get_client().get_string_value("response-message", "Hello User!")
return f"Message from server: {new_response}"
# Example route where we use the transaction context
@app.get('/greeting')
async def some_endpoint():
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
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.
```python
from openfeature import api
api.shutdown()
```
## Extending
### Develop a provider
To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency.
This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/python-sdk-contrib) available under the OpenFeature organization.
Youll then need to write the provider by implementing the `AbstractProvider` class exported by the OpenFeature SDK.
```python
from typing import List, Optional, Union
from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagResolutionDetails
from openfeature.hook import Hook
from openfeature.provider import AbstractProvider, Metadata
class MyProvider(AbstractProvider):
def get_metadata(self) -> Metadata:
...
def get_provider_hooks(self) -> List[Hook]:
return []
def resolve_boolean_details(
self,
flag_key: str,
default_value: bool,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
...
def resolve_string_details(
self,
flag_key: str,
default_value: str,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]:
...
def resolve_integer_details(
self,
flag_key: str,
default_value: int,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]:
...
def resolve_float_details(
self,
flag_key: str,
default_value: float,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]:
...
def resolve_object_details(
self,
flag_key: str,
default_value: Union[dict, list],
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[Union[dict, list]]:
...
```
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!
### Develop a hook
To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency.
This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/python-sdk-contrib) available under the OpenFeature organization.
Implement your own hook by creating a hook that inherits from the `Hook` class.
Any of the evaluation life-cycle stages (`before`/`after`/`error`/`finally_after`) can be override to add the desired business logic.
```python
from openfeature.hook import Hook
class MyHook(Hook):
def after(self, hook_context: HookContext, details: FlagEvaluationDetails, hints: dict):
print("This runs after the flag has been evaluated")
```
> Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs!
<!-- x-hide-in-docs-start -->
## ⭐️ Support the project
- Give this repo a ⭐️!
- Follow us on social media:
- Twitter: [@openfeature](https://twitter.com/openfeature)
- LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/)
- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1)
- For more, check out our [community page](https://openfeature.dev/community/)
## 🤝 Contributing
Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide.
### Thanks to everyone who has already contributed
<a href="https://github.com/open-feature/python-sdk/graphs/contributors">
<img src="https://contrib.rocks/image?repo=open-feature/python-sdk" alt="Pictures of the folks who have contributed to the project" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
<!-- x-hide-in-docs-end -->

View File

@ -1,11 +0,0 @@
try:
from enum import StrEnum
except ImportError:
from enum import Enum
class StrEnum(str, Enum):
"""
Backport StrEnum for Python <3.11
"""
pass

View File

@ -1,65 +0,0 @@
import typing
from open_feature.client import OpenFeatureClient
from open_feature.evaluation_context import EvaluationContext
from open_feature.exception import GeneralError
from open_feature.hook import Hook
from open_feature.provider.metadata import Metadata
from open_feature.provider.no_op_provider import NoOpProvider
from open_feature.provider.provider import AbstractProvider
_provider: AbstractProvider = NoOpProvider()
_evaluation_context = EvaluationContext()
_hooks: typing.List[Hook] = []
def get_client(
name: typing.Optional[str] = None, version: typing.Optional[str] = None
) -> OpenFeatureClient:
return OpenFeatureClient(name=name, version=version, provider=_provider)
def set_provider(provider: AbstractProvider):
global _provider
if provider is None:
raise GeneralError(error_message="No provider")
_provider = provider
def get_provider() -> typing.Optional[AbstractProvider]:
global _provider
return _provider
def get_provider_metadata() -> typing.Optional[Metadata]:
global _provider
return _provider.get_metadata()
def get_evaluation_context() -> EvaluationContext:
global _evaluation_context
return _evaluation_context
def set_evaluation_context(evaluation_context: EvaluationContext):
global _evaluation_context
if evaluation_context is None:
raise GeneralError(error_message="No api level evaluation context")
_evaluation_context = evaluation_context
def add_hooks(hooks: typing.List[Hook]):
global _hooks
_hooks = _hooks + hooks
def clear_hooks():
global _hooks
_hooks = []
def get_hooks() -> typing.List[Hook]:
global _hooks
return _hooks

View File

@ -1,398 +0,0 @@
import logging
import typing
from dataclasses import dataclass
from open_feature import api
from open_feature.evaluation_context import EvaluationContext
from open_feature.exception import (
ErrorCode,
GeneralError,
OpenFeatureError,
TypeMismatchError,
)
from open_feature.flag_evaluation import (
FlagEvaluationDetails,
FlagEvaluationOptions,
FlagType,
Reason,
FlagResolutionDetails,
)
from open_feature.hook import Hook, HookContext
from open_feature.hook.hook_support import (
after_all_hooks,
after_hooks,
before_hooks,
error_hooks,
)
from open_feature.provider.no_op_provider import NoOpProvider
from open_feature.provider.provider import AbstractProvider
GetDetailCallable = typing.Union[
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]],
],
]
@dataclass
class ClientMetadata:
name: str
class OpenFeatureClient:
def __init__(
self,
name: typing.Optional[str],
version: typing.Optional[str],
provider: AbstractProvider,
context: typing.Optional[EvaluationContext] = None,
hooks: typing.Optional[typing.List[Hook]] = None,
):
self.name = name
self.version = version
self.context = context or EvaluationContext()
self.hooks = hooks or []
self.provider = provider
def get_metadata(self):
return ClientMetadata(name=self.name)
def add_hooks(self, hooks: typing.List[Hook]):
self.hooks = self.hooks + hooks
def get_boolean_value(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> bool:
return self.get_boolean_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).value
def get_boolean_details(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
return self.evaluate_flag_details(
FlagType.BOOLEAN,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
def get_string_value(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> str:
return self.get_string_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).value
def get_string_details(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
return self.evaluate_flag_details(
FlagType.STRING,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
def get_integer_value(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> int:
return self.get_integer_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).value
def get_integer_details(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
return self.evaluate_flag_details(
FlagType.INTEGER,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
def get_float_value(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> float:
return self.get_float_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).value
def get_float_details(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
return self.evaluate_flag_details(
FlagType.FLOAT,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
def get_object_value(
self,
flag_key: str,
default_value: typing.Union[dict, list],
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> dict:
return self.get_object_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).value
def get_object_details(
self,
flag_key: str,
default_value: typing.Union[dict, list],
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
return self.evaluate_flag_details(
FlagType.OBJECT,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
def evaluate_flag_details(
self,
flag_type: FlagType,
flag_key: str,
default_value: typing.Any,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
Evaluate the flag requested by the user from the clients provider.
:param flag_type: the type of the flag being returned
:param flag_key: the string key of the selected flag
:param default_value: backup value returned if no result found by the provider
:param evaluation_context: Information for the purposes of flag evaluation
:param flag_evaluation_options: Additional flag evaluation information
:return: a FlagEvaluationDetails object with the fully evaluated flag from a
provider
"""
if evaluation_context is None:
evaluation_context = EvaluationContext()
if flag_evaluation_options is None:
flag_evaluation_options = FlagEvaluationOptions()
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=None,
provider_metadata=None,
)
# 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
+ self.provider.get_provider_hooks()
)
# after, error, finally: Provider, Invocation, Client, API
reversed_merged_hooks = merged_hooks[:]
reversed_merged_hooks.reverse()
try:
# 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
invocation_context = before_hooks(
flag_type, hook_context, merged_hooks, hook_hints
)
invocation_context = invocation_context.merge(ctx2=evaluation_context)
# Requirement 3.2.2 merge: API.context->client.context->invocation.context
merged_context = (
api.get_evaluation_context()
.merge(self.context)
.merge(invocation_context)
)
flag_evaluation = self._create_provider_evaluation(
flag_type,
flag_key,
default_value,
merged_context,
)
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)
return FlagEvaluationDetails(
flag_key=flag_key,
value=default_value,
reason=Reason.ERROR,
error_code=err.error_code,
error_message=err.error_message,
)
# Catch any type of exception here since the user can provide any exception
# in the error hooks
except Exception as err: # noqa
error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints)
error_message = getattr(err, "error_message", str(err))
return FlagEvaluationDetails(
flag_key=flag_key,
value=default_value,
reason=Reason.ERROR,
error_code=ErrorCode.GENERAL,
error_message=error_message,
)
finally:
after_all_hooks(flag_type, hook_context, reversed_merged_hooks, hook_hints)
def _create_provider_evaluation(
self,
flag_type: FlagType,
flag_key: str,
default_value: typing.Any,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagEvaluationDetails:
"""
Encapsulated method to create a FlagEvaluationDetail from a specific provider.
:param flag_type: the type of the flag being returned
:param 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
:return: a FlagEvaluationDetails object with the fully evaluated flag from a
provider
"""
args = (
flag_key,
default_value,
evaluation_context,
)
if not self.provider:
logging.info("No provider configured, using no-op provider.")
self.provider = NoOpProvider()
get_details_callables: typing.Mapping[FlagType, GetDetailCallable] = {
FlagType.BOOLEAN: self.provider.resolve_boolean_details,
FlagType.INTEGER: self.provider.resolve_integer_details,
FlagType.FLOAT: self.provider.resolve_float_details,
FlagType.OBJECT: self.provider.resolve_object_details,
FlagType.STRING: self.provider.resolve_string_details,
}
get_details_callable = get_details_callables.get(flag_type)
if not get_details_callable:
raise GeneralError(error_message="Unknown flag type")
resolution = get_details_callable(*args)
# we need to check the get_args to be compatible with union types.
_typecheck_flag_value(resolution.value, flag_type)
return FlagEvaluationDetails(
flag_key=flag_key,
value=resolution.value,
variant=resolution.variant,
reason=resolution.reason,
error_code=resolution.error_code,
error_message=resolution.error_message,
)
def _typecheck_flag_value(value, flag_type):
type_map = {
FlagType.BOOLEAN: bool,
FlagType.STRING: str,
FlagType.OBJECT: (dict, list),
FlagType.FLOAT: float,
FlagType.INTEGER: int,
}
_type = type_map.get(flag_type)
if not _type:
raise GeneralError(error_message="Unknown flag type")
if not isinstance(value, _type):
raise TypeMismatchError(f"Expected type {_type} but got {type(value)}")

View File

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

View File

@ -1,60 +0,0 @@
from __future__ import annotations
import typing
from dataclasses import dataclass, field
from open_feature._backports.strenum import StrEnum
from open_feature.exception import ErrorCode
if typing.TYPE_CHECKING: # resolves a circular dependency in type annotations
from open_feature.hook import Hook
class FlagType(StrEnum):
BOOLEAN = "BOOLEAN"
STRING = "STRING"
OBJECT = "OBJECT"
FLOAT = "FLOAT"
INTEGER = "INTEGER"
class Reason(StrEnum):
CACHED = "CACHED"
DEFAULT = "DEFAULT"
DISABLED = "DISABLED"
ERROR = "ERROR"
STATIC = "STATIC"
SPLIT = "SPLIT"
TARGETING_MATCH = "TARGETING_MATCH"
UNKNOWN = "UNKNOWN"
T = typing.TypeVar("T", covariant=True)
@dataclass
class FlagEvaluationDetails(typing.Generic[T]):
flag_key: str
value: T
variant: typing.Optional[str] = None
reason: typing.Optional[Reason] = None
error_code: typing.Optional[ErrorCode] = None
error_message: typing.Optional[str] = None
@dataclass
class FlagEvaluationOptions:
hooks: typing.List[Hook] = field(default_factory=list)
hook_hints: dict = field(default_factory=dict)
U = typing.TypeVar("U", covariant=True)
@dataclass
class FlagResolutionDetails(typing.Generic[U]):
value: U
error_code: typing.Optional[ErrorCode] = None
error_message: typing.Optional[str] = None
reason: typing.Optional[Reason] = None
variant: typing.Optional[str] = None
flag_metadata: typing.Optional[str] = None

View File

@ -1,87 +0,0 @@
from __future__ import annotations
import typing
from abc import abstractmethod
from dataclasses import dataclass
from enum import Enum
from open_feature.evaluation_context import EvaluationContext
from open_feature.flag_evaluation import FlagEvaluationDetails, FlagType
class HookType(Enum):
BEFORE = "before"
AFTER = "after"
FINALLY_AFTER = "finally_after"
ERROR = "error"
@dataclass
class HookContext:
flag_key: str
flag_type: FlagType
default_value: typing.Any
evaluation_context: EvaluationContext
client_metadata: typing.Optional[dict] = None
provider_metadata: typing.Optional[dict] = None
class Hook:
@abstractmethod
def before(self, hook_context: HookContext, hints: dict) -> EvaluationContext:
"""
Runs before flag is resolved.
:param hook_context: Information about the particular flag evaluation
:param hints: An immutable mapping of data for users to
communicate to the hooks.
:return: An EvaluationContext. It will be merged with the
EvaluationContext instances from other hooks, the client and API.
"""
pass
@abstractmethod
def after(
self, hook_context: HookContext, details: FlagEvaluationDetails, hints: dict
):
"""
Runs after a flag is resolved.
:param hook_context: Information about the particular flag evaluation
:param details: Information about how the flag was resolved,
including any resolved values.
:param hints: A mapping of data for users to communicate to the hooks.
"""
pass
@abstractmethod
def error(self, hook_context: HookContext, exception: Exception, hints: dict):
"""
Run when evaluation encounters an error. Errors thrown will be swallowed.
:param hook_context: Information about the particular flag evaluation
:param exception: The exception that was thrown
:param hints: A mapping of data for users to communicate to the hooks.
"""
pass
@abstractmethod
def finally_after(self, hook_context: HookContext, hints: dict):
"""
Run after flag evaluation, including any error processing.
This will always run. Errors will be swallowed.
:param hook_context: Information about the particular flag evaluation
:param hints: A mapping of data for users to communicate to the hooks.
"""
pass
@abstractmethod
def supports_flag_value_type(self, flag_type: FlagType) -> bool:
"""
Check to see if the hook supports the particular flag type.
:param flag_type: particular type of the flag
:return: a boolean containing whether the flag type is supported (True)
or not (False)
"""
return True

View File

@ -1,130 +0,0 @@
import logging
import typing
from functools import reduce
from open_feature.evaluation_context import EvaluationContext
from open_feature.flag_evaluation import FlagEvaluationDetails, FlagType
from open_feature.hook import Hook, HookContext, HookType
def error_hooks(
flag_type: FlagType,
hook_context: HookContext,
exception: Exception,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
):
kwargs = {"hook_context": hook_context, "exception": exception, "hints": hints}
_execute_hooks(
flag_type=flag_type, hooks=hooks, hook_method=HookType.ERROR, **kwargs
)
def after_all_hooks(
flag_type: FlagType,
hook_context: HookContext,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
):
kwargs = {"hook_context": hook_context, "hints": hints}
_execute_hooks(
flag_type=flag_type, hooks=hooks, hook_method=HookType.FINALLY_AFTER, **kwargs
)
def after_hooks(
flag_type: FlagType,
hook_context: HookContext,
details: FlagEvaluationDetails,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
):
kwargs = {"hook_context": hook_context, "details": details, "hints": hints}
_execute_hooks_unchecked(
flag_type=flag_type, hooks=hooks, hook_method=HookType.AFTER, **kwargs
)
def before_hooks(
flag_type: FlagType,
hook_context: HookContext,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
) -> EvaluationContext:
kwargs = {"hook_context": hook_context, "hints": hints}
executed_hooks = _execute_hooks_unchecked(
flag_type=flag_type, hooks=hooks, hook_method=HookType.BEFORE, **kwargs
)
filtered_hooks = list(filter(lambda hook: hook is not None, executed_hooks))
if filtered_hooks:
return reduce(lambda a, b: a.merge(b), filtered_hooks)
return EvaluationContext()
def _execute_hooks(
flag_type: FlagType, hooks: typing.List[Hook], hook_method: HookType, **kwargs
) -> list:
"""
Run multiple hooks of any hook type. All of these hooks will be run through an
exception check.
:param flag_type: particular type of flag
:param hooks: a list of hooks
:param hook_method: the type of hook that is being run
:param kwargs: arguments that need to be provided to the hook method
:return: a list of results from the applied hook methods
"""
if hooks:
filtered_hooks = list(
filter(
lambda hook: hook.supports_flag_value_type(flag_type=flag_type), hooks
)
)
return [
_execute_hook_checked(hook, hook_method, **kwargs)
for hook in filtered_hooks
]
return []
def _execute_hooks_unchecked(
flag_type: FlagType, hooks, hook_method: HookType, **kwargs
) -> list:
"""
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
client.
:param flag_type: particular type of flag
:param hooks: a list of hooks
:param hook_method: the type of hook that is being run
:param kwargs: arguments that need to be provided to the hook method
:return: a list of results from the applied hook methods
"""
if hooks:
filtered_hooks = list(
filter(
lambda hook: hook.supports_flag_value_type(flag_type=flag_type), hooks
)
)
return [getattr(hook, hook_method.value)(**kwargs) for hook in filtered_hooks]
return []
def _execute_hook_checked(hook: Hook, hook_method: HookType, **kwargs):
"""
Try and run a single hook and catch any exception thrown. This is used in the
after all and error hooks since any error thrown at this point needs to be caught.
:param hook: a list of hooks
:param hook_method: the type of hook that is being run
:param kwargs: arguments that need to be provided to the hook method
:return: the result of the hook method
"""
try:
return getattr(hook, hook_method.value)(**kwargs)
except Exception: # noqa
logging.error(f"Exception when running {hook_method.value} hooks")

View File

@ -1,123 +0,0 @@
from dataclasses import dataclass
import typing
from open_feature._backports.strenum import StrEnum
from open_feature.evaluation_context import EvaluationContext
from open_feature.exception import ErrorCode
from open_feature.flag_evaluation import FlagResolutionDetails, Reason
from open_feature.hook import Hook
from open_feature.provider.metadata import Metadata
from open_feature.provider.provider import AbstractProvider
PASSED_IN_DEFAULT = "Passed in default"
@dataclass
class InMemoryMetadata(Metadata):
name: str = "In-Memory Provider"
T = typing.TypeVar("T", covariant=True)
@dataclass(frozen=True)
class InMemoryFlag(typing.Generic[T]):
class State(StrEnum):
ENABLED = "ENABLED"
DISABLED = "DISABLED"
flag_key: str
default_variant: str
variants: typing.Dict[str, T]
state: State = State.ENABLED
context_evaluator: typing.Optional[
typing.Callable[["InMemoryFlag", EvaluationContext], FlagResolutionDetails[T]]
] = None
def resolve(
self, evaluation_context: typing.Optional[EvaluationContext]
) -> FlagResolutionDetails[T]:
if self.context_evaluator:
return self.context_evaluator(
self, evaluation_context or EvaluationContext()
)
return FlagResolutionDetails(
value=self.variants[self.default_variant],
reason=Reason.STATIC,
variant=self.default_variant,
)
FlagStorage = typing.Dict[str, InMemoryFlag]
V = typing.TypeVar("V")
class InMemoryProvider(AbstractProvider):
_flags: FlagStorage
def __init__(self, flags: FlagStorage):
self._flags = flags.copy()
def get_metadata(self) -> Metadata:
return InMemoryMetadata()
def get_provider_hooks(self) -> typing.List[Hook]:
return []
def resolve_boolean_details(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
return self._resolve(flag_key, default_value, evaluation_context)
def resolve_string_details(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]:
return self._resolve(flag_key, default_value, evaluation_context)
def resolve_integer_details(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]:
return self._resolve(flag_key, default_value, evaluation_context)
def resolve_float_details(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]:
return self._resolve(flag_key, default_value, evaluation_context)
def resolve_object_details(
self,
flag_key: str,
default_value: typing.Union[dict, list],
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[typing.Union[dict, list]]:
return self._resolve(flag_key, default_value, evaluation_context)
def _resolve(
self,
flag_key: str,
default_value: V,
evaluation_context: typing.Optional[EvaluationContext],
) -> FlagResolutionDetails[V]:
flag = self._flags.get(flag_key)
if flag is None:
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)

View File

@ -1,62 +0,0 @@
import typing
from abc import abstractmethod
from open_feature.evaluation_context import EvaluationContext
from open_feature.flag_evaluation import FlagResolutionDetails
from open_feature.hook import Hook
from open_feature.provider.metadata import Metadata
class AbstractProvider:
@abstractmethod
def get_metadata(self) -> Metadata:
pass
@abstractmethod
def get_provider_hooks(self) -> typing.List[Hook]:
return []
@abstractmethod
def resolve_boolean_details(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
pass
@abstractmethod
def resolve_string_details(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]:
pass
@abstractmethod
def resolve_integer_details(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]:
pass
@abstractmethod
def resolve_float_details(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]:
pass
@abstractmethod
def resolve_object_details(
self,
flag_key: str,
default_value: typing.Union[dict, list],
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[typing.Union[dict, list]]:
pass

View File

@ -0,0 +1,14 @@
import sys
if sys.version_info >= (3, 11):
# re-export needed for type checking
from enum import StrEnum as StrEnum # noqa: PLC0414
else:
from enum import Enum
class StrEnum(str, Enum):
"""
Backport StrEnum for Python <3.11
"""
pass

View File

@ -0,0 +1,108 @@
from __future__ import annotations
import threading
from collections import defaultdict
from typing import TYPE_CHECKING
from openfeature.event import (
EventDetails,
EventHandler,
ProviderEvent,
ProviderEventDetails,
)
from openfeature.provider import FeatureProvider, ProviderStatus
if TYPE_CHECKING:
from openfeature.client import OpenFeatureClient
_global_lock = threading.RLock()
_global_handlers: dict[ProviderEvent, list[EventHandler]] = defaultdict(list)
_client_lock = threading.RLock()
_client_handlers: dict[OpenFeatureClient, dict[ProviderEvent, list[EventHandler]]] = (
defaultdict(lambda: defaultdict(list))
)
def run_client_handlers(
client: OpenFeatureClient, event: ProviderEvent, details: EventDetails
) -> None:
with _client_lock:
for handler in _client_handlers[client][event]:
handler(details)
def run_global_handlers(event: ProviderEvent, details: EventDetails) -> None:
with _global_lock:
for handler in _global_handlers[event]:
handler(details)
def add_client_handler(
client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler
) -> None:
with _client_lock:
handlers = _client_handlers[client][event]
handlers.append(handler)
_run_immediate_handler(client, event, handler)
def remove_client_handler(
client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler
) -> None:
with _client_lock:
handlers = _client_handlers[client][event]
handlers.remove(handler)
def add_global_handler(event: ProviderEvent, handler: EventHandler) -> None:
with _global_lock:
_global_handlers[event].append(handler)
from openfeature.api import get_client # noqa: PLC0415
_run_immediate_handler(get_client(), event, handler)
def remove_global_handler(event: ProviderEvent, handler: EventHandler) -> None:
with _global_lock:
_global_handlers[event].remove(handler)
def run_handlers_for_provider(
provider: FeatureProvider,
event: ProviderEvent,
provider_details: ProviderEventDetails,
) -> None:
details = EventDetails.from_provider_event_details(
provider.get_metadata().name, provider_details
)
# run the global handlers
run_global_handlers(event, details)
# run the handlers for clients associated to this provider
with _client_lock:
for client in _client_handlers:
if client.provider == provider:
run_client_handlers(client, event, details)
def _run_immediate_handler(
client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler
) -> None:
status_to_event = {
ProviderStatus.READY: ProviderEvent.PROVIDER_READY,
ProviderStatus.ERROR: ProviderEvent.PROVIDER_ERROR,
ProviderStatus.FATAL: ProviderEvent.PROVIDER_ERROR,
ProviderStatus.STALE: ProviderEvent.PROVIDER_STALE,
}
if event == status_to_event.get(client.get_provider_status()):
handler(EventDetails(provider_name=client.provider.get_metadata().name))
def clear() -> None:
with _global_lock:
_global_handlers.clear()
with _client_lock:
_client_handlers.clear()

75
openfeature/api.py Normal file
View File

@ -0,0 +1,75 @@
import typing
from openfeature import _event_support
from openfeature.client import OpenFeatureClient
from openfeature.evaluation_context import (
get_evaluation_context,
set_evaluation_context,
)
from openfeature.event import (
EventHandler,
ProviderEvent,
)
from openfeature.hook import add_hooks, clear_hooks, get_hooks
from openfeature.provider import FeatureProvider
from openfeature.provider._registry import provider_registry
from openfeature.provider.metadata import Metadata
from openfeature.transaction_context import (
get_transaction_context,
set_transaction_context,
set_transaction_context_propagator,
)
__all__ = [
"add_handler",
"add_hooks",
"clear_hooks",
"clear_providers",
"get_client",
"get_evaluation_context",
"get_hooks",
"get_provider_metadata",
"get_transaction_context",
"remove_handler",
"set_evaluation_context",
"set_provider",
"set_transaction_context",
"set_transaction_context_propagator",
"shutdown",
]
def get_client(
domain: typing.Optional[str] = None, version: typing.Optional[str] = None
) -> OpenFeatureClient:
return OpenFeatureClient(domain=domain, version=version)
def set_provider(
provider: FeatureProvider, domain: typing.Optional[str] = None
) -> None:
if domain is None:
provider_registry.set_default_provider(provider)
else:
provider_registry.set_provider(domain, provider)
def clear_providers() -> None:
provider_registry.clear_providers()
_event_support.clear()
def get_provider_metadata(domain: typing.Optional[str] = None) -> Metadata:
return provider_registry.get_provider(domain).get_metadata()
def shutdown() -> None:
provider_registry.shutdown()
def add_handler(event: ProviderEvent, handler: EventHandler) -> None:
_event_support.add_global_handler(event, handler)
def remove_handler(event: ProviderEvent, handler: EventHandler) -> None:
_event_support.remove_global_handler(event, handler)

978
openfeature/client.py Normal file
View File

@ -0,0 +1,978 @@
import logging
import typing
from collections.abc import Awaitable, Sequence
from dataclasses import dataclass
from openfeature import _event_support
from openfeature.evaluation_context import EvaluationContext, get_evaluation_context
from openfeature.event import EventHandler, ProviderEvent
from openfeature.exception import (
ErrorCode,
GeneralError,
OpenFeatureError,
ProviderFatalError,
ProviderNotReadyError,
TypeMismatchError,
)
from openfeature.flag_evaluation import (
FlagEvaluationDetails,
FlagEvaluationOptions,
FlagResolutionDetails,
FlagType,
FlagValueType,
Reason,
)
from openfeature.hook import Hook, HookContext, HookHints, get_hooks
from openfeature.hook._hook_support import (
after_all_hooks,
after_hooks,
before_hooks,
error_hooks,
)
from openfeature.provider import FeatureProvider, ProviderStatus
from openfeature.provider._registry import provider_registry
from openfeature.transaction_context import get_transaction_context
__all__ = [
"ClientMetadata",
"OpenFeatureClient",
]
logger = logging.getLogger("openfeature")
TypeMap = dict[
FlagType,
typing.Union[
type[bool],
type[int],
type[float],
type[str],
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
class ClientMetadata:
name: typing.Optional[str] = None
domain: typing.Optional[str] = None
class OpenFeatureClient:
def __init__(
self,
domain: typing.Optional[str],
version: typing.Optional[str],
context: typing.Optional[EvaluationContext] = None,
hooks: typing.Optional[list[Hook]] = None,
) -> None:
self.domain = domain
self.version = version
self.context = context or EvaluationContext()
self.hooks = hooks or []
@property
def provider(self) -> FeatureProvider:
return provider_registry.get_provider(self.domain)
def get_provider_status(self) -> ProviderStatus:
return provider_registry.get_provider_status(self.provider)
def get_metadata(self) -> ClientMetadata:
return ClientMetadata(domain=self.domain)
def add_hooks(self, hooks: list[Hook]) -> None:
self.hooks = self.hooks + hooks
def get_boolean_value(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> bool:
return self.get_boolean_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).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(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[bool]:
return self.evaluate_flag_details(
FlagType.BOOLEAN,
flag_key,
default_value,
evaluation_context,
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(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> str:
return self.get_string_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).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(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[str]:
return self.evaluate_flag_details(
FlagType.STRING,
flag_key,
default_value,
evaluation_context,
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(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> int:
return self.get_integer_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).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(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[int]:
return self.evaluate_flag_details(
FlagType.INTEGER,
flag_key,
default_value,
evaluation_context,
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(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> float:
return self.get_float_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).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(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[float]:
return self.evaluate_flag_details(
FlagType.FLOAT,
flag_key,
default_value,
evaluation_context,
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(
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]]:
return self.get_object_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).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(
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 self.evaluate_flag_details(
FlagType.OBJECT,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
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,
flag_type: FlagType,
flag_key: str,
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,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[bool]: ...
@typing.overload
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[int]: ...
@typing.overload
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[float]: ...
@typing.overload
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[str]: ...
@typing.overload
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: Sequence["FlagValueType"],
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[Sequence["FlagValueType"]]: ...
@typing.overload
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: typing.Mapping[str, "FlagValueType"],
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[typing.Mapping[str, "FlagValueType"]]: ...
async def evaluate_flag_details_async(
self,
flag_type: FlagType,
flag_key: str,
default_value: FlagValueType,
evaluation_context: typing.Optional[EvaluationContext] = None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails[FlagValueType]:
"""
Evaluate the flag requested by the user from the clients provider.
:param flag_type: the type of the flag being returned
:param flag_key: the string key of the selected flag
:param default_value: backup value returned if no result found by the provider
:param evaluation_context: Information for the purposes of flag evaluation
:param flag_evaluation_options: Additional flag evaluation information
:return: a typing.Awaitable[FlagEvaluationDetails] object with the fully evaluated flag from a
provider
"""
provider, hook_context, hook_hints, merged_hooks, reversed_merged_hooks = (
self._establish_hooks_and_provider(
flag_type,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
)
try:
if provider_err := self._assert_provider_status():
error_hooks(
flag_type,
hook_context,
provider_err,
reversed_merged_hooks,
hook_hints,
)
flag_evaluation = FlagEvaluationDetails(
flag_key=flag_key,
value=default_value,
reason=Reason.ERROR,
error_code=provider_err.error_code,
error_message=provider_err.error_message,
)
return flag_evaluation
merged_context = self._run_before_hooks_and_update_context(
flag_type,
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.
:param flag_type: the type of the flag being returned
:param flag_key: the string key of the selected flag
:param default_value: backup value returned if no result found by the provider
:param evaluation_context: Information for the purposes of flag evaluation
:param flag_evaluation_options: Additional flag evaluation information
:return: a FlagEvaluationDetails object with the fully evaluated flag from a
provider
"""
provider, hook_context, hook_hints, merged_hooks, reversed_merged_hooks = (
self._establish_hooks_and_provider(
flag_type,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
)
try:
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 = self._create_provider_evaluation(
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
)
flag_evaluation.value = default_value
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,
)
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(
self,
provider: FeatureProvider,
flag_type: FlagType,
flag_key: str,
default_value: FlagValueType,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagEvaluationDetails[FlagValueType]:
"""
Encapsulated method to create a FlagEvaluationDetail from a specific provider.
:param flag_type: the type of the flag being returned
:param 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
:return: a FlagEvaluationDetails object with the fully evaluated flag from a
provider
"""
get_details_callables: typing.Mapping[FlagType, ResolveDetailsCallable] = {
FlagType.BOOLEAN: provider.resolve_boolean_details,
FlagType.INTEGER: provider.resolve_integer_details,
FlagType.FLOAT: provider.resolve_float_details,
FlagType.OBJECT: provider.resolve_object_details,
FlagType.STRING: provider.resolve_string_details,
}
get_details_callable = get_details_callables.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 = 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 add_handler(self, event: ProviderEvent, handler: EventHandler) -> None:
_event_support.add_client_handler(self, event, handler)
def remove_handler(self, event: ProviderEvent, handler: EventHandler) -> None:
_event_support.remove_client_handler(self, event, handler)
def _typecheck_flag_value(
value: typing.Any, flag_type: FlagType
) -> typing.Optional[OpenFeatureError]:
type_map: TypeMap = {
FlagType.BOOLEAN: bool,
FlagType.STRING: str,
FlagType.OBJECT: (dict, list),
FlagType.FLOAT: float,
FlagType.INTEGER: int,
}
_type = type_map.get(flag_type)
if not _type:
return GeneralError(error_message="Unknown flag type")
if not isinstance(value, _type):
return TypeMismatchError(f"Expected type {_type} but got {type(value)}")
return None

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()

48
openfeature/event.py Normal file
View File

@ -0,0 +1,48 @@
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Callable, Optional, Union
from openfeature.exception import ErrorCode
__all__ = ["EventDetails", "EventHandler", "ProviderEvent", "ProviderEventDetails"]
class ProviderEvent(Enum):
PROVIDER_READY = "PROVIDER_READY"
PROVIDER_CONFIGURATION_CHANGED = "PROVIDER_CONFIGURATION_CHANGED"
PROVIDER_ERROR = "PROVIDER_ERROR"
PROVIDER_STALE = "PROVIDER_STALE"
@dataclass
class ProviderEventDetails:
flags_changed: Optional[list[str]] = None
message: Optional[str] = None
error_code: Optional[ErrorCode] = None
metadata: dict[str, Union[bool, str, int, float]] = field(default_factory=dict)
@dataclass
class EventDetails(ProviderEventDetails):
provider_name: str = ""
flags_changed: Optional[list[str]] = None
message: Optional[str] = None
error_code: Optional[ErrorCode] = None
metadata: dict[str, Union[bool, str, int, float]] = field(default_factory=dict)
@classmethod
def from_provider_event_details(
cls, provider_name: str, details: ProviderEventDetails
) -> EventDetails:
return cls(
provider_name=provider_name,
flags_changed=details.flags_changed,
message=details.message,
error_code=details.error_code,
metadata=details.metadata,
)
EventHandler = Callable[[EventDetails], None]

View File

@ -1,15 +1,22 @@
from __future__ import annotations
import typing
from enum import Enum
from collections.abc import Mapping
from openfeature._backports.strenum import StrEnum
class ErrorCode(Enum):
PROVIDER_NOT_READY = "PROVIDER_NOT_READY"
FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
PARSE_ERROR = "PARSE_ERROR"
TYPE_MISMATCH = "TYPE_MISMATCH"
TARGETING_KEY_MISSING = "TARGETING_KEY_MISSING"
INVALID_CONTEXT = "INVALID_CONTEXT"
GENERAL = "GENERAL"
__all__ = [
"ErrorCode",
"FlagNotFoundError",
"GeneralError",
"InvalidContextError",
"OpenFeatureError",
"ParseError",
"ProviderFatalError",
"ProviderNotReadyError",
"TargetingKeyMissingError",
"TypeMismatchError",
]
class OpenFeatureError(Exception):
@ -31,6 +38,36 @@ class OpenFeatureError(Exception):
self.error_code = error_code
class ProviderNotReadyError(OpenFeatureError):
"""
This exception should be raised when the provider is not ready to be used.
"""
def __init__(self, error_message: typing.Optional[str] = None):
"""
Constructor for the ProviderNotReadyError. The error code for this type of
exception is ErrorCode.PROVIDER_NOT_READY.
@param error_message: a string message representing why the error has been
raised
"""
super().__init__(ErrorCode.PROVIDER_NOT_READY, error_message)
class ProviderFatalError(OpenFeatureError):
"""
This exception should be raised when the provider encounters a fatal error.
"""
def __init__(self, error_message: typing.Optional[str] = None):
"""
Constructor for the ProviderFatalError. The error code for this type of
exception is ErrorCode.PROVIDER_FATAL.
@param error_message: a string message representing why the error has been
raised
"""
super().__init__(ErrorCode.PROVIDER_FATAL, error_message)
class FlagNotFoundError(OpenFeatureError):
"""
This exception should be raised when the provider cannot find a flag with the
@ -125,3 +162,32 @@ class InvalidContextError(OpenFeatureError):
raised
"""
super().__init__(ErrorCode.INVALID_CONTEXT, error_message)
class ErrorCode(StrEnum):
PROVIDER_NOT_READY = "PROVIDER_NOT_READY"
PROVIDER_FATAL = "PROVIDER_FATAL"
FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
PARSE_ERROR = "PARSE_ERROR"
TYPE_MISMATCH = "TYPE_MISMATCH"
TARGETING_KEY_MISSING = "TARGETING_KEY_MISSING"
INVALID_CONTEXT = "INVALID_CONTEXT"
GENERAL = "GENERAL"
__exceptions__: Mapping[str, typing.Callable[[str], OpenFeatureError]] = {
PROVIDER_NOT_READY: ProviderNotReadyError,
PROVIDER_FATAL: ProviderFatalError,
FLAG_NOT_FOUND: FlagNotFoundError,
PARSE_ERROR: ParseError,
TYPE_MISMATCH: TypeMismatchError,
TARGETING_KEY_MISSING: TargetingKeyMissingError,
INVALID_CONTEXT: InvalidContextError,
GENERAL: GeneralError,
}
@classmethod
def to_exception(
cls, error_code: ErrorCode, error_message: str
) -> OpenFeatureError:
exc = cls.__exceptions__.get(error_code.value, GeneralError)
return exc(error_message)

View File

@ -0,0 +1,106 @@
from __future__ import annotations
import typing
from collections.abc import Sequence
from dataclasses import dataclass, field
from openfeature._backports.strenum import StrEnum
from openfeature.exception import ErrorCode, OpenFeatureError
if typing.TYPE_CHECKING: # pragma: no cover
# resolves a circular dependency in type annotations
from openfeature.hook import Hook, HookHints
__all__ = [
"FlagEvaluationDetails",
"FlagEvaluationOptions",
"FlagMetadata",
"FlagResolutionDetails",
"FlagType",
"Reason",
]
class FlagType(StrEnum):
BOOLEAN = "BOOLEAN"
STRING = "STRING"
OBJECT = "OBJECT"
FLOAT = "FLOAT"
INTEGER = "INTEGER"
class Reason(StrEnum):
CACHED = "CACHED"
DEFAULT = "DEFAULT"
DISABLED = "DISABLED"
ERROR = "ERROR"
SPLIT = "SPLIT"
STATIC = "STATIC"
STALE = "STALE"
TARGETING_MATCH = "TARGETING_MATCH"
UNKNOWN = "UNKNOWN"
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)
@dataclass
class FlagEvaluationDetails(typing.Generic[T_co]):
flag_key: str
value: T_co
variant: typing.Optional[str] = None
flag_metadata: FlagMetadata = field(default_factory=dict)
reason: typing.Optional[typing.Union[str, Reason]] = None
error_code: typing.Optional[ErrorCode] = 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
class FlagEvaluationOptions:
hooks: list[Hook] = field(default_factory=list)
hook_hints: HookHints = field(default_factory=dict)
U_co = typing.TypeVar("U_co", covariant=True)
@dataclass
class FlagResolutionDetails(typing.Generic[U_co]):
value: U_co
error_code: typing.Optional[ErrorCode] = None
error_message: typing.Optional[str] = None
reason: typing.Optional[typing.Union[str, Reason]] = None
variant: typing.Optional[str] = None
flag_metadata: FlagMetadata = field(default_factory=dict)
def raise_for_error(self) -> None:
if self.error_code:
raise ErrorCode.to_exception(self.error_code, self.error_message or "")
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

@ -0,0 +1,160 @@
from __future__ import annotations
import typing
from collections.abc import Sequence
from datetime import datetime
from enum import Enum
from typing import TYPE_CHECKING
from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType, FlagValueType
if TYPE_CHECKING:
from openfeature.client import ClientMetadata
from openfeature.provider.metadata import Metadata
__all__ = [
"Hook",
"HookContext",
"HookHints",
"HookType",
"add_hooks",
"clear_hooks",
"get_hooks",
]
_hooks: list[Hook] = []
class HookType(Enum):
BEFORE = "before"
AFTER = "after"
FINALLY_AFTER = "finally_after"
ERROR = "error"
class HookContext:
def __init__(
self,
flag_key: str,
flag_type: FlagType,
default_value: FlagValueType,
evaluation_context: EvaluationContext,
client_metadata: typing.Optional[ClientMetadata] = None,
provider_metadata: typing.Optional[Metadata] = None,
):
self.flag_key = flag_key
self.flag_type = flag_type
self.default_value = default_value
self.evaluation_context = evaluation_context
self.client_metadata = client_metadata
self.provider_metadata = provider_metadata
def __setattr__(self, key: str, value: typing.Any) -> None:
if hasattr(self, key) and key in (
"flag_key",
"flag_type",
"default_value",
"client_metadata",
"provider_metadata",
):
raise AttributeError(f"Attribute {key!r} is immutable")
super().__setattr__(key, value)
# https://openfeature.dev/specification/sections/hooks/#requirement-421
HookHints = typing.Mapping[
str,
typing.Union[
bool,
int,
float,
str,
datetime,
Sequence["HookHints"],
typing.Mapping[str, "HookHints"],
],
]
class Hook:
def before(
self, hook_context: HookContext, hints: HookHints
) -> typing.Optional[EvaluationContext]:
"""
Runs before flag is resolved.
:param hook_context: Information about the particular flag evaluation
:param hints: An immutable mapping of data for users to
communicate to the hooks.
:return: An EvaluationContext. It will be merged with the
EvaluationContext instances from other hooks, the client and API.
"""
return None
def after(
self,
hook_context: HookContext,
details: FlagEvaluationDetails[FlagValueType],
hints: HookHints,
) -> None:
"""
Runs after a flag is resolved.
:param hook_context: Information about the particular flag evaluation
:param details: Information about how the flag was resolved,
including any resolved values.
:param hints: A mapping of data for users to communicate to the hooks.
"""
pass
def error(
self, hook_context: HookContext, exception: Exception, hints: HookHints
) -> None:
"""
Run when evaluation encounters an error. Errors thrown will be swallowed.
:param hook_context: Information about the particular flag evaluation
:param exception: The exception that was thrown
:param hints: A mapping of data for users to communicate to the hooks.
"""
pass
def finally_after(
self,
hook_context: HookContext,
details: FlagEvaluationDetails[FlagValueType],
hints: HookHints,
) -> None:
"""
Run after flag evaluation, including any error processing.
This will always run. Errors will be swallowed.
:param hook_context: Information about the particular flag evaluation
:param hints: A mapping of data for users to communicate to the hooks.
"""
pass
def supports_flag_value_type(self, flag_type: FlagType) -> bool:
"""
Check to see if the hook supports the particular flag type.
:param flag_type: particular type of the flag
:return: a boolean containing whether the flag type is supported (True)
or not (False)
"""
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

@ -2,18 +2,20 @@ import logging
import typing
from functools import reduce
from open_feature.evaluation_context import EvaluationContext
from open_feature.flag_evaluation import FlagEvaluationDetails, FlagType
from open_feature.hook import Hook, HookContext, HookType
from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType
from openfeature.hook import Hook, HookContext, HookHints, HookType
logger = logging.getLogger("openfeature")
def error_hooks(
flag_type: FlagType,
hook_context: HookContext,
exception: Exception,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
):
hooks: list[Hook],
hints: typing.Optional[HookHints] = None,
) -> None:
kwargs = {"hook_context": hook_context, "exception": exception, "hints": hints}
_execute_hooks(
flag_type=flag_type, hooks=hooks, hook_method=HookType.ERROR, **kwargs
@ -23,10 +25,11 @@ def error_hooks(
def after_all_hooks(
flag_type: FlagType,
hook_context: HookContext,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
):
kwargs = {"hook_context": hook_context, "hints": hints}
details: FlagEvaluationDetails[typing.Any],
hooks: list[Hook],
hints: typing.Optional[HookHints] = None,
) -> None:
kwargs = {"hook_context": hook_context, "details": details, "hints": hints}
_execute_hooks(
flag_type=flag_type, hooks=hooks, hook_method=HookType.FINALLY_AFTER, **kwargs
)
@ -35,10 +38,10 @@ def after_all_hooks(
def after_hooks(
flag_type: FlagType,
hook_context: HookContext,
details: FlagEvaluationDetails,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
):
details: FlagEvaluationDetails[typing.Any],
hooks: list[Hook],
hints: typing.Optional[HookHints] = None,
) -> None:
kwargs = {"hook_context": hook_context, "details": details, "hints": hints}
_execute_hooks_unchecked(
flag_type=flag_type, hooks=hooks, hook_method=HookType.AFTER, **kwargs
@ -48,14 +51,14 @@ def after_hooks(
def before_hooks(
flag_type: FlagType,
hook_context: HookContext,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
hooks: list[Hook],
hints: typing.Optional[HookHints] = None,
) -> EvaluationContext:
kwargs = {"hook_context": hook_context, "hints": hints}
executed_hooks = _execute_hooks_unchecked(
flag_type=flag_type, hooks=hooks, hook_method=HookType.BEFORE, **kwargs
)
filtered_hooks = list(filter(lambda hook: hook is not None, executed_hooks))
filtered_hooks = [result for result in executed_hooks if result is not None]
if filtered_hooks:
return reduce(lambda a, b: a.merge(b), filtered_hooks)
@ -64,7 +67,10 @@ def before_hooks(
def _execute_hooks(
flag_type: FlagType, hooks: typing.List[Hook], hook_method: HookType, **kwargs
flag_type: FlagType,
hooks: list[Hook],
hook_method: HookType,
**kwargs: typing.Any,
) -> list:
"""
Run multiple hooks of any hook type. All of these hooks will be run through an
@ -76,22 +82,19 @@ def _execute_hooks(
:param kwargs: arguments that need to be provided to the hook method
:return: a list of results from the applied hook methods
"""
if hooks:
filtered_hooks = list(
filter(
lambda hook: hook.supports_flag_value_type(flag_type=flag_type), hooks
)
)
return [
_execute_hook_checked(hook, hook_method, **kwargs)
for hook in filtered_hooks
]
return []
return [
_execute_hook_checked(hook, hook_method, **kwargs)
for hook in hooks
if hook.supports_flag_value_type(flag_type)
]
def _execute_hooks_unchecked(
flag_type: FlagType, hooks, hook_method: HookType, **kwargs
) -> list:
flag_type: FlagType,
hooks: list[Hook],
hook_method: HookType,
**kwargs: typing.Any,
) -> list[typing.Optional[EvaluationContext]]:
"""
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
@ -103,18 +106,16 @@ def _execute_hooks_unchecked(
:param kwargs: arguments that need to be provided to the hook method
:return: a list of results from the applied hook methods
"""
if hooks:
filtered_hooks = list(
filter(
lambda hook: hook.supports_flag_value_type(flag_type=flag_type), hooks
)
)
return [getattr(hook, hook_method.value)(**kwargs) for hook in filtered_hooks]
return []
return [
getattr(hook, hook_method.value)(**kwargs)
for hook in hooks
if hook.supports_flag_value_type(flag_type)
]
def _execute_hook_checked(hook: Hook, hook_method: HookType, **kwargs):
def _execute_hook_checked(
hook: Hook, hook_method: HookType, **kwargs: typing.Any
) -> typing.Optional[EvaluationContext]:
"""
Try and run a single hook and catch any exception thrown. This is used in the
after all and error hooks since any error thrown at this point needs to be caught.
@ -125,6 +126,10 @@ def _execute_hook_checked(hook: Hook, hook_method: HookType, **kwargs):
:return: the result of the hook method
"""
try:
return getattr(hook, hook_method.value)(**kwargs)
except Exception: # noqa
logging.error(f"Exception when running {hook_method.value} hooks")
return typing.cast(
"typing.Optional[EvaluationContext]",
getattr(hook, hook_method.value)(**kwargs),
)
except Exception: # pragma: no cover
logger.exception(f"Exception when running {hook_method.value} hooks")
return None

View File

@ -1,3 +1,6 @@
import typing
class MappingProxyType(dict):
"""
MappingProxyType is an immutable dictionary type, written to
@ -8,16 +11,16 @@ class MappingProxyType(dict):
When upgrading to Python 3.12, you can update all references
from:
`from open_feature.immutable_dict.mapping_proxy_type import MappingProxyType`
`from openfeature.immutable_dict.mapping_proxy_type import MappingProxyType`
to:
`from types import MappingProxyType`
"""
def __hash__(self):
def __hash__(self) -> int: # type:ignore[override]
return id(self)
def _immutable(self, *args, **kws):
def _immutable(self, *args: typing.Any, **kws: typing.Any) -> typing.NoReturn:
raise TypeError("immutable instance of dictionary")
__setitem__ = _immutable

View File

@ -0,0 +1,265 @@
from __future__ import annotations
import typing
from abc import abstractmethod
from collections.abc import Sequence
from enum import Enum
from openfeature.evaluation_context import EvaluationContext
from openfeature.event import ProviderEvent, ProviderEventDetails
from openfeature.flag_evaluation import FlagResolutionDetails
from openfeature.hook import Hook
from .metadata import Metadata
if typing.TYPE_CHECKING:
from openfeature.flag_evaluation import FlagValueType
__all__ = ["AbstractProvider", "FeatureProvider", "Metadata", "ProviderStatus"]
class ProviderStatus(Enum):
NOT_READY = "NOT_READY"
READY = "READY"
ERROR = "ERROR"
STALE = "STALE"
FATAL = "FATAL"
class FeatureProvider(typing.Protocol): # pragma: no cover
def attach(
self,
on_emit: typing.Callable[
[FeatureProvider, ProviderEvent, ProviderEventDetails], None
],
) -> None: ...
def detach(self) -> None: ...
def initialize(self, evaluation_context: EvaluationContext) -> None: ...
def shutdown(self) -> None: ...
def get_metadata(self) -> Metadata: ...
def get_provider_hooks(self) -> list[Hook]: ...
def resolve_boolean_details(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> 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(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> 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(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> 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(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> 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(
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]]
]: ...
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):
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(
self,
on_emit: typing.Callable[
[FeatureProvider, ProviderEvent, ProviderEventDetails], None
],
) -> None:
self._on_emit = on_emit
def detach(self) -> None:
if hasattr(self, "_on_emit"):
del self._on_emit
def initialize(self, evaluation_context: EvaluationContext) -> None:
pass
def shutdown(self) -> None:
pass
@abstractmethod
def get_metadata(self) -> Metadata:
pass
def get_provider_hooks(self) -> list[Hook]:
return []
@abstractmethod
def resolve_boolean_details(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
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
def resolve_string_details(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]:
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
def resolve_integer_details(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]:
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
def resolve_float_details(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]:
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
def resolve_object_details(
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]]
]:
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:
self.emit(ProviderEvent.PROVIDER_READY, details)
def emit_provider_configuration_changed(
self, details: ProviderEventDetails
) -> None:
self.emit(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details)
def emit_provider_error(self, details: ProviderEventDetails) -> None:
self.emit(ProviderEvent.PROVIDER_ERROR, details)
def emit_provider_stale(self, details: ProviderEventDetails) -> None:
self.emit(ProviderEvent.PROVIDER_STALE, details)
def emit(self, event: ProviderEvent, details: ProviderEventDetails) -> None:
if hasattr(self, "_on_emit"):
self._on_emit(self, event, details)

View File

@ -0,0 +1,140 @@
import typing
from openfeature._event_support import run_handlers_for_provider
from openfeature.evaluation_context import EvaluationContext, get_evaluation_context
from openfeature.event import (
ProviderEvent,
ProviderEventDetails,
)
from openfeature.exception import ErrorCode, GeneralError, OpenFeatureError
from openfeature.provider import FeatureProvider, ProviderStatus
from openfeature.provider.no_op_provider import NoOpProvider
class ProviderRegistry:
_default_provider: FeatureProvider
_providers: dict[str, FeatureProvider]
_provider_status: dict[FeatureProvider, ProviderStatus]
def __init__(self) -> None:
self._default_provider = NoOpProvider()
self._providers = {}
self._provider_status = {
self._default_provider: ProviderStatus.READY,
}
def set_provider(self, domain: str, provider: FeatureProvider) -> None:
if provider is None:
raise GeneralError(error_message="No provider")
providers = self._providers
if domain in providers:
old_provider = providers[domain]
del providers[domain]
if old_provider not in providers.values():
self._shutdown_provider(old_provider)
if provider not in providers.values():
self._initialize_provider(provider)
providers[domain] = provider
def get_provider(self, domain: typing.Optional[str]) -> FeatureProvider:
if domain is None:
return self._default_provider
return self._providers.get(domain, self._default_provider)
def set_default_provider(self, provider: FeatureProvider) -> None:
if provider is None:
raise GeneralError(error_message="No provider")
if self._default_provider:
self._shutdown_provider(self._default_provider)
self._default_provider = provider
self._initialize_provider(provider)
def get_default_provider(self) -> FeatureProvider:
return self._default_provider
def clear_providers(self) -> None:
self.shutdown()
self._providers.clear()
self._default_provider = NoOpProvider()
self._provider_status = {
self._default_provider: ProviderStatus.READY,
}
def shutdown(self) -> None:
for provider in {self._default_provider, *self._providers.values()}:
self._shutdown_provider(provider)
def _get_evaluation_context(self) -> EvaluationContext:
return get_evaluation_context()
def _initialize_provider(self, provider: FeatureProvider) -> None:
provider.attach(self.dispatch_event)
try:
if hasattr(provider, "initialize"):
provider.initialize(self._get_evaluation_context())
self.dispatch_event(
provider, ProviderEvent.PROVIDER_READY, ProviderEventDetails()
)
except Exception as err:
error_code = (
err.error_code
if isinstance(err, OpenFeatureError)
else ErrorCode.GENERAL
)
self.dispatch_event(
provider,
ProviderEvent.PROVIDER_ERROR,
ProviderEventDetails(
message=f"Provider initialization failed: {err}",
error_code=error_code,
),
)
def _shutdown_provider(self, provider: FeatureProvider) -> None:
try:
if hasattr(provider, "shutdown"):
provider.shutdown()
self._provider_status[provider] = ProviderStatus.NOT_READY
except Exception as err:
self.dispatch_event(
provider,
ProviderEvent.PROVIDER_ERROR,
ProviderEventDetails(
message=f"Provider shutdown failed: {err}",
error_code=ErrorCode.PROVIDER_FATAL,
),
)
provider.detach()
def get_provider_status(self, provider: FeatureProvider) -> ProviderStatus:
return self._provider_status.get(provider, ProviderStatus.NOT_READY)
def dispatch_event(
self,
provider: FeatureProvider,
event: ProviderEvent,
details: ProviderEventDetails,
) -> None:
self._update_provider_status(provider, event, details)
run_handlers_for_provider(provider, event, details)
def _update_provider_status(
self,
provider: FeatureProvider,
event: ProviderEvent,
details: ProviderEventDetails,
) -> None:
if event == ProviderEvent.PROVIDER_READY:
self._provider_status[provider] = ProviderStatus.READY
elif event == ProviderEvent.PROVIDER_STALE:
self._provider_status[provider] = ProviderStatus.STALE
elif event == ProviderEvent.PROVIDER_ERROR:
status = (
ProviderStatus.FATAL
if details.error_code == ErrorCode.PROVIDER_FATAL
else ProviderStatus.ERROR
)
self._provider_status[provider] = status
provider_registry = ProviderRegistry()

View File

@ -0,0 +1,187 @@
from __future__ import annotations
import typing
from collections.abc import Sequence
from dataclasses import dataclass, field
from openfeature._backports.strenum import StrEnum
from openfeature.evaluation_context import EvaluationContext
from openfeature.exception import ErrorCode
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
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"
@dataclass
class InMemoryMetadata(Metadata):
name: str = "In-Memory Provider"
T_co = typing.TypeVar("T_co", covariant=True)
@dataclass(frozen=True)
class InMemoryFlag(typing.Generic[T_co]):
class State(StrEnum):
ENABLED = "ENABLED"
DISABLED = "DISABLED"
default_variant: str
variants: dict[str, T_co]
flag_metadata: FlagMetadata = field(default_factory=dict)
state: State = State.ENABLED
context_evaluator: typing.Optional[
typing.Callable[
[InMemoryFlag[T_co], EvaluationContext], FlagResolutionDetails[T_co]
]
] = None
def resolve(
self, evaluation_context: typing.Optional[EvaluationContext]
) -> FlagResolutionDetails[T_co]:
if self.context_evaluator:
return self.context_evaluator(
self, evaluation_context or EvaluationContext()
)
return FlagResolutionDetails(
value=self.variants[self.default_variant],
reason=Reason.STATIC,
variant=self.default_variant,
flag_metadata=self.flag_metadata,
)
FlagStorage = dict[str, InMemoryFlag[typing.Any]]
V = typing.TypeVar("V")
class InMemoryProvider(AbstractProvider):
_flags: FlagStorage
def __init__(self, flags: FlagStorage) -> None:
self._flags = flags.copy()
def get_metadata(self) -> Metadata:
return InMemoryMetadata()
def get_provider_hooks(self) -> list[Hook]:
return []
def resolve_boolean_details(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
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(
self,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]:
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(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]:
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(
self,
flag_key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]:
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(
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(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(
self,
flag_key: str,
default_value: V,
evaluation_context: typing.Optional[EvaluationContext],
) -> FlagResolutionDetails[V]:
flag = self._flags.get(flag_key)
if flag is None:
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)
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,6 +1,6 @@
from dataclasses import dataclass
from open_feature.provider.metadata import Metadata
from openfeature.provider.metadata import Metadata
@dataclass

View File

@ -1,11 +1,17 @@
import typing
from __future__ import annotations
from open_feature.evaluation_context import EvaluationContext
from open_feature.flag_evaluation import FlagResolutionDetails, Reason
from open_feature.hook import Hook
from open_feature.provider.metadata import Metadata
from open_feature.provider.no_op_metadata import NoOpMetadata
from open_feature.provider.provider import AbstractProvider
import typing
from collections.abc import Sequence
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
from openfeature.provider import AbstractProvider
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"
@ -14,7 +20,7 @@ class NoOpProvider(AbstractProvider):
def get_metadata(self) -> Metadata:
return NoOpMetadata()
def get_provider_hooks(self) -> typing.List[Hook]:
def get_provider_hooks(self) -> list[Hook]:
return []
def resolve_boolean_details(
@ -68,9 +74,13 @@ class NoOpProvider(AbstractProvider):
def resolve_object_details(
self,
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,
) -> FlagResolutionDetails[typing.Union[dict, list]]:
) -> FlagResolutionDetails[
typing.Union[Sequence[FlagValueType], typing.Mapping[str, FlagValueType]]
]:
return FlagResolutionDetails(
value=default_value,
reason=Reason.DEFAULT,

View File

@ -0,0 +1,11 @@
import warnings
from openfeature.provider import AbstractProvider
__all__ = ["AbstractProvider"]
warnings.warn(
"openfeature.provider.provider is deprecated, use openfeature.provider instead",
DeprecationWarning,
stacklevel=1,
)

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

@ -0,0 +1,40 @@
from openfeature.evaluation_context import EvaluationContext
from openfeature.transaction_context.context_var_transaction_context_propagator import (
ContextVarsTransactionContextPropagator,
)
from openfeature.transaction_context.no_op_transaction_context_propagator import (
NoOpTransactionContextPropagator,
)
from openfeature.transaction_context.transaction_context_propagator import (
TransactionContextPropagator,
)
__all__ = [
"ContextVarsTransactionContextPropagator",
"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

@ -0,0 +1,24 @@
from contextvars import ContextVar
from typing import Optional
from openfeature.evaluation_context import EvaluationContext
from openfeature.transaction_context.transaction_context_propagator import (
TransactionContextPropagator,
)
class ContextVarsTransactionContextPropagator(TransactionContextPropagator):
_transaction_context_var: ContextVar[Optional[EvaluationContext]] = ContextVar(
"transaction_context", default=None
)
def get_transaction_context(self) -> EvaluationContext:
context = self._transaction_context_var.get()
if context is None:
context = EvaluationContext()
self._transaction_context_var.set(context)
return context
def set_transaction_context(self, transaction_context: EvaluationContext) -> None:
self._transaction_context_var.set(transaction_context)

View File

@ -0,0 +1,12 @@
from openfeature.evaluation_context import EvaluationContext
from openfeature.transaction_context.transaction_context_propagator import (
TransactionContextPropagator,
)
class NoOpTransactionContextPropagator(TransactionContextPropagator):
def get_transaction_context(self) -> EvaluationContext:
return EvaluationContext()
def set_transaction_context(self, transaction_context: EvaluationContext) -> None:
pass

View File

@ -0,0 +1,11 @@
import typing
from openfeature.evaluation_context import EvaluationContext
class TransactionContextPropagator(typing.Protocol):
def get_transaction_context(self) -> EvaluationContext: ...
def set_transaction_context(
self, transaction_context: EvaluationContext
) -> None: ...

1
openfeature/version.py Normal file
View File

@ -0,0 +1 @@
__version__ = "0.8.2"

View File

@ -1,13 +1,13 @@
# pyproject.toml
[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta"
requires = ["uv_build~=0.8.0"]
build-backend = "uv_build"
[project]
name = "openfeature_sdk"
version = "0.2.0"
version = "0.8.2"
description = "Standardizing Feature Flagging for Everyone"
readme = "readme.md"
readme = "README.md"
authors = [{ name = "OpenFeature", email = "openfeature-core@groups.io" }]
license = { file = "LICENSE" }
classifiers = [
@ -15,16 +15,112 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
]
keywords = []
keywords = [
"openfeature",
"feature",
"flags",
"toggles",
]
dependencies = []
requires-python = ">=3.8"
[project.optional-dependencies]
dev = ["black", "flake8", "isort", "pip-tools", "pytest", "pre-commit"]
requires-python = ">=3.9"
[project.urls]
Homepage = "https://github.com/open-feature/python-sdk"
[tool.isort]
profile = "black"
multi_line_output = 3
[dependency-groups]
dev = [
"behave",
"coverage[toml]>=6.5",
"pytest",
"pytest-asyncio",
"pre-commit"
]
[tool.uv]
required-version = "~=0.8.0"
[tool.uv.build-backend]
module-name = "openfeature"
module-root = ""
namespace = true
[tool.mypy]
files = "openfeature"
python_version = "3.9" # should be identical to the minimum supported version
namespace_packages = true
explicit_package_bases = true
local_partial_types = true # will become the new default from version 2
pretty = true
strict = true
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]
exclude = [
".git",
".venv",
"__pycache__",
"venv",
]
target-version = "py39"
[tool.ruff.lint]
select = [
"A",
"B",
"C4",
"C90",
"E",
"F",
"FLY",
"FURB",
"I",
"LOG",
"N",
"PERF",
"PGH",
"PLC",
"PLR0913",
"PLR0915",
"RUF",
"S",
"SIM",
"T10",
"T20",
"UP",
"W",
"YTT",
]
ignore = [
"E501", # the formatter will handle any too long line
]
[tool.ruff.lint.per-file-ignores]
"tests/**/*" = ["PLR0913", "S101"]
[tool.ruff.lint.pylint]
max-args = 6
max-statements = 30
[tool.ruff.lint.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
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"

201
readme.md
View File

@ -1,201 +0,0 @@
<!-- markdownlint-disable MD033 -->
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/white/openfeature-horizontal-white.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg">
<img align="center" alt="OpenFeature Logo">
</picture>
</p>
<h2 align="center">OpenFeature Python SDK</h2>
[![PyPI version](https://badge.fury.io/py/openfeature-sdk.svg)](https://badge.fury.io/py/openfeature-sdk)
![Python 3.8+](https://img.shields.io/badge/python->=3.8-blue.svg)
[![Project Status: WIP Initial development is in progress, but there has not yet been a stable, usable release suitable for the public.](https://www.repostatus.org/badges/latest/wip.svg)](https://www.repostatus.org/#wip)
[![Specification](https://img.shields.io/static/v1?label=Specification&message=v0.3.0&color=red)](https://github.com/open-feature/spec/tree/v0.3.0)
[![on-merge](https://github.com/open-feature/python-sdk/actions/workflows/merge.yml/badge.svg)](https://github.com/open-feature/python-sdk/actions/workflows/merge.yml)
[![codecov](https://codecov.io/gh/open-feature/python-sdk/branch/main/graph/badge.svg?token=FQ1I444HB3)](https://codecov.io/gh/open-feature/python-sdk)
> ⚠️ Development is in progress, but there's not a stable release available. ⚠️
This is the Python implementation of [OpenFeature](https://openfeature.dev), a vendor-agnostic abstraction library for evaluating feature flags.
We support multiple data types for flags (numbers, strings, booleans, objects) as well as hooks, which can alter the lifecycle of a flag evaluation.
This library is intended to be used in server-side contexts and has not been evaluated for use in mobile devices.
## 🔍 Requirements:
- Python 3.8+
## 📦 Installation:
### Add it to your build
<!---x-release-please-start-version-->
Pip install
```bash
pip install openfeature-sdk==0.2.0
```
requirements.txt
```bash
openfeature-sdk==0.2.0
```
```python
pip install requirements.txt
```
<!---x-release-please-end-->
## 🌟 Features:
- support for various backend [providers](https://openfeature.dev/docs/reference/concepts/provider)
- easy integration and extension via [hooks](https://openfeature.dev/docs/reference/concepts/hooks)
- bool, string, numeric, and object flag types
- [context-aware](https://openfeature.dev/docs/reference/concepts/evaluation-context) evaluation
## 🚀 Usage:
### Configure it
In order to use the sdk there is some minor configuration. Follow the script below:
```python
from open_feature import api
from open_feature.provider.no_op_provider import NoOpProvider
api.set_provider(NoOpProvider())
open_feature_client = api.get_client()
```
### Basics:
While Boolean provides the simplest introduction, we offer a variety of flag types.
```python
# Depending on the flag type, use one of the methods below
flag_key = "PROVIDER_FLAG"
boolean_result = open_feature_client.get_boolean_value(key=flag_key,default_value=False)
integer_result = open_feature_client.get_integer_value(key=flag_key,default_value=-1)
float_result = open_feature_client.get_float_value(key=flag_key,default_value=-1)
string_result = open_feature_client.get_string_value(key=flag_key,default_value="")
object_result = open_feature_client.get_object_value(key=flag_key,default_value={})
```
You can also bind a provider to a specific client by name instead of setting that provider globally:
```python
api.set_provider(NoOpProvider())
```
Each provider class may have further setup required i.e. secret keys, environment variables etc
### Context-aware evaluation:
Sometimes the value of a flag must take into account some dynamic criteria about the application or user, such as the user location, IP, email address, or the location of the server.
In OpenFeature, we refer to this as [`targeting`](https://openfeature.dev/specification/glossary#targeting).
If the flag system you're using supports targeting, you can provide the input data using the `EvaluationContext`.
```python
from open_feature.api import (
get_client,
get_provider,
set_provider
get_evaluation_context,
set_evaluation_context,
)
global_context = EvaluationContext(
targeting_key="targeting_key1", attributes={"application": "value1"}
)
request_context = EvaluationContext(
targeting_key="targeting_key2", attributes={"email": request.form['email']}
)
## set global context
set_evaluation_context(first_context)
# merge second context
client = get_client(name="No-op Provider", version="0.5.2")
client.get_string_value("email", None, request_context)
```
### Events
TBD (See Issue [#131](https://github.com/open-feature/python-sdk/issues/131))
### Providers:
To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/python-sdk-contrib) available under the OpenFeature organization. Finally, youll then need to write the provider itself. This can be accomplished by implementing the `Provider` interface exported by the OpenFeature SDK.
See [here](https://openfeature.dev/ecosystem) for a catalog of available providers.
### Hooks:
A hook is a mechanism that allows for adding arbitrary behavior at well-defined points of the flag evaluation life-cycle. Use cases include validating the resolved flag value, modifying or adding data to the evaluation context, logging, telemetry, and tracking.
```python
from open_feature.hook import Hook
class MyHook(Hook):
def after(self, hook_context: HookContext, details: FlagEvaluationDetails, hints: dict):
print("This runs after the flag has been evaluated")
# set global hooks at the API-level
from open_feature.api import add_hooks
add_hooks([MyHook()])
# or configure them in the client
client = OpenFeatureClient()
client.add_hooks([MyHook()])
```
See [here](https://openfeature.dev/ecosystem) for a catalog of available hooks.
### Logging:
TBD
## ⭐️ Support the project
- Give this repo a ⭐️!
- Follow us on social media:
- Twitter: [@openfeature](https://twitter.com/openfeature)
- LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/)
- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1)
- For more check out our [community page](https://openfeature.dev/community/)
## 🤝 Contributing
Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide.
### Thanks to everyone that has already contributed
<!-- TODO: update with correct repo -->
<a href="https://github.com/open-feature/python-sdk/graphs/contributors">
<img src="https://contrib.rocks/image?repo=open-feature/python-sdk" alt="Pictures of the folks who have contributed to the project" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
## Contacting us
We hold regular meetings which you can see [here](https://github.com/open-feature/community/#meetings-and-events).
We are also present on the `#openfeature` channel in the [CNCF slack](https://slack.cncf.io/).
## 📜 License
[Apache License 2.0](LICENSE)
<!-- TODO: add FOSSA widget -->
[openfeature-website]: https://openfeature.dev

View File

@ -9,8 +9,63 @@
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true,
"extra-files": [
"readme.md"
"README.md"
]
}
}
},
"changelog-sections": [
{
"type": "fix",
"section": "🐛 Bug Fixes"
},
{
"type": "feat",
"section": "✨ New Features"
},
{
"type": "chore",
"section": "🧹 Chore"
},
{
"type": "docs",
"section": "📚 Documentation"
},
{
"type": "perf",
"section": "🚀 Performance"
},
{
"type": "build",
"hidden": true,
"section": "🛠️ Build"
},
{
"type": "deps",
"section": "📦 Dependencies"
},
{
"type": "ci",
"hidden": true,
"section": "🚦 CI"
},
{
"type": "refactor",
"section": "🔄 Refactoring"
},
{
"type": "revert",
"section": "🔙 Reverts"
},
{
"type": "style",
"hidden": true,
"section": "🎨 Styling"
},
{
"type": "test",
"hidden": true,
"section": "🧪 Tests"
}
],
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
}

View File

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

View File

@ -1,10 +0,0 @@
pylint
pytest
pytest-mock
black
pip-tools
pre-commit
flake8
pytest-mock
coverage
behave

View File

@ -1,102 +0,0 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile requirements-dev.in
#
astroid==2.15.6
# via pylint
behave==1.2.6
# via -r requirements-dev.in
black==23.9.0
# via -r requirements-dev.in
build==1.0.3
# via pip-tools
cfgv==3.4.0
# via pre-commit
click==8.1.7
# via
# black
# pip-tools
coverage==7.3.1
# via -r requirements-dev.in
dill==0.3.7
# via pylint
distlib==0.3.7
# via virtualenv
filelock==3.12.3
# via virtualenv
flake8==6.0.0
# via -r requirements-dev.in
identify==2.5.27
# via pre-commit
iniconfig==2.0.0
# via pytest
isort==5.12.0
# via pylint
lazy-object-proxy==1.9.0
# via astroid
mccabe==0.7.0
# via
# flake8
# pylint
mypy-extensions==1.0.0
# via black
nodeenv==1.8.0
# via pre-commit
packaging==23.1
# via
# black
# build
# pytest
parse==1.19.1
# via
# behave
# parse-type
parse-type==0.6.2
# via behave
pathspec==0.11.2
# via black
pip-tools==7.3.0
# via -r requirements-dev.in
platformdirs==3.10.0
# via
# black
# pylint
# virtualenv
pluggy==1.3.0
# via pytest
pre-commit==3.4.0
# via -r requirements-dev.in
pycodestyle==2.10.0
# via flake8
pyflakes==3.0.1
# via flake8
pylint==2.17.5
# via -r requirements-dev.in
pyproject-hooks==1.0.0
# via build
pytest==7.4.2
# via
# -r requirements-dev.in
# pytest-mock
pytest-mock==3.11.1
# via -r requirements-dev.in
pyyaml==6.0.1
# via pre-commit
six==1.16.0
# via
# behave
# parse-type
tomlkit==0.12.1
# via pylint
virtualenv==20.24.5
# via pre-commit
wheel==0.41.2
# via pip-tools
wrapt==1.15.0
# via astroid
# The following packages are considered to be unsafe in a requirements file:
# pip
# setuptools

View File

View File

@ -1,6 +0,0 @@
#
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile requirements.in
#

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

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

View File

@ -1,17 +1,16 @@
import pytest
from open_feature import api
from open_feature.provider.no_op_provider import NoOpProvider
from openfeature import api
from openfeature.provider.no_op_provider import NoOpProvider
@pytest.fixture(autouse=True)
def clear_provider():
def clear_providers():
"""
For tests that use set_provider(), we need to clear the provider to avoid issues
in other tests.
"""
yield
_provider = None # noqa: F841
api.clear_providers()
@pytest.fixture()

View File

@ -1,4 +1,4 @@
from open_feature.evaluation_context import EvaluationContext
from openfeature.evaluation_context import EvaluationContext
def test_empty_evaluation_context_can_be_merged_with_non_empty_context():

View File

@ -1,6 +1,6 @@
from open_feature.evaluation_context import EvaluationContext
from open_feature.flag_evaluation import FlagResolutionDetails, Reason
from open_feature.provider.in_memory_provider import InMemoryFlag
from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
from openfeature.provider.in_memory_provider import InMemoryFlag
def context_func(flag: InMemoryFlag, evaluation_context: EvaluationContext):
@ -22,35 +22,30 @@ def context_func(flag: InMemoryFlag, evaluation_context: EvaluationContext):
IN_MEMORY_FLAGS = {
"boolean-flag": InMemoryFlag(
flag_key="boolean-flag",
state=InMemoryFlag.State.ENABLED,
default_variant="on",
variants={"on": True, "off": False},
context_evaluator=None,
),
"string-flag": InMemoryFlag(
flag_key="string-flag",
state=InMemoryFlag.State.ENABLED,
default_variant="greeting",
variants={"greeting": "hi", "parting": "bye"},
context_evaluator=None,
),
"integer-flag": InMemoryFlag(
flag_key="integer-flag",
state=InMemoryFlag.State.ENABLED,
default_variant="ten",
variants={"one": 1, "ten": 10},
context_evaluator=None,
),
"float-flag": InMemoryFlag(
flag_key="float-flag",
state=InMemoryFlag.State.ENABLED,
default_variant="half",
variants={"tenth": 0.1, "half": 0.5},
context_evaluator=None,
),
"object-flag": InMemoryFlag(
flag_key="object-flag",
state=InMemoryFlag.State.ENABLED,
default_variant="template",
variants={
@ -64,16 +59,26 @@ IN_MEMORY_FLAGS = {
context_evaluator=None,
),
"context-aware": InMemoryFlag(
flag_key="context-aware",
state=InMemoryFlag.State.ENABLED,
variants={"internal": "INTERNAL", "external": "EXTERNAL"},
default_variant="external",
context_evaluator=context_func,
),
"wrong-flag": InMemoryFlag(
flag_key="wrong-flag",
state="ENABLED",
state=InMemoryFlag.State.ENABLED,
variants={"one": "uno", "two": "dos"},
default_variant="one",
),
"metadata-flag": InMemoryFlag(
state=InMemoryFlag.State.ENABLED,
default_variant="on",
variants={"on": True, "off": False},
context_evaluator=None,
flag_metadata={
"string": "1.0.2",
"integer": 2,
"float": 0.1,
"boolean": True,
},
),
}

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ from unittest import mock
import pytest
from open_feature.evaluation_context import EvaluationContext
from openfeature.evaluation_context import EvaluationContext
@pytest.fixture()

View File

@ -1,20 +1,81 @@
from unittest.mock import ANY
from unittest.mock import ANY, MagicMock
from open_feature.flag_evaluation import FlagEvaluationDetails, FlagType
from open_feature.hook import HookContext
from open_feature.hook.hook_support import (
import pytest
from openfeature.client import ClientMetadata
from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType
from openfeature.hook import Hook, HookContext
from openfeature.hook._hook_support import (
after_all_hooks,
after_hooks,
before_hooks,
error_hooks,
)
from open_feature.immutable_dict.mapping_proxy_type import MappingProxyType
from openfeature.immutable_dict.mapping_proxy_type import MappingProxyType
from openfeature.provider.metadata import Metadata
def test_hook_context_has_required_and_optional_fields():
"""Requirement
4.1.1 - Hook context MUST provide: the "flag key", "flag value type", "evaluation context", and the "default value".
4.1.2 - The "hook context" SHOULD provide: access to the "client metadata" and the "provider metadata" fields.
"""
# Given/When
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, EvaluationContext())
# Then
assert hasattr(hook_context, "flag_key")
assert hasattr(hook_context, "flag_type")
assert hasattr(hook_context, "default_value")
assert hasattr(hook_context, "evaluation_context")
assert hasattr(hook_context, "client_metadata")
assert hasattr(hook_context, "provider_metadata")
def test_hook_context_has_immutable_and_mutable_fields():
"""Requirement
4.1.3 - The "flag key", "flag type", and "default value" properties MUST be immutable.
4.1.4.1 - The evaluation context MUST be mutable only within the before hook.
4.2.2.2 - The client "metadata" field in the "hook context" MUST be immutable.
4.2.2.3 - The provider "metadata" field in the "hook context" MUST be immutable.
"""
# Given
hook_context = HookContext(
"flag_key", FlagType.BOOLEAN, True, EvaluationContext(), ClientMetadata("name")
)
# When
with pytest.raises(AttributeError):
hook_context.flag_key = "new_key"
with pytest.raises(AttributeError):
hook_context.flag_type = FlagType.STRING
with pytest.raises(AttributeError):
hook_context.default_value = "new_value"
with pytest.raises(AttributeError):
hook_context.client_metadata = ClientMetadata("new_name")
with pytest.raises(AttributeError):
hook_context.provider_metadata = Metadata("name")
hook_context.evaluation_context = EvaluationContext("targeting_key")
# Then
assert hook_context.flag_key == "flag_key"
assert hook_context.flag_type is FlagType.BOOLEAN
assert hook_context.default_value is True
assert hook_context.evaluation_context.targeting_key == "targeting_key"
assert hook_context.client_metadata.name == "name"
assert hook_context.provider_metadata is None
def test_error_hooks_run_error_method(mock_hook):
# Given
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, "")
hook_hints = MappingProxyType(dict())
hook_hints = MappingProxyType({})
# When
error_hooks(FlagType.BOOLEAN, hook_context, Exception, [mock_hook], hook_hints)
# Then
@ -28,7 +89,7 @@ def test_error_hooks_run_error_method(mock_hook):
def test_before_hooks_run_before_method(mock_hook):
# Given
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, "")
hook_hints = MappingProxyType(dict())
hook_hints = MappingProxyType({})
# When
before_hooks(FlagType.BOOLEAN, hook_context, [mock_hook], hook_hints)
# Then
@ -37,13 +98,30 @@ def test_before_hooks_run_before_method(mock_hook):
mock_hook.before.assert_called_with(hook_context=hook_context, hints=hook_hints)
def test_before_hooks_merges_evaluation_contexts():
# Given
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, "")
hook_1 = MagicMock(spec=Hook)
hook_1.before.return_value = EvaluationContext("foo", {"key_1": "val_1"})
hook_2 = MagicMock(spec=Hook)
hook_2.before.return_value = EvaluationContext("bar", {"key_2": "val_2"})
hook_3 = MagicMock(spec=Hook)
hook_3.before.return_value = None
# When
context = before_hooks(FlagType.BOOLEAN, hook_context, [hook_1, hook_2, hook_3])
# Then
assert context == EvaluationContext("bar", {"key_1": "val_1", "key_2": "val_2"})
def test_after_hooks_run_after_method(mock_hook):
# Given
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, "")
flag_evaluation_details = FlagEvaluationDetails(
hook_context.flag_key, "val", "unknown"
)
hook_hints = MappingProxyType(dict())
hook_hints = MappingProxyType({})
# When
after_hooks(
FlagType.BOOLEAN, hook_context, flag_evaluation_details, [mock_hook], hook_hints
@ -59,12 +137,17 @@ def test_after_hooks_run_after_method(mock_hook):
def test_finally_after_hooks_run_finally_after_method(mock_hook):
# Given
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, "")
hook_hints = MappingProxyType(dict())
flag_evaluation_details = FlagEvaluationDetails(
hook_context.flag_key, "val", "unknown"
)
hook_hints = MappingProxyType({})
# When
after_all_hooks(FlagType.BOOLEAN, hook_context, [mock_hook], hook_hints)
after_all_hooks(
FlagType.BOOLEAN, hook_context, flag_evaluation_details, [mock_hook], hook_hints
)
# Then
mock_hook.supports_flag_value_type.assert_called_once()
mock_hook.finally_after.assert_called_once()
mock_hook.finally_after.assert_called_with(
hook_context=hook_context, hints=hook_hints
hook_context=hook_context, details=flag_evaluation_details, hints=hook_hints
)

View File

@ -1,8 +1,10 @@
from numbers import Number
from open_feature.exception import ErrorCode
from open_feature.flag_evaluation import FlagResolutionDetails, Reason
from open_feature.provider.in_memory_provider import InMemoryProvider, InMemoryFlag
import pytest
from openfeature.exception import ErrorCode
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
def test_should_return_in_memory_provider_metadata():
@ -15,21 +17,27 @@ def test_should_return_in_memory_provider_metadata():
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
provider = InMemoryProvider({})
# When
flag = provider.resolve_boolean_details(flag_key="Key", default_value=True)
flag_sync = 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
assert flag is not None
assert flag.value is True
assert isinstance(flag.value, bool)
assert flag.reason == Reason.ERROR
assert flag.error_code == ErrorCode.FLAG_NOT_FOUND
assert flag.error_message == "Flag 'Key' not found"
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
def context_evaluator(flag: InMemoryFlag, evaluation_context: dict):
return FlagResolutionDetails(
@ -40,7 +48,6 @@ def test_calls_context_evaluator_if_present():
provider = InMemoryProvider(
{
"Key": InMemoryFlag(
"Key",
"true",
{"true": True, "false": False},
context_evaluator=context_evaluator,
@ -48,95 +55,126 @@ def test_calls_context_evaluator_if_present():
}
)
# 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
assert flag is not None
assert flag.value is False
assert isinstance(flag.value, bool)
assert flag.reason == Reason.TARGETING_MATCH
assert flag_sync == flag_async
for flag in [flag_sync, flag_async]:
assert flag is not None
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
provider = InMemoryProvider(
{"Key": InMemoryFlag("Key", "true", {"true": True, "false": False})}
{"Key": InMemoryFlag("true", {"true": True, "false": False})}
)
# 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
assert flag is not None
assert flag.value is True
assert isinstance(flag.value, bool)
assert flag.variant == "true"
assert flag_sync == flag_async
for flag in [flag_sync, flag_async]:
assert flag is not None
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
provider = InMemoryProvider(
{"Key": InMemoryFlag("Key", "hundred", {"zero": 0, "hundred": 100})}
{"Key": InMemoryFlag("hundred", {"zero": 0, "hundred": 100})}
)
# 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
assert flag is not None
assert flag.value == 100
assert isinstance(flag.value, Number)
assert flag.variant == "hundred"
assert flag_sync == flag_async
for flag in [flag_sync, flag_async]:
assert flag is not None
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
provider = InMemoryProvider(
{"Key": InMemoryFlag("Key", "ten", {"zero": 0.0, "ten": 10.23})}
{"Key": InMemoryFlag("ten", {"zero": 0.0, "ten": 10.23})}
)
# 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
assert flag is not None
assert flag.value == 10.23
assert isinstance(flag.value, Number)
assert flag.variant == "ten"
assert flag_sync == flag_async
for flag in [flag_sync, flag_async]:
assert flag is not None
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
provider = InMemoryProvider(
{
"Key": InMemoryFlag(
"Key",
"stringVariant",
{"defaultVariant": "Default", "stringVariant": "String"},
)
}
)
# 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
assert flag is not None
assert flag.value == "String"
assert isinstance(flag.value, str)
assert flag.variant == "stringVariant"
assert flag_sync == flag_async
for flag in [flag_sync, flag_async]:
assert flag is not None
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
provider = InMemoryProvider(
{
"Key": InMemoryFlag(
"Key", "twoItems", {"empty": [], "twoItems": ["item1", "item2"]}
)
}
{"Key": InMemoryFlag("twoItems", {"empty": [], "twoItems": ["item1", "item2"]})}
)
# 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
assert flag is not None
assert flag.value == ["item1", "item2"]
assert isinstance(flag.value, list)
assert flag.variant == "twoItems"
assert flag_sync == flag_async
for flag in [flag_sync, flag_async]:
assert flag is not None
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
return_value = {
"String": "string",
@ -144,12 +182,15 @@ def test_should_resolve_object_flag_from_in_memory():
"Boolean": True,
}
provider = InMemoryProvider(
{"Key": InMemoryFlag("Key", "obj", {"obj": return_value, "empty": {}})}
{"Key": InMemoryFlag("obj", {"obj": return_value, "empty": {}})}
)
# 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
assert flag is not None
assert flag.value == return_value
assert isinstance(flag.value, dict)
assert flag.variant == "obj"
assert flag_sync == flag_async
for flag in [flag_sync, flag_async]:
assert flag is not None
assert flag.value == return_value
assert isinstance(flag.value, dict)
assert flag.variant == "obj"

View File

@ -1,6 +1,6 @@
from numbers import Number
from open_feature.provider.no_op_provider import NoOpProvider
from openfeature.provider.no_op_provider import NoOpProvider
def test_should_return_no_op_provider_metadata():

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

@ -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

@ -2,33 +2,35 @@ from unittest.mock import MagicMock
import pytest
from open_feature.api import (
get_client,
get_provider,
set_provider,
get_provider_metadata,
get_evaluation_context,
set_evaluation_context,
get_hooks,
from openfeature.api import (
add_handler,
add_hooks,
clear_hooks,
clear_providers,
get_client,
get_evaluation_context,
get_hooks,
get_provider_metadata,
remove_handler,
set_evaluation_context,
set_provider,
shutdown,
)
from open_feature.evaluation_context import EvaluationContext
from open_feature.exception import ErrorCode, GeneralError
from open_feature.hook import Hook
from open_feature.provider.metadata import Metadata
from open_feature.provider.no_op_provider import NoOpProvider
from openfeature.evaluation_context import EvaluationContext
from openfeature.event import EventDetails, ProviderEvent, ProviderEventDetails
from openfeature.exception import ErrorCode, GeneralError, ProviderFatalError
from openfeature.hook import Hook
from openfeature.provider import FeatureProvider, Metadata, ProviderStatus
from openfeature.provider.no_op_provider import NoOpProvider
def test_should_not_raise_exception_with_noop_client():
# Given
# No provider has been set
# When
client = get_client(name="Default Provider", version="1.0")
client = get_client()
# Then
assert client.name == "Default Provider"
assert client.version == "1.0"
assert isinstance(client.provider, NoOpProvider)
@ -37,11 +39,9 @@ def test_should_return_open_feature_client_when_configured_correctly():
set_provider(NoOpProvider())
# When
client = get_client(name="No-op Provider", version="1.0")
client = get_client()
# Then
assert client.name == "No-op Provider"
assert client.version == "1.0"
assert isinstance(client.provider, NoOpProvider)
@ -56,16 +56,30 @@ def test_should_try_set_provider_and_fail_if_none_provided():
assert ge.value.error_code == ErrorCode.GENERAL
def test_should_return_a_provider_if_setup_correctly():
def test_should_invoke_provider_initialize_function_on_newly_registered_provider():
# Given
set_provider(NoOpProvider())
evaluation_context = EvaluationContext("targeting_key", {"attr1": "val1"})
provider = MagicMock(spec=FeatureProvider)
# When
provider = get_provider()
set_evaluation_context(evaluation_context)
set_provider(provider)
# Then
assert provider
assert isinstance(provider, NoOpProvider)
provider.initialize.assert_called_with(evaluation_context)
def test_should_invoke_provider_shutdown_function_once_provider_is_no_longer_in_use():
# Given
provider_1 = MagicMock(spec=FeatureProvider)
provider_2 = MagicMock(spec=FeatureProvider)
# When
set_provider(provider_1)
set_provider(provider_2)
# Then
assert provider_1.shutdown.called
def test_should_retrieve_metadata_for_configured_provider():
@ -116,3 +130,230 @@ def test_should_add_hooks_to_api_hooks():
# Then
assert get_hooks() == [hook_1, hook_2]
def test_should_call_provider_shutdown_on_api_shutdown():
# Given
provider = MagicMock(spec=FeatureProvider)
set_provider(provider)
# When
shutdown()
# Then
assert provider.shutdown.called
def test_should_provide_a_function_to_bind_provider_through_domain():
# Given
provider = MagicMock(spec=FeatureProvider)
test_client = get_client("test")
default_client = get_client()
# When
set_provider(provider, domain="test")
# Then
assert default_client.provider != provider
assert default_client.domain is None
assert test_client.provider == provider
assert test_client.domain == "test"
def test_should_not_initialize_provider_already_bound_to_another_domain():
# Given
provider = MagicMock(spec=FeatureProvider)
set_provider(provider, "foo")
# When
set_provider(provider, "bar")
# Then
provider.initialize.assert_called_once()
def test_should_shutdown_unbound_provider():
# Given
provider = MagicMock(spec=FeatureProvider)
set_provider(provider, "foo")
# When
other_provider = MagicMock(spec=FeatureProvider)
set_provider(other_provider, "foo")
provider.shutdown.assert_called_once()
def test_should_not_shutdown_provider_bound_to_another_domain():
# Given
provider = MagicMock(spec=FeatureProvider)
set_provider(provider, "foo")
set_provider(provider, "bar")
# When
other_provider = MagicMock(spec=FeatureProvider)
set_provider(other_provider, "foo")
provider.shutdown.assert_not_called()
def test_shutdown_should_shutdown_every_registered_provider_once():
# Given
provider_1 = MagicMock(spec=FeatureProvider)
provider_2 = MagicMock(spec=FeatureProvider)
set_provider(provider_1)
set_provider(provider_1, "foo")
set_provider(provider_2, "bar")
set_provider(provider_2, "baz")
# When
shutdown()
# Then
provider_1.shutdown.assert_called_once()
provider_2.shutdown.assert_called_once()
def test_clear_providers_shutdowns_every_provider_and_resets_default_provider():
# Given
provider_1 = MagicMock(spec=FeatureProvider)
provider_2 = MagicMock(spec=FeatureProvider)
set_provider(provider_1)
set_provider(provider_2, "foo")
set_provider(provider_2, "bar")
# When
clear_providers()
# Then
provider_1.shutdown.assert_called_once()
provider_2.shutdown.assert_called_once()
assert isinstance(get_client().provider, NoOpProvider)
def test_provider_events():
# Given
spy = MagicMock()
provider = NoOpProvider()
set_provider(provider)
add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready)
add_handler(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed
)
add_handler(ProviderEvent.PROVIDER_ERROR, spy.provider_error)
add_handler(ProviderEvent.PROVIDER_STALE, spy.provider_stale)
provider_details = ProviderEventDetails(message="message")
details = EventDetails.from_provider_event_details(
provider.get_metadata().name, provider_details
)
# When
provider.emit_provider_configuration_changed(provider_details)
provider.emit_provider_error(provider_details)
provider.emit_provider_stale(provider_details)
# Then
# NOTE: provider_ready is called immediately after adding the handler
spy.provider_ready.assert_called_once()
spy.provider_configuration_changed.assert_called_once_with(details)
spy.provider_error.assert_called_once_with(details)
spy.provider_stale.assert_called_once_with(details)
def test_add_remove_event_handler():
# Given
provider = NoOpProvider()
set_provider(provider)
spy = MagicMock()
add_handler(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed
)
remove_handler(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed
)
provider_details = ProviderEventDetails(message="message")
# When
provider.emit_provider_configuration_changed(provider_details)
# Then
spy.provider_configuration_changed.assert_not_called()
# Requirement 5.3.3
def test_handlers_attached_to_provider_already_in_associated_state_should_run_immediately():
# Given
provider = NoOpProvider()
set_provider(provider)
spy = MagicMock()
# When
add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready)
# Then
spy.provider_ready.assert_called_once()
def test_provider_ready_handlers_run_if_provider_initialize_function_terminates_normally():
# Given
provider = NoOpProvider()
spy = MagicMock()
add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready)
spy.reset_mock() # reset the mock to avoid counting the immediate call on subscribe
# When
set_provider(provider)
# Then
spy.provider_ready.assert_called_once()
def test_provider_error_handlers_run_if_provider_initialize_function_terminates_abnormally():
# Given
provider = MagicMock(spec=FeatureProvider)
provider.initialize.side_effect = ProviderFatalError()
spy = MagicMock()
add_handler(ProviderEvent.PROVIDER_ERROR, spy.provider_error)
# When
set_provider(provider)
# Then
spy.provider_error.assert_called_once()
def test_provider_status_is_updated_after_provider_emits_event():
# Given
provider = NoOpProvider()
set_provider(provider)
client = get_client()
# When
provider.emit_provider_error(ProviderEventDetails(error_code=ErrorCode.GENERAL))
# Then
assert client.get_provider_status() == ProviderStatus.ERROR
# When
provider.emit_provider_error(
ProviderEventDetails(error_code=ErrorCode.PROVIDER_FATAL)
)
# Then
assert client.get_provider_status() == ProviderStatus.FATAL
# When
provider.emit_provider_stale(ProviderEventDetails())
# Then
assert client.get_provider_status() == ProviderStatus.STALE
# When
provider.emit_provider_ready(ProviderEventDetails())
# Then
assert client.get_provider_status() == ProviderStatus.READY

View File

@ -1,22 +1,36 @@
import inspect
import time
import uuid
from concurrent.futures import ThreadPoolExecutor
from unittest.mock import MagicMock
import pytest
from open_feature.api import add_hooks, clear_hooks
from open_feature.client import OpenFeatureClient
from open_feature.exception import ErrorCode, OpenFeatureError
from open_feature.flag_evaluation import Reason
from open_feature.hook import Hook
from open_feature.provider.no_op_provider import NoOpProvider
from openfeature import api
from openfeature.api import add_hooks, clear_hooks, get_client, set_provider
from openfeature.client import OpenFeatureClient, _typecheck_flag_value
from openfeature.evaluation_context import EvaluationContext
from openfeature.event import EventDetails, ProviderEvent, ProviderEventDetails
from openfeature.exception import ErrorCode, OpenFeatureError
from openfeature.flag_evaluation import FlagResolutionDetails, FlagType, Reason
from openfeature.hook import Hook
from openfeature.provider import FeatureProvider, ProviderStatus
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
from openfeature.provider.no_op_provider import NoOpProvider
from openfeature.transaction_context import ContextVarsTransactionContextPropagator
@pytest.mark.parametrize(
"flag_type, default_value, get_method",
(
(bool, True, "get_boolean_value"),
(bool, True, "get_boolean_value_async"),
(str, "String", "get_string_value"),
(str, "String", "get_string_value_async"),
(int, 100, "get_integer_value"),
(int, 100, "get_integer_value_async"),
(float, 10.23, "get_float_value"),
(float, 10.23, "get_float_value_async"),
(
dict,
{
@ -26,21 +40,38 @@ from open_feature.provider.no_op_provider import NoOpProvider
},
"get_object_value",
),
(
dict,
{
"String": "string",
"Number": 2,
"Boolean": True,
},
"get_object_value_async",
),
(
list,
["string1", "string2"],
"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
):
# Given
# When
flag = getattr(no_op_provider_client, get_method)(
flag_key="Key", default_value=default_value
)
method = getattr(no_op_provider_client, get_method)
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
assert flag is not None
assert flag == default_value
@ -51,9 +82,13 @@ def test_should_get_flag_value_based_on_method_type(
"flag_type, default_value, get_method",
(
(bool, True, "get_boolean_details"),
(bool, True, "get_boolean_details_async"),
(str, "String", "get_string_details"),
(str, "String", "get_string_details_async"),
(int, 100, "get_integer_details"),
(int, 100, "get_integer_details_async"),
(float, 10.23, "get_float_details"),
(float, 10.23, "get_float_details_async"),
(
dict,
{
@ -63,38 +98,85 @@ def test_should_get_flag_value_based_on_method_type(
},
"get_object_details",
),
(
dict,
{
"String": "string",
"Number": 2,
"Boolean": True,
},
"get_object_details_async",
),
(
list,
["string1", "string2"],
"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
):
# Given
# When
flag = getattr(no_op_provider_client, get_method)(
flag_key="Key", default_value=default_value
)
method = getattr(no_op_provider_client, get_method)
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
assert flag is not None
assert flag.value == default_value
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
# 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
)
# Then
assert flag.value
assert flag.error_message == "Unknown flag type"
assert flag.error_code == ErrorCode.GENERAL
assert flag.reason == Reason.ERROR
for flag in [flag_sync, flag_async]:
assert flag.value
assert flag.error_message == "Unknown flag type"
assert flag.error_code == ErrorCode.GENERAL
assert flag.reason == Reason.ERROR
def test_should_pass_flag_metadata_from_resolution_to_evaluation_details():
# Given
provider = InMemoryProvider(
{
"Key": InMemoryFlag(
"true",
{"true": True, "false": False},
flag_metadata={"foo": "bar"},
)
}
)
set_provider(provider, "my-client")
client = OpenFeatureClient("my-client", None)
# When
details = client.get_boolean_details(flag_key="Key", default_value=False)
# Then
assert details is not None
assert details.flag_metadata == {"foo": "bar"}
def test_should_handle_a_generic_exception_thrown_by_a_provider(no_op_provider_client):
@ -136,14 +218,14 @@ def test_should_handle_an_open_feature_exception_thrown_by_a_provider(
assert flag_details.error_message == "error_message"
def test_should_return_client_metadata_with_name():
def test_should_return_client_metadata_with_domain():
# Given
client = OpenFeatureClient("my-client", None, NoOpProvider())
# When
metadata = client.get_metadata()
# Then
assert metadata is not None
assert metadata.name == "my-client"
assert metadata.domain == "my-client"
def test_should_call_api_level_hooks(no_op_provider_client):
@ -158,3 +240,320 @@ def test_should_call_api_level_hooks(no_op_provider_client):
# Then
api_hook.before.assert_called_once()
api_hook.after.assert_called_once()
# Requirement 1.7.5
def test_should_define_a_provider_status_accessor(no_op_provider_client):
# When
status = no_op_provider_client.get_provider_status()
# Then
assert status is not None
assert status == ProviderStatus.READY
# Requirement 1.7.6
@pytest.mark.asyncio
async def test_should_shortcircuit_if_provider_is_not_ready(
no_op_provider_client, monkeypatch
):
# Given
monkeypatch.setattr(
no_op_provider_client, "get_provider_status", lambda: ProviderStatus.NOT_READY
)
spy_hook = MagicMock(spec=Hook)
no_op_provider_client.add_hooks([spy_hook])
# When
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
)
# 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_NOT_READY
spy_hook.error.assert_called_once()
spy_hook.finally_after.assert_called_once()
# Requirement 1.7.7
@pytest.mark.asyncio
async def test_should_shortcircuit_if_provider_is_in_irrecoverable_error_state(
no_op_provider_client, monkeypatch
):
# Given
monkeypatch.setattr(
no_op_provider_client, "get_provider_status", lambda: ProviderStatus.FATAL
)
spy_hook = MagicMock(spec=Hook)
no_op_provider_client.add_hooks([spy_hook])
# When
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
)
# 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()
spy_hook.finally_after.assert_called_once()
@pytest.mark.asyncio
async def test_should_run_error_hooks_if_provider_returns_resolution_with_error_code():
# Given
spy_hook = MagicMock(spec=Hook)
provider = MagicMock(spec=FeatureProvider)
provider.get_provider_hooks.return_value = []
mock_resolution = FlagResolutionDetails(
value=True,
reason=Reason.ERROR,
error_code=ErrorCode.PROVIDER_FATAL,
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)
client = get_client()
client.add_hooks([spy_hook])
# When
flag_details_sync = client.get_boolean_details(flag_key="Key", default_value=True)
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():
# Given
provider = NoOpProvider()
set_provider(provider)
other_provider = NoOpProvider()
set_provider(other_provider, "my-domain")
provider_details = ProviderEventDetails(message="message")
details = EventDetails.from_provider_event_details(
provider.get_metadata().name, provider_details
)
def emit_all_events(provider):
provider.emit_provider_configuration_changed(provider_details)
provider.emit_provider_error(provider_details)
provider.emit_provider_stale(provider_details)
spy = MagicMock()
client = get_client()
client.add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready)
client.add_handler(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed
)
client.add_handler(ProviderEvent.PROVIDER_ERROR, spy.provider_error)
client.add_handler(ProviderEvent.PROVIDER_STALE, spy.provider_stale)
# When
emit_all_events(provider)
emit_all_events(other_provider)
# Then
# NOTE: provider_ready is called immediately after adding the handler
spy.provider_ready.assert_called_once()
spy.provider_configuration_changed.assert_called_once_with(details)
spy.provider_error.assert_called_once_with(details)
spy.provider_stale.assert_called_once_with(details)
def test_add_remove_event_handler():
# Given
provider = NoOpProvider()
set_provider(provider)
spy = MagicMock()
client = get_client()
client.add_handler(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed
)
client.remove_handler(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed
)
provider_details = ProviderEventDetails(message="message")
# When
provider.emit_provider_configuration_changed(provider_details)
# Then
spy.provider_configuration_changed.assert_not_called()
# Requirement 5.1.2, Requirement 5.1.3
def test_provider_event_late_binding():
# Given
provider = NoOpProvider()
set_provider(provider, "my-domain")
other_provider = NoOpProvider()
spy = MagicMock()
client = get_client("my-domain")
client.add_handler(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed
)
set_provider(other_provider, "my-domain")
provider_details = ProviderEventDetails(message="message from provider")
other_provider_details = ProviderEventDetails(message="message from other provider")
details = EventDetails.from_provider_event_details(
other_provider.get_metadata().name, other_provider_details
)
# When
provider.emit_provider_configuration_changed(provider_details)
other_provider.emit_provider_configuration_changed(other_provider_details)
# Then
spy.provider_configuration_changed.assert_called_once_with(details)
def test_client_handlers_thread_safety():
provider = NoOpProvider()
set_provider(provider)
def add_handlers_task():
def handler(*args, **kwargs):
time.sleep(0.005)
for _ in range(10):
time.sleep(0.01)
client = get_client(str(uuid.uuid4()))
client.add_handler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler)
def emit_events_task():
for _ in range(10):
time.sleep(0.01)
provider.emit_provider_configuration_changed(ProviderEventDetails())
with ThreadPoolExecutor(max_workers=2) as executor:
f1 = executor.submit(add_handlers_task)
f2 = executor.submit(emit_events_task)
f1.result()
f2.result()
def test_client_should_merge_contexts():
api.clear_hooks()
api.set_transaction_context_propagator(ContextVarsTransactionContextPropagator())
provider = NoOpProvider()
provider.resolve_boolean_details = MagicMock(wraps=provider.resolve_boolean_details)
api.set_provider(provider)
# Global evaluation context
global_context = EvaluationContext(
targeting_key="global", attributes={"global_attr": "global_value"}
)
api.set_evaluation_context(global_context)
# Transaction context
transaction_context = EvaluationContext(
targeting_key="transaction",
attributes={"transaction_attr": "transaction_value"},
)
api.set_transaction_context(transaction_context)
# Client-specific context
client_context = EvaluationContext(
targeting_key="client", attributes={"client_attr": "client_value"}
)
client = OpenFeatureClient(domain=None, version=None, context=client_context)
# Invocation-specific context
invocation_context = EvaluationContext(
targeting_key="invocation", attributes={"invocation_attr": "invocation_value"}
)
flag_input = "flag"
flag_default = False
client.get_boolean_details(flag_input, flag_default, invocation_context)
# Retrieve the call arguments
args, kwargs = provider.resolve_boolean_details.call_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.attributes["global_attr"] == "global_value"
assert context.attributes["transaction_attr"] == "transaction_value"
assert context.attributes["client_attr"] == "client_value"
assert context.attributes["invocation_attr"] == "invocation_value"

View File

@ -1,12 +1,13 @@
from open_feature.exception import ErrorCode
from open_feature.flag_evaluation import FlagEvaluationDetails, Reason
from openfeature.exception import ErrorCode
from openfeature.flag_evaluation import FlagEvaluationDetails, Reason
def test_evaulation_details_reason_should_be_a_string():
def test_evaluation_details_reason_should_be_a_string():
# Given
flag_key = "my-flag"
flag_value = 100
variant = "1-hundred"
flag_metadata = {}
reason = Reason.DEFAULT
error_code = ErrorCode.GENERAL
error_message = "message"
@ -16,6 +17,7 @@ def test_evaulation_details_reason_should_be_a_string():
flag_key,
flag_value,
variant,
flag_metadata,
reason,
error_code,
error_message,
@ -30,7 +32,7 @@ def test_evaulation_details_reason_should_be_a_string():
assert reason == flag_details.reason
def test_evaulation_details_reason_should_be_a_string_when_set():
def test_evaluation_details_reason_should_be_a_string_when_set():
# Given
flag_key = "my-flag"
flag_value = 100
@ -51,4 +53,4 @@ def test_evaulation_details_reason_should_be_a_string_when_set():
flag_details.reason = Reason.STATIC
# Then
assert Reason.STATIC == flag_details.reason
assert Reason.STATIC == flag_details.reason # noqa: SIM300

View File

@ -0,0 +1,175 @@
import asyncio
import threading
from unittest.mock import MagicMock
import pytest
from openfeature.api import (
get_transaction_context,
set_transaction_context,
set_transaction_context_propagator,
)
from openfeature.evaluation_context import EvaluationContext
from openfeature.transaction_context import (
ContextVarsTransactionContextPropagator,
TransactionContextPropagator,
)
from openfeature.transaction_context.no_op_transaction_context_propagator import (
NoOpTransactionContextPropagator,
)
# Test cases
def test_should_return_default_evaluation_context_with_noop_propagator():
# Given
set_transaction_context_propagator(NoOpTransactionContextPropagator())
# When
context = get_transaction_context()
# Then
assert isinstance(context, EvaluationContext)
assert context.attributes == {}
def test_should_set_and_get_custom_transaction_context():
# Given
set_transaction_context_propagator(ContextVarsTransactionContextPropagator())
evaluation_context = EvaluationContext("custom_key", {"attr1": "val1"})
# When
set_transaction_context(evaluation_context)
# Then
context = get_transaction_context()
assert context.targeting_key == "custom_key"
assert context.attributes == {"attr1": "val1"}
def test_should_override_propagator_and_reset_context():
# Given
custom_propagator = MagicMock(spec=TransactionContextPropagator)
default_context = EvaluationContext()
set_transaction_context_propagator(custom_propagator)
# When
set_transaction_context_propagator(NoOpTransactionContextPropagator())
# Then
assert get_transaction_context() == default_context
def test_should_call_set_transaction_context_on_propagator():
# Given
custom_propagator = MagicMock(spec=TransactionContextPropagator)
evaluation_context = EvaluationContext("custom_key", {"attr1": "val1"})
set_transaction_context_propagator(custom_propagator)
# When
set_transaction_context(evaluation_context)
# Then
custom_propagator.set_transaction_context.assert_called_with(evaluation_context)
def test_should_return_default_context_with_noop_propagator_set():
# Given
noop_propagator = NoOpTransactionContextPropagator()
set_transaction_context_propagator(noop_propagator)
# When
context = get_transaction_context()
# Then
assert context == EvaluationContext()
def test_should_propagate_event_when_context_set():
# Given
custom_propagator = ContextVarsTransactionContextPropagator()
set_transaction_context_propagator(custom_propagator)
evaluation_context = EvaluationContext("custom_key", {"attr1": "val1"})
# When
set_transaction_context(evaluation_context)
# Then
assert (
custom_propagator._transaction_context_var.get().targeting_key == "custom_key"
)
assert custom_propagator._transaction_context_var.get().attributes == {
"attr1": "val1"
}
def test_context_vars_transaction_context_propagator_multiple_threads():
# Given
context_var_propagator = ContextVarsTransactionContextPropagator()
set_transaction_context_propagator(context_var_propagator)
number_of_threads = 3
barrier = threading.Barrier(number_of_threads)
def thread_func(context_value, result_list, index):
context = EvaluationContext(
f"context_{context_value}", {"thread": context_value}
)
set_transaction_context(context)
barrier.wait()
result_list[index] = get_transaction_context()
results = [None] * number_of_threads
threads = []
# When
for i in range(3):
thread = threading.Thread(target=thread_func, args=(i, results, i))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
# Then
for i in range(3):
assert results[i].targeting_key == f"context_{i}"
assert results[i].attributes == {"thread": i}
@pytest.mark.asyncio
async def test_context_vars_transaction_context_propagator_asyncio():
# Given
context_var_propagator = ContextVarsTransactionContextPropagator()
set_transaction_context_propagator(context_var_propagator)
number_of_tasks = 3
event = asyncio.Event()
ready_count = 0
async def async_func(context_value, results, index):
nonlocal ready_count
context = EvaluationContext(
f"context_{context_value}", {"async": context_value}
)
set_transaction_context(context)
ready_count += 1 # Increment the ready count
if ready_count == number_of_tasks:
event.set() # Set the event when all tasks are ready
await event.wait() # Wait for the event to be set
results[index] = get_transaction_context()
# Placeholder for results
results = [None] * number_of_tasks
# When
tasks = [async_func(i, results, i) for i in range(number_of_tasks)]
await asyncio.gather(*tasks)
# Then
for i in range(number_of_tasks):
assert results[i].targeting_key == f"context_{i}"
assert results[i].attributes == {"async": i}

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" },
]