OpenFeature Python Provider
Introduction
The OpenFeature Python Provider is intended to be used in combination with the OpenFeature Python SDK. The provider wraps the ConfigDirector Python SDK.
The minimum Python version supported is 3.10.
The provider is thread safe. Register one instance per process when your application starts, and shut OpenFeature down when it stops. Evaluations read config state the provider already holds in memory, so they make no network calls on the request path.
Installation
The provider can be installed from PyPI: https://pypi.org/project/configdirector-openfeature-server-provider/
The OpenFeature Python SDK (openfeature-sdk) and the ConfigDirector Python SDK are included as dependencies.
pip install configdirector-openfeature-server-provider
Configure and initialize the client
- Create an instance of the provider using your server SDK key. You can retrieve a server SDK key for each environment under
SDK keysin the dashboard's navigation panel. - Set the OpenFeature provider.
- Get a client instance from OpenFeature.
import logging
from configdirector_openfeature import ConfigDirectorProvider
from openfeature import api
from openfeature.exception import ProviderNotReadyError
try:
api.set_provider_and_wait(ConfigDirectorProvider("YOUR-SERVER-SDK-KEY"))
except ProviderNotReadyError as error:
logging.warning("Starting on in-code default values until ConfigDirector sends config: %s", error)
client = api.get_client()
When no config arrives in time
set_provider_and_wait raises ProviderNotReadyError when no config arrives from ConfigDirector within the connection timeout, which is 3 seconds unless you set the connection's timeout, or when ConfigDirector rejects the server SDK key. The provider then reports the ERROR status, with a PROVIDER_ERROR event, and after a timeout it keeps connecting in the background. Until config arrives, flags resolve to their in-code default values with the PROVIDER_NOT_READY error code. Once it arrives, the provider reports READY, with one PROVIDER_READY event.
The example above catches the error, logs it and starts on in-code default values. To start without waiting for ConfigDirector at all, set the provider with set_provider instead:
api.set_provider(ConfigDirectorProvider("YOUR-SERVER-SDK-KEY"))
Before version 1.7.0 of the provider, set_provider_and_wait returned after the timeout, and the provider reported READY while it served in-code default values.
Additional configuration options
Additional configuration options can be passed into the provider as keyword arguments of the constructor.
For example, the metadata can be provided like this:
from configdirector import Metadata
from configdirector_openfeature import ConfigDirectorProvider
provider = ConfigDirectorProvider(
"YOUR-SERVER-SDK-KEY",
metadata=Metadata(app_name="YOUR-APP-NAME", app_version="1.0.2"),
)
The provider accepts the same metadata, connection, logger, log_level, and telemetry options as the Python SDK client, refer to the additional configuration options section of the Python SDK for a full list.
That includes running without a connection: created with ConfigDirectorProvider(connection=ConnectionOptions(mode="offline", ...)) and no server SDK key, the provider serves the values you give it without connecting to ConfigDirector, and set_provider_and_wait raises ConfigDirectorInitializationError if those values cannot be loaded.
Shut down
Shutting OpenFeature down closes the provider, which closes its connections and reports any pending telemetry:
import atexit
from openfeature import api
atexit.register(api.shutdown)
Retrieve config values
To retrieve config values, use the OpenFeature client:
boolean_value = client.get_boolean_value("my-config-key", False)
string_value = client.get_string_value("my-string-config-key", "Default")
Each OpenFeature getter maps to a ConfigDirector config type:
| OpenFeature getter | ConfigDirector config value |
|---|---|
get_boolean_value | Boolean |
get_string_value | String or enum |
get_integer_value, get_float_value | Number |
get_object_value | JSON object or JSON array |
get_object_value returns the parsed JSON, a dict for a JSON object or a list for a JSON array:
settings = client.get_object_value("my-json-config-key", {})
theme = settings.get("theme")
For additional information regarding the OpenFeature client refer to the OpenFeature Python SDK documentation.
Evaluation details
The detailed getters of the OpenFeature client, such as get_boolean_details, report why an evaluation produced the value that it did:
| Outcome | Reason | Error code |
|---|---|---|
| A value was found | TARGETING_MATCH | |
| The config carries no value | DEFAULT | |
| The config key is unknown | ERROR | FLAG_NOT_FOUND |
| No config state has arrived yet | ERROR | PROVIDER_NOT_READY |
| The value does not match the requested type | ERROR | TYPE_MISMATCH |
When a value was found, the variant is ConfigDirector's identifier for that value. In every other case the in-code default value is returned.
Context
The context can be provided as the third argument to value getter functions of the OpenFeature client. The OpenFeature Python provider evaluates targeting rules locally without additional network calls for different contexts.
from openfeature.evaluation_context import EvaluationContext
context = EvaluationContext(
"12345", # In OpenFeature, the targeting key represents the context's identifier
{
"name": "Example User",
# Any arbitrary traits which can be referenced in targeting rules
"traits": {"region": "North America"},
},
)
boolean_value = client.get_boolean_value("my-config-key", False, context)
To evaluate flags for an entity other than a user, such as a tenant, set the context kind with a top-level kind attribute:
tenant = EvaluationContext("acme", {"kind": "tenant", "name": "Acme Inc.", "traits": {"plan": "enterprise"}})
boolean_value = client.get_boolean_value("my-config-key", False, tenant)
Leaving kind out means user. A kind inside traits is an ordinary trait, not the context kind. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and flags still resolve successfully, as if no context had been given. Context kinds are available from version 1.5.0 of the provider.
The evaluation context maps onto the ConfigDirector context as follows:
| OpenFeature evaluation context | ConfigDirector context |
|---|---|
The targeting key, or otherwise an id attribute | id |
The top-level kind attribute | kind |
The name attribute | name |
The traits mapping attribute | traits |
The boolean anonymous attribute | anonymous |
For additional information regarding the OpenFeature client refer to the OpenFeature Python SDK documentation.
Events
The provider emits PROVIDER_CONFIGURATION_CHANGED whenever configs are updated on the dashboard or via the admin API, carrying the keys of the configs in the update:
from openfeature import api
from openfeature.event import EventDetails, ProviderEvent
def on_configuration_changed(details: EventDetails) -> None:
print(f"Configs updated: {details.flags_changed}")
api.add_handler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, on_configuration_changed)
When no config arrived in time at startup, the provider emits PROVIDER_READY once config arrives, as described in When no config arrives in time.
Test your code
Code that reads flags through OpenFeature is tested by swapping the provider, so it never has to change for a test and nothing from ConfigDirector is involved. The OpenFeature Python SDK ships an in-memory provider for that, InMemoryProvider, which serves the values your test sets.
Each flag is an InMemoryFlag with the name of the variant it resolves to and its named variants:
import pytest
from openfeature import api
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
@pytest.fixture(autouse=True)
def shut_openfeature_down():
yield
api.shutdown()
def test_shows_the_new_checkout():
api.set_provider_and_wait(
InMemoryProvider(
{
"new-checkout": InMemoryFlag("on", {"on": True, "off": False}),
"max-items": InMemoryFlag("default", {"default": 20}),
}
)
)
client = api.get_client()
assert client.get_boolean_value("new-checkout", False) is True
assert client.get_integer_value("max-items", 0) == 20
The provider copies the flags it is given and has no method to change them, so a test that changes a value mid-way sets a new provider with set_provider_and_wait. That emits no configuration-changed event; a test of code that reacts to one asks the current provider to emit it:
from openfeature.event import ProviderEventDetails
provider = InMemoryProvider({"new-checkout": InMemoryFlag("off", {"on": True, "off": False})})
api.set_provider_and_wait(provider)
provider.emit_provider_configuration_changed(ProviderEventDetails(flags_changed=["new-checkout"]))
set_provider_and_wait requires openfeature-sdk 0.10 or newer; on older versions api.set_provider waits for the provider to initialize. The OpenFeature API is one global, so shut it down after each test with api.shutdown(), as the fixture above does, which lets the next test register its own provider. A flag can also carry a context_evaluator, a function from the flag and the evaluation context to the resolution details, for a test of code that targets specific users.