configdirector-openfeature-server-provider 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- configdirector_openfeature_server_provider-1.0.0/.gitignore +30 -0
- configdirector_openfeature_server_provider-1.0.0/LICENSE +21 -0
- configdirector_openfeature_server_provider-1.0.0/PKG-INFO +113 -0
- configdirector_openfeature_server_provider-1.0.0/README.md +86 -0
- configdirector_openfeature_server_provider-1.0.0/pyproject.toml +101 -0
- configdirector_openfeature_server_provider-1.0.0/src/configdirector_openfeature/__init__.py +19 -0
- configdirector_openfeature_server_provider-1.0.0/src/configdirector_openfeature/_context.py +45 -0
- configdirector_openfeature_server_provider-1.0.0/src/configdirector_openfeature/_resolution.py +44 -0
- configdirector_openfeature_server_provider-1.0.0/src/configdirector_openfeature/_values.py +19 -0
- configdirector_openfeature_server_provider-1.0.0/src/configdirector_openfeature/_version.py +1 -0
- configdirector_openfeature_server_provider-1.0.0/src/configdirector_openfeature/provider.py +190 -0
- configdirector_openfeature_server_provider-1.0.0/src/configdirector_openfeature/py.typed +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.eggs/
|
|
5
|
+
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
.coverage.*
|
|
17
|
+
coverage.xml
|
|
18
|
+
htmlcov/
|
|
19
|
+
|
|
20
|
+
# Sample apps and the profiling harness resolve the SDK from a local path, so their locks are
|
|
21
|
+
# not portable.
|
|
22
|
+
samples/*/*/uv.lock
|
|
23
|
+
profiling/uv.lock
|
|
24
|
+
.env
|
|
25
|
+
|
|
26
|
+
.DS_Store
|
|
27
|
+
.idea/
|
|
28
|
+
*.swp
|
|
29
|
+
.claude
|
|
30
|
+
.vscode
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ConfigDirector
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is furnished
|
|
10
|
+
to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: configdirector-openfeature-server-provider
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: OpenFeature server provider for ConfigDirector. ConfigDirector is a remote configuration and feature flag service.
|
|
5
|
+
Project-URL: Homepage, https://www.configdirector.com
|
|
6
|
+
Project-URL: Documentation, https://docs.configdirector.com/sdks/openfeature/python
|
|
7
|
+
Project-URL: Repository, https://github.com/ConfigDirector/python-server-sdk
|
|
8
|
+
Project-URL: Changelog, https://github.com/ConfigDirector/python-server-sdk/blob/main/configdirector-openfeature-server-provider/CHANGELOG.md
|
|
9
|
+
Project-URL: Support, https://www.configdirector.com/support
|
|
10
|
+
Author: ConfigDirector
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: config,configdirector,configuration,feature flag,feature switch,feature toggle,openfeature,openfeature provider,openfeature server provider,provider,remote config,remote configuration,server
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: configdirector-server-sdk<2,>=1.2
|
|
25
|
+
Requires-Dist: openfeature-sdk<1,>=0.8.2
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# ConfigDirector OpenFeature Python Server Provider
|
|
29
|
+
|
|
30
|
+
[![CI][ci-badge]][ci] [![PyPI][pypi-badge]][pypi]
|
|
31
|
+
|
|
32
|
+
[OpenFeature](https://openfeature.dev) Python server provider for [ConfigDirector](https://www.configdirector.com), remote config and feature flags with typed values, JSON Schema validation, and safe renames of live flags. It wraps the [ConfigDirector Python server SDK](https://pypi.org/project/configdirector-server-sdk/) and requires Python 3.10 or newer.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install configdirector-openfeature-server-provider
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The OpenFeature Python SDK (`openfeature-sdk`) and the ConfigDirector Python server SDK come with it as dependencies.
|
|
41
|
+
|
|
42
|
+
## Retrieve a value
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from configdirector_openfeature import ConfigDirectorProvider
|
|
46
|
+
from openfeature import api
|
|
47
|
+
|
|
48
|
+
# The server SDK key is a secret. Do not commit it to your source code.
|
|
49
|
+
api.set_provider_and_wait(ConfigDirectorProvider("YOUR-SERVER-SDK-KEY"))
|
|
50
|
+
client = api.get_client()
|
|
51
|
+
|
|
52
|
+
new_checkout = client.get_boolean_value("new-checkout", False)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`set_provider_and_wait` needs `openfeature-sdk` 0.10 or newer. On an older version use `api.set_provider`, which waits for the provider to initialize there.
|
|
56
|
+
|
|
57
|
+
The provider accepts the same `metadata`, `connection`, `logger`, `log_level`, and `telemetry` options as the server SDK's `create_client`.
|
|
58
|
+
|
|
59
|
+
## Evaluation context
|
|
60
|
+
|
|
61
|
+
The OpenFeature evaluation context maps onto the ConfigDirector context used by targeting rules:
|
|
62
|
+
|
|
63
|
+
| OpenFeature | ConfigDirector |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `targeting_key`, or failing that an `id` attribute | `id` |
|
|
66
|
+
| `name` attribute | `name` |
|
|
67
|
+
| `traits` attribute, a mapping | `traits` |
|
|
68
|
+
| `anonymous` attribute, a boolean | `anonymous` |
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
72
|
+
|
|
73
|
+
context = EvaluationContext("user-123", {"name": "Ada", "traits": {"plan": "pro"}})
|
|
74
|
+
new_checkout = client.get_boolean_value("new-checkout", False, context)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Evaluation details
|
|
78
|
+
|
|
79
|
+
| Outcome | `reason` | `error_code` |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| A value was evaluated | `TARGETING_MATCH`, with the value's id as the `variant` | |
|
|
82
|
+
| The config has no value | `DEFAULT` | |
|
|
83
|
+
| No config has that key | `ERROR` | `FLAG_NOT_FOUND` |
|
|
84
|
+
| The value does not match the requested type | `ERROR` | `TYPE_MISMATCH` |
|
|
85
|
+
| No config state has been received yet | `ERROR` | `PROVIDER_NOT_READY` |
|
|
86
|
+
|
|
87
|
+
If the first config state does not arrive within the configured timeout, initialization still completes and evaluations return their defaults. The provider keeps connecting, and emits `PROVIDER_READY` once config state arrives. It emits `PROVIDER_CONFIGURATION_CHANGED`, with the keys that changed, whenever configs are updated.
|
|
88
|
+
|
|
89
|
+
## Documentation
|
|
90
|
+
|
|
91
|
+
Refer to the [official documentation for the OpenFeature Python provider](https://docs.configdirector.com/sdks/openfeature/python).
|
|
92
|
+
|
|
93
|
+
There is also [a quickstart guide for ConfigDirector and any of our SDKs](https://docs.configdirector.com/getting-started/quickstart).
|
|
94
|
+
|
|
95
|
+
## Sample apps
|
|
96
|
+
|
|
97
|
+
[`samples/configdirector-openfeature-server-provider/`](https://github.com/ConfigDirector/python-server-sdk/tree/main/samples/configdirector-openfeature-server-provider) holds small, runnable applications built on this provider. Start with [`flask`](https://github.com/ConfigDirector/python-server-sdk/tree/main/samples/configdirector-openfeature-server-provider/flask):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
cd samples/configdirector-openfeature-server-provider/flask
|
|
101
|
+
uv run flask --app app run --port 3600
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Getting Help
|
|
105
|
+
|
|
106
|
+
- [Ask a question in Discussions](https://github.com/orgs/ConfigDirector/discussions)
|
|
107
|
+
- [Contact support](https://www.configdirector.com/support)
|
|
108
|
+
|
|
109
|
+
[//]: # "links"
|
|
110
|
+
[ci-badge]: https://github.com/ConfigDirector/python-server-sdk/actions/workflows/ci.yml/badge.svg
|
|
111
|
+
[ci]: https://github.com/ConfigDirector/python-server-sdk/actions/workflows/ci.yml
|
|
112
|
+
[pypi-badge]: https://img.shields.io/pypi/v/configdirector-openfeature-server-provider
|
|
113
|
+
[pypi]: https://pypi.org/project/configdirector-openfeature-server-provider/
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# ConfigDirector OpenFeature Python Server Provider
|
|
2
|
+
|
|
3
|
+
[![CI][ci-badge]][ci] [![PyPI][pypi-badge]][pypi]
|
|
4
|
+
|
|
5
|
+
[OpenFeature](https://openfeature.dev) Python server provider for [ConfigDirector](https://www.configdirector.com), remote config and feature flags with typed values, JSON Schema validation, and safe renames of live flags. It wraps the [ConfigDirector Python server SDK](https://pypi.org/project/configdirector-server-sdk/) and requires Python 3.10 or newer.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install configdirector-openfeature-server-provider
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The OpenFeature Python SDK (`openfeature-sdk`) and the ConfigDirector Python server SDK come with it as dependencies.
|
|
14
|
+
|
|
15
|
+
## Retrieve a value
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from configdirector_openfeature import ConfigDirectorProvider
|
|
19
|
+
from openfeature import api
|
|
20
|
+
|
|
21
|
+
# The server SDK key is a secret. Do not commit it to your source code.
|
|
22
|
+
api.set_provider_and_wait(ConfigDirectorProvider("YOUR-SERVER-SDK-KEY"))
|
|
23
|
+
client = api.get_client()
|
|
24
|
+
|
|
25
|
+
new_checkout = client.get_boolean_value("new-checkout", False)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`set_provider_and_wait` needs `openfeature-sdk` 0.10 or newer. On an older version use `api.set_provider`, which waits for the provider to initialize there.
|
|
29
|
+
|
|
30
|
+
The provider accepts the same `metadata`, `connection`, `logger`, `log_level`, and `telemetry` options as the server SDK's `create_client`.
|
|
31
|
+
|
|
32
|
+
## Evaluation context
|
|
33
|
+
|
|
34
|
+
The OpenFeature evaluation context maps onto the ConfigDirector context used by targeting rules:
|
|
35
|
+
|
|
36
|
+
| OpenFeature | ConfigDirector |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `targeting_key`, or failing that an `id` attribute | `id` |
|
|
39
|
+
| `name` attribute | `name` |
|
|
40
|
+
| `traits` attribute, a mapping | `traits` |
|
|
41
|
+
| `anonymous` attribute, a boolean | `anonymous` |
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
45
|
+
|
|
46
|
+
context = EvaluationContext("user-123", {"name": "Ada", "traits": {"plan": "pro"}})
|
|
47
|
+
new_checkout = client.get_boolean_value("new-checkout", False, context)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Evaluation details
|
|
51
|
+
|
|
52
|
+
| Outcome | `reason` | `error_code` |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| A value was evaluated | `TARGETING_MATCH`, with the value's id as the `variant` | |
|
|
55
|
+
| The config has no value | `DEFAULT` | |
|
|
56
|
+
| No config has that key | `ERROR` | `FLAG_NOT_FOUND` |
|
|
57
|
+
| The value does not match the requested type | `ERROR` | `TYPE_MISMATCH` |
|
|
58
|
+
| No config state has been received yet | `ERROR` | `PROVIDER_NOT_READY` |
|
|
59
|
+
|
|
60
|
+
If the first config state does not arrive within the configured timeout, initialization still completes and evaluations return their defaults. The provider keeps connecting, and emits `PROVIDER_READY` once config state arrives. It emits `PROVIDER_CONFIGURATION_CHANGED`, with the keys that changed, whenever configs are updated.
|
|
61
|
+
|
|
62
|
+
## Documentation
|
|
63
|
+
|
|
64
|
+
Refer to the [official documentation for the OpenFeature Python provider](https://docs.configdirector.com/sdks/openfeature/python).
|
|
65
|
+
|
|
66
|
+
There is also [a quickstart guide for ConfigDirector and any of our SDKs](https://docs.configdirector.com/getting-started/quickstart).
|
|
67
|
+
|
|
68
|
+
## Sample apps
|
|
69
|
+
|
|
70
|
+
[`samples/configdirector-openfeature-server-provider/`](https://github.com/ConfigDirector/python-server-sdk/tree/main/samples/configdirector-openfeature-server-provider) holds small, runnable applications built on this provider. Start with [`flask`](https://github.com/ConfigDirector/python-server-sdk/tree/main/samples/configdirector-openfeature-server-provider/flask):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
cd samples/configdirector-openfeature-server-provider/flask
|
|
74
|
+
uv run flask --app app run --port 3600
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Getting Help
|
|
78
|
+
|
|
79
|
+
- [Ask a question in Discussions](https://github.com/orgs/ConfigDirector/discussions)
|
|
80
|
+
- [Contact support](https://www.configdirector.com/support)
|
|
81
|
+
|
|
82
|
+
[//]: # "links"
|
|
83
|
+
[ci-badge]: https://github.com/ConfigDirector/python-server-sdk/actions/workflows/ci.yml/badge.svg
|
|
84
|
+
[ci]: https://github.com/ConfigDirector/python-server-sdk/actions/workflows/ci.yml
|
|
85
|
+
[pypi-badge]: https://img.shields.io/pypi/v/configdirector-openfeature-server-provider
|
|
86
|
+
[pypi]: https://pypi.org/project/configdirector-openfeature-server-provider/
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "configdirector-openfeature-server-provider"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "OpenFeature server provider for ConfigDirector. ConfigDirector is a remote configuration and feature flag service."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "ConfigDirector" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"configdirector",
|
|
16
|
+
"openfeature server provider",
|
|
17
|
+
"openfeature provider",
|
|
18
|
+
"openfeature",
|
|
19
|
+
"server",
|
|
20
|
+
"provider",
|
|
21
|
+
"config",
|
|
22
|
+
"configuration",
|
|
23
|
+
"remote config",
|
|
24
|
+
"remote configuration",
|
|
25
|
+
"feature flag",
|
|
26
|
+
"feature toggle",
|
|
27
|
+
"feature switch",
|
|
28
|
+
]
|
|
29
|
+
classifiers = [
|
|
30
|
+
"Intended Audience :: Developers",
|
|
31
|
+
"Operating System :: OS Independent",
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Programming Language :: Python :: 3.10",
|
|
34
|
+
"Programming Language :: Python :: 3.11",
|
|
35
|
+
"Programming Language :: Python :: 3.12",
|
|
36
|
+
"Programming Language :: Python :: 3.13",
|
|
37
|
+
"Programming Language :: Python :: 3.14",
|
|
38
|
+
"Typing :: Typed",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
dependencies = [
|
|
42
|
+
"configdirector-server-sdk>=1.2,<2",
|
|
43
|
+
"openfeature-sdk>=0.8.2,<1",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://www.configdirector.com"
|
|
48
|
+
Documentation = "https://docs.configdirector.com/sdks/openfeature/python"
|
|
49
|
+
Repository = "https://github.com/ConfigDirector/python-server-sdk"
|
|
50
|
+
Changelog = "https://github.com/ConfigDirector/python-server-sdk/blob/main/configdirector-openfeature-server-provider/CHANGELOG.md"
|
|
51
|
+
Support = "https://www.configdirector.com/support"
|
|
52
|
+
|
|
53
|
+
[dependency-groups]
|
|
54
|
+
dev = [
|
|
55
|
+
"mypy>=1.14",
|
|
56
|
+
"pytest>=8.3",
|
|
57
|
+
"pytest-cov>=6.0",
|
|
58
|
+
"pytest-timeout>=2.3",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.uv.sources]
|
|
62
|
+
configdirector-server-sdk = { workspace = true }
|
|
63
|
+
|
|
64
|
+
[tool.hatch.version]
|
|
65
|
+
path = "src/configdirector_openfeature/_version.py"
|
|
66
|
+
|
|
67
|
+
[tool.hatch.build.targets.wheel]
|
|
68
|
+
packages = ["src/configdirector_openfeature"]
|
|
69
|
+
|
|
70
|
+
[tool.hatch.build.targets.sdist]
|
|
71
|
+
include = ["src/configdirector_openfeature", "README.md", "LICENSE"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
# Inherit the repository's style, but resolve first-party imports against this package so that
|
|
75
|
+
# `configdirector` sorts as the third-party dependency it is here.
|
|
76
|
+
extend = "../pyproject.toml"
|
|
77
|
+
src = ["src", "."]
|
|
78
|
+
|
|
79
|
+
[tool.mypy]
|
|
80
|
+
python_version = "3.10"
|
|
81
|
+
files = ["src", "tests"]
|
|
82
|
+
strict = true
|
|
83
|
+
warn_unreachable = true
|
|
84
|
+
enable_error_code = ["redundant-expr", "possibly-undefined", "truthy-bool"]
|
|
85
|
+
|
|
86
|
+
[tool.pytest.ini_options]
|
|
87
|
+
testpaths = ["tests"]
|
|
88
|
+
addopts = "-ra --strict-markers --strict-config --timeout=60 --timeout-method=thread"
|
|
89
|
+
xfail_strict = true
|
|
90
|
+
|
|
91
|
+
[tool.coverage.run]
|
|
92
|
+
source = ["src/configdirector_openfeature"]
|
|
93
|
+
branch = true
|
|
94
|
+
|
|
95
|
+
[tool.coverage.report]
|
|
96
|
+
exclude_lines = [
|
|
97
|
+
"pragma: no cover",
|
|
98
|
+
"raise NotImplementedError",
|
|
99
|
+
"if TYPE_CHECKING:",
|
|
100
|
+
"\\.\\.\\.",
|
|
101
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""OpenFeature server provider for ConfigDirector.
|
|
2
|
+
|
|
3
|
+
ConfigDirector is a remote configuration and feature flag service.
|
|
4
|
+
|
|
5
|
+
Example::
|
|
6
|
+
|
|
7
|
+
from configdirector_openfeature import ConfigDirectorProvider
|
|
8
|
+
from openfeature import api
|
|
9
|
+
|
|
10
|
+
api.set_provider_and_wait(ConfigDirectorProvider("YOUR-SERVER-SDK-KEY"))
|
|
11
|
+
|
|
12
|
+
if api.get_client().get_boolean_value("new-checkout", False):
|
|
13
|
+
...
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from ._version import __version__
|
|
17
|
+
from .provider import ConfigDirectorProvider
|
|
18
|
+
|
|
19
|
+
__all__ = ["ConfigDirectorProvider", "__version__"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from configdirector import Context
|
|
7
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
8
|
+
|
|
9
|
+
from ._values import to_plain
|
|
10
|
+
|
|
11
|
+
__all__ = ["to_context"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def to_context(evaluation_context: EvaluationContext | None) -> Context | None:
|
|
15
|
+
if evaluation_context is None:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
attributes = evaluation_context.attributes
|
|
19
|
+
traits = attributes.get("traits")
|
|
20
|
+
anonymous = attributes.get("anonymous")
|
|
21
|
+
|
|
22
|
+
return Context(
|
|
23
|
+
id=_identifier(evaluation_context),
|
|
24
|
+
name=_text(attributes.get("name")),
|
|
25
|
+
traits=to_plain(traits) if isinstance(traits, Mapping) and traits else None,
|
|
26
|
+
anonymous=anonymous if isinstance(anonymous, bool) else False,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _identifier(evaluation_context: EvaluationContext) -> str | None:
|
|
31
|
+
if evaluation_context.targeting_key is not None:
|
|
32
|
+
return evaluation_context.targeting_key
|
|
33
|
+
return _text(evaluation_context.attributes.get("id"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _text(value: Any) -> str | None:
|
|
37
|
+
if value is None or isinstance(value, Mapping):
|
|
38
|
+
return None
|
|
39
|
+
if isinstance(value, str):
|
|
40
|
+
return value
|
|
41
|
+
if isinstance(value, Sequence):
|
|
42
|
+
return None
|
|
43
|
+
if isinstance(value, bool):
|
|
44
|
+
return "true" if value else "false"
|
|
45
|
+
return str(to_plain(value))
|
configdirector_openfeature_server_provider-1.0.0/src/configdirector_openfeature/_resolution.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
|
|
5
|
+
from configdirector import ConfigEvaluation
|
|
6
|
+
from openfeature.exception import ErrorCode
|
|
7
|
+
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
|
|
8
|
+
|
|
9
|
+
__all__ = ["to_resolution_details"]
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def to_resolution_details(value: T, evaluation: ConfigEvaluation | None) -> FlagResolutionDetails[T]:
|
|
15
|
+
if evaluation is None:
|
|
16
|
+
return FlagResolutionDetails(value=value)
|
|
17
|
+
|
|
18
|
+
if evaluation.reason == "found-match":
|
|
19
|
+
return FlagResolutionDetails(value=value, variant=evaluation.value_id, reason=Reason.TARGETING_MATCH)
|
|
20
|
+
if evaluation.reason == "value-missing":
|
|
21
|
+
return FlagResolutionDetails(value=value, reason=Reason.DEFAULT)
|
|
22
|
+
if evaluation.reason == "config-state-missing":
|
|
23
|
+
return _error(
|
|
24
|
+
value, ErrorCode.FLAG_NOT_FOUND, f"No config with the key '{evaluation.key}' was found."
|
|
25
|
+
)
|
|
26
|
+
if evaluation.reason == "client-not-ready":
|
|
27
|
+
return _error(
|
|
28
|
+
value,
|
|
29
|
+
ErrorCode.PROVIDER_NOT_READY,
|
|
30
|
+
"The ConfigDirector client has not received any configs yet.",
|
|
31
|
+
)
|
|
32
|
+
if evaluation.is_default:
|
|
33
|
+
return _error(
|
|
34
|
+
value,
|
|
35
|
+
ErrorCode.TYPE_MISMATCH,
|
|
36
|
+
f"The value of '{evaluation.key}' does not match the requested type ({evaluation.reason}).",
|
|
37
|
+
)
|
|
38
|
+
return FlagResolutionDetails(value=value)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _error(value: T, error_code: ErrorCode, message: str) -> FlagResolutionDetails[T]:
|
|
42
|
+
return FlagResolutionDetails(
|
|
43
|
+
value=value, reason=Reason.ERROR, error_code=error_code, error_message=message
|
|
44
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
__all__ = ["to_plain"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def to_plain(value: Any) -> Any:
|
|
11
|
+
if isinstance(value, Mapping):
|
|
12
|
+
return {str(key): to_plain(entry) for key, entry in value.items()}
|
|
13
|
+
if isinstance(value, (str, bytes)):
|
|
14
|
+
return value
|
|
15
|
+
if isinstance(value, Sequence):
|
|
16
|
+
return [to_plain(item) for item in value]
|
|
17
|
+
if isinstance(value, datetime):
|
|
18
|
+
return value.isoformat()
|
|
19
|
+
return value
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
from configdirector import (
|
|
9
|
+
ClientHooks,
|
|
10
|
+
ClientReadyEvent,
|
|
11
|
+
ConfigDirectorLogger,
|
|
12
|
+
ConfigEvaluatedEvent,
|
|
13
|
+
ConfigEvaluation,
|
|
14
|
+
ConfigsUpdatedEvent,
|
|
15
|
+
ConfigValue,
|
|
16
|
+
ConnectionOptions,
|
|
17
|
+
Metadata,
|
|
18
|
+
TelemetryOptions,
|
|
19
|
+
)
|
|
20
|
+
from configdirector._wrappers import Wrapper, create_wrapped_client
|
|
21
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
22
|
+
from openfeature.event import ProviderEventDetails
|
|
23
|
+
from openfeature.flag_evaluation import FlagResolutionDetails, FlagValueType
|
|
24
|
+
from openfeature.provider import AbstractProvider
|
|
25
|
+
from openfeature.provider import Metadata as ProviderMetadata
|
|
26
|
+
|
|
27
|
+
from ._context import to_context
|
|
28
|
+
from ._resolution import to_resolution_details
|
|
29
|
+
from ._values import to_plain
|
|
30
|
+
|
|
31
|
+
__all__ = ["ConfigDirectorProvider"]
|
|
32
|
+
|
|
33
|
+
_NAME = "ConfigDirectorProvider"
|
|
34
|
+
|
|
35
|
+
_ValueT = TypeVar("_ValueT", bound=ConfigValue)
|
|
36
|
+
|
|
37
|
+
_evaluated: ContextVar[ConfigEvaluation | None] = ContextVar("configdirector_evaluated", default=None)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ConfigDirectorProvider(AbstractProvider):
|
|
41
|
+
"""An OpenFeature provider backed by the ConfigDirector Python server SDK.
|
|
42
|
+
|
|
43
|
+
Register it with the OpenFeature API and read values through an OpenFeature client. The
|
|
44
|
+
provider owns a ConfigDirector client: it connects when OpenFeature initializes the provider,
|
|
45
|
+
and closes when OpenFeature shuts it down. Targeting rules are evaluated locally, so an
|
|
46
|
+
evaluation makes no network calls.
|
|
47
|
+
|
|
48
|
+
Example::
|
|
49
|
+
|
|
50
|
+
from configdirector_openfeature import ConfigDirectorProvider
|
|
51
|
+
from openfeature import api
|
|
52
|
+
|
|
53
|
+
api.set_provider_and_wait(ConfigDirectorProvider("YOUR-SERVER-SDK-KEY"))
|
|
54
|
+
|
|
55
|
+
enabled = api.get_client().get_boolean_value("new-checkout", False)
|
|
56
|
+
|
|
57
|
+
The OpenFeature evaluation context maps onto the ConfigDirector
|
|
58
|
+
:class:`~configdirector.Context`: the targeting key, or failing that an ``id`` attribute,
|
|
59
|
+
becomes the context's id, ``name`` its name, a ``traits`` mapping its traits, and a boolean
|
|
60
|
+
``anonymous`` its anonymous flag.
|
|
61
|
+
|
|
62
|
+
If the first config state does not arrive within the configured timeout, initialization
|
|
63
|
+
still completes and evaluations return their defaults with the error code
|
|
64
|
+
``PROVIDER_NOT_READY``. The provider keeps connecting, and emits ``PROVIDER_READY`` once
|
|
65
|
+
config state arrives.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
server_sdk_key: Your ConfigDirector server SDK key. This is a secret value — do not
|
|
69
|
+
commit it to source control.
|
|
70
|
+
metadata: Metadata about your application. Supplying ``app_name`` and ``app_version`` is
|
|
71
|
+
recommended so that they can be referenced from targeting rules.
|
|
72
|
+
connection: Connection options such as mode, timeout, and polling interval.
|
|
73
|
+
logger: Any object implementing :class:`~configdirector.ConfigDirectorLogger`, including
|
|
74
|
+
a standard library :class:`logging.Logger`. Defaults to the standard library logger
|
|
75
|
+
named ``"configdirector"``.
|
|
76
|
+
log_level: A level to set on that default logger, as either a :mod:`logging` constant or
|
|
77
|
+
its name. Ignored when ``logger`` is supplied.
|
|
78
|
+
telemetry: Telemetry queue and flush tuning.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
configdirector.ConfigDirectorValidationError: If ``server_sdk_key`` is missing or empty,
|
|
82
|
+
or if a ``connection`` or ``telemetry`` setting is invalid.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
server_sdk_key: str,
|
|
88
|
+
*,
|
|
89
|
+
metadata: Metadata | None = None,
|
|
90
|
+
connection: ConnectionOptions | None = None,
|
|
91
|
+
logger: ConfigDirectorLogger | None = None,
|
|
92
|
+
log_level: int | str | None = None,
|
|
93
|
+
telemetry: TelemetryOptions | None = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
super().__init__()
|
|
96
|
+
self._lock = threading.Lock()
|
|
97
|
+
self._initialized = False
|
|
98
|
+
self._client = create_wrapped_client(
|
|
99
|
+
Wrapper.OPENFEATURE_SERVER_PROVIDER,
|
|
100
|
+
server_sdk_key,
|
|
101
|
+
metadata=metadata,
|
|
102
|
+
connection=connection,
|
|
103
|
+
logger=logger,
|
|
104
|
+
log_level=log_level,
|
|
105
|
+
telemetry=telemetry,
|
|
106
|
+
hooks=ClientHooks(
|
|
107
|
+
client_ready=self._on_client_ready,
|
|
108
|
+
configs_updated=self._on_configs_updated,
|
|
109
|
+
config_evaluated=self._on_config_evaluated,
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def get_metadata(self) -> ProviderMetadata:
|
|
114
|
+
"""The name this provider is registered under with OpenFeature."""
|
|
115
|
+
return ProviderMetadata(name=_NAME)
|
|
116
|
+
|
|
117
|
+
def initialize(self, evaluation_context: EvaluationContext) -> None:
|
|
118
|
+
"""Connects to ConfigDirector, waiting up to the configured timeout for config state."""
|
|
119
|
+
self._client.initialize()
|
|
120
|
+
with self._lock:
|
|
121
|
+
self._initialized = True
|
|
122
|
+
|
|
123
|
+
def shutdown(self) -> None:
|
|
124
|
+
"""Closes the connection to ConfigDirector and flushes pending telemetry."""
|
|
125
|
+
self._client.close()
|
|
126
|
+
|
|
127
|
+
def resolve_boolean_details(
|
|
128
|
+
self,
|
|
129
|
+
flag_key: str,
|
|
130
|
+
default_value: bool,
|
|
131
|
+
evaluation_context: EvaluationContext | None = None,
|
|
132
|
+
) -> FlagResolutionDetails[bool]:
|
|
133
|
+
"""Evaluates ``flag_key`` as a boolean."""
|
|
134
|
+
return self._resolve(flag_key, default_value, evaluation_context)
|
|
135
|
+
|
|
136
|
+
def resolve_string_details(
|
|
137
|
+
self,
|
|
138
|
+
flag_key: str,
|
|
139
|
+
default_value: str,
|
|
140
|
+
evaluation_context: EvaluationContext | None = None,
|
|
141
|
+
) -> FlagResolutionDetails[str]:
|
|
142
|
+
"""Evaluates ``flag_key`` as a string."""
|
|
143
|
+
return self._resolve(flag_key, default_value, evaluation_context)
|
|
144
|
+
|
|
145
|
+
def resolve_integer_details(
|
|
146
|
+
self,
|
|
147
|
+
flag_key: str,
|
|
148
|
+
default_value: int,
|
|
149
|
+
evaluation_context: EvaluationContext | None = None,
|
|
150
|
+
) -> FlagResolutionDetails[int]:
|
|
151
|
+
"""Evaluates ``flag_key`` as an integer."""
|
|
152
|
+
return self._resolve(flag_key, default_value, evaluation_context)
|
|
153
|
+
|
|
154
|
+
def resolve_float_details(
|
|
155
|
+
self,
|
|
156
|
+
flag_key: str,
|
|
157
|
+
default_value: float,
|
|
158
|
+
evaluation_context: EvaluationContext | None = None,
|
|
159
|
+
) -> FlagResolutionDetails[float]:
|
|
160
|
+
"""Evaluates ``flag_key`` as a float."""
|
|
161
|
+
return self._resolve(flag_key, float(default_value), evaluation_context)
|
|
162
|
+
|
|
163
|
+
def resolve_object_details(
|
|
164
|
+
self,
|
|
165
|
+
flag_key: str,
|
|
166
|
+
default_value: Sequence[FlagValueType] | Mapping[str, FlagValueType],
|
|
167
|
+
evaluation_context: EvaluationContext | None = None,
|
|
168
|
+
) -> FlagResolutionDetails[Sequence[FlagValueType] | Mapping[str, FlagValueType]]:
|
|
169
|
+
"""Evaluates ``flag_key`` as a JSON object or array."""
|
|
170
|
+
default: dict[str, FlagValueType] | list[FlagValueType] = to_plain(default_value)
|
|
171
|
+
return self._resolve(flag_key, default, evaluation_context)
|
|
172
|
+
|
|
173
|
+
def _resolve(
|
|
174
|
+
self, flag_key: str, default_value: _ValueT, evaluation_context: EvaluationContext | None
|
|
175
|
+
) -> FlagResolutionDetails[_ValueT]:
|
|
176
|
+
_evaluated.set(None)
|
|
177
|
+
value = self._client.get_value(flag_key, default_value, to_context(evaluation_context))
|
|
178
|
+
return to_resolution_details(value, _evaluated.get())
|
|
179
|
+
|
|
180
|
+
def _on_config_evaluated(self, event: ConfigEvaluatedEvent) -> None:
|
|
181
|
+
_evaluated.set(event.evaluation)
|
|
182
|
+
|
|
183
|
+
def _on_configs_updated(self, event: ConfigsUpdatedEvent) -> None:
|
|
184
|
+
self.emit_provider_configuration_changed(ProviderEventDetails(flags_changed=list(event.keys)))
|
|
185
|
+
|
|
186
|
+
def _on_client_ready(self, _event: ClientReadyEvent) -> None:
|
|
187
|
+
with self._lock:
|
|
188
|
+
initialized = self._initialized
|
|
189
|
+
if initialized:
|
|
190
|
+
self.emit_provider_ready(ProviderEventDetails())
|
|
File without changes
|