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.
@@ -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))
@@ -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,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())