launchdarkly-openfeature-server 0.8.2__tar.gz → 0.9.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.
- {launchdarkly_openfeature_server-0.8.2 → launchdarkly_openfeature_server-0.9.0}/PKG-INFO +7 -5
- {launchdarkly_openfeature_server-0.8.2 → launchdarkly_openfeature_server-0.9.0}/README.md +5 -3
- {launchdarkly_openfeature_server-0.8.2 → launchdarkly_openfeature_server-0.9.0}/ld_openfeature/provider.py +43 -31
- launchdarkly_openfeature_server-0.9.0/ld_openfeature/version.py +1 -0
- {launchdarkly_openfeature_server-0.8.2 → launchdarkly_openfeature_server-0.9.0}/pyproject.toml +3 -3
- launchdarkly_openfeature_server-0.8.2/ld_openfeature/version.py +0 -1
- {launchdarkly_openfeature_server-0.8.2 → launchdarkly_openfeature_server-0.9.0}/LICENSE +0 -0
- {launchdarkly_openfeature_server-0.8.2 → launchdarkly_openfeature_server-0.9.0}/ld_openfeature/__init__.py +0 -0
- {launchdarkly_openfeature_server-0.8.2 → launchdarkly_openfeature_server-0.9.0}/ld_openfeature/impl/context_converter.py +0 -0
- {launchdarkly_openfeature_server-0.8.2 → launchdarkly_openfeature_server-0.9.0}/ld_openfeature/impl/details_converter.py +0 -0
- {launchdarkly_openfeature_server-0.8.2 → launchdarkly_openfeature_server-0.9.0}/ld_openfeature/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: launchdarkly-openfeature-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: An OpenFeature provider for the LaunchDarkly Python server SDK
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.15
|
|
20
20
|
Classifier: Topic :: Software Development
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
-
Requires-Dist: launchdarkly-server-sdk (>=9.
|
|
22
|
+
Requires-Dist: launchdarkly-server-sdk (>=9.18.3,<10)
|
|
23
23
|
Requires-Dist: openfeature-sdk (>=0.9.0,<1)
|
|
24
24
|
Project-URL: Documentation, https://launchdarkly-openfeature-python-sdk.readthedocs.io/en/latest/
|
|
25
25
|
Project-URL: Repository, https://github.com/launchdarkly/openfeature-python-server
|
|
@@ -51,7 +51,7 @@ This provider is designed primarily for use in multi-user systems such as web se
|
|
|
51
51
|
|
|
52
52
|
## Supported Python versions
|
|
53
53
|
|
|
54
|
-
This version of the LaunchDarkly provider works with Python 3.
|
|
54
|
+
This version of the LaunchDarkly provider works with Python 3.10 and above.
|
|
55
55
|
|
|
56
56
|
## Feature matrix
|
|
57
57
|
|
|
@@ -66,7 +66,7 @@ This matrix mirrors the [feature matrix of the OpenFeature SDK for Python](https
|
|
|
66
66
|
| ✅ | Domains | Domains bind clients to providers in the OpenFeature SDK; a separate provider instance may be registered per domain. |
|
|
67
67
|
| ✅ | Eventing | LaunchDarkly data source status changes are emitted as `PROVIDER_READY`, `PROVIDER_STALE` and `PROVIDER_ERROR`, and only when the status changes; flag changes as `PROVIDER_CONFIGURATION_CHANGED` with the changed flag key. |
|
|
68
68
|
| ✅ | Tracking | `track` sends a LaunchDarkly custom event for the evaluation context, with the tracking event value and remaining details attached. |
|
|
69
|
-
| ✅ | Initialization | `initialize` reports whether the LaunchDarkly client became ready. The optional `start_wait` parameter bounds initialization;
|
|
69
|
+
| ✅ | Initialization | `initialize` reports whether the LaunchDarkly client became ready. The optional `start_wait` parameter bounds initialization; `None` waits until the data source becomes valid or permanently fails. Since LaunchDarkly Python SDK 9.18.0 a rejected SDK key is retried rather than failing permanently, so `None` can wait indefinitely. |
|
|
70
70
|
| ✅ | Shutdown | `shutdown` closes the LaunchDarkly client; a closed client cannot be restarted, so a new provider instance is required afterward. |
|
|
71
71
|
| ✅ | Transaction Context Propagation | Provided by the OpenFeature SDK, which merges the transaction context into the evaluation context before the provider is called; no provider support is required. |
|
|
72
72
|
| ✅ | Extending | The underlying LaunchDarkly client is available through the `client` property. |
|
|
@@ -97,7 +97,9 @@ api.set_provider(openfeature_provider)
|
|
|
97
97
|
# Refer to OpenFeature documentation for getting a client and performing evaluations.
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
The optional `start_wait` parameter is the number of seconds to wait for a successful connection to LaunchDarkly, matching the same parameter of the LaunchDarkly SDK's `LDClient`, and defaulting to the same five seconds. A positive value bounds the whole of initialization: the provider constructor blocks for up to that long, and OpenFeature initialization then completes immediately, reporting a failed initialization if the client did not become ready in time. Zero
|
|
100
|
+
The optional `start_wait` parameter is the number of seconds to wait for a successful connection to LaunchDarkly, matching the same parameter of the LaunchDarkly SDK's `LDClient`, and defaulting to the same five seconds. A positive value bounds the whole of initialization: the provider constructor blocks for up to that long, and OpenFeature initialization then completes immediately, reporting a failed initialization if the client did not become ready in time. Zero waits nowhere, so initialization fails unless the client is already ready, and the application learns when the client becomes usable from provider events. `None` requests waiting indefinitely: the constructor does not block, and initialization waits without a deadline for the data source to become valid or to fail permanently. Since LaunchDarkly Python SDK 9.18.0 a rejected SDK key is retried rather than treated as a permanent failure, so with `None`, initialization does not complete while the key is rejected.
|
|
101
|
+
|
|
102
|
+
`api.set_provider` registers the provider without waiting for it to initialize, so the first evaluations may use fallback values while the LaunchDarkly client is still connecting. Use `api.set_provider_and_wait` instead when the application needs registration to block until initialization has succeeded or failed.
|
|
101
103
|
|
|
102
104
|
Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/python) for instructions on getting started with using the SDK.
|
|
103
105
|
|
|
@@ -24,7 +24,7 @@ This provider is designed primarily for use in multi-user systems such as web se
|
|
|
24
24
|
|
|
25
25
|
## Supported Python versions
|
|
26
26
|
|
|
27
|
-
This version of the LaunchDarkly provider works with Python 3.
|
|
27
|
+
This version of the LaunchDarkly provider works with Python 3.10 and above.
|
|
28
28
|
|
|
29
29
|
## Feature matrix
|
|
30
30
|
|
|
@@ -39,7 +39,7 @@ This matrix mirrors the [feature matrix of the OpenFeature SDK for Python](https
|
|
|
39
39
|
| ✅ | Domains | Domains bind clients to providers in the OpenFeature SDK; a separate provider instance may be registered per domain. |
|
|
40
40
|
| ✅ | Eventing | LaunchDarkly data source status changes are emitted as `PROVIDER_READY`, `PROVIDER_STALE` and `PROVIDER_ERROR`, and only when the status changes; flag changes as `PROVIDER_CONFIGURATION_CHANGED` with the changed flag key. |
|
|
41
41
|
| ✅ | Tracking | `track` sends a LaunchDarkly custom event for the evaluation context, with the tracking event value and remaining details attached. |
|
|
42
|
-
| ✅ | Initialization | `initialize` reports whether the LaunchDarkly client became ready. The optional `start_wait` parameter bounds initialization;
|
|
42
|
+
| ✅ | Initialization | `initialize` reports whether the LaunchDarkly client became ready. The optional `start_wait` parameter bounds initialization; `None` waits until the data source becomes valid or permanently fails. Since LaunchDarkly Python SDK 9.18.0 a rejected SDK key is retried rather than failing permanently, so `None` can wait indefinitely. |
|
|
43
43
|
| ✅ | Shutdown | `shutdown` closes the LaunchDarkly client; a closed client cannot be restarted, so a new provider instance is required afterward. |
|
|
44
44
|
| ✅ | Transaction Context Propagation | Provided by the OpenFeature SDK, which merges the transaction context into the evaluation context before the provider is called; no provider support is required. |
|
|
45
45
|
| ✅ | Extending | The underlying LaunchDarkly client is available through the `client` property. |
|
|
@@ -70,7 +70,9 @@ api.set_provider(openfeature_provider)
|
|
|
70
70
|
# Refer to OpenFeature documentation for getting a client and performing evaluations.
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
The optional `start_wait` parameter is the number of seconds to wait for a successful connection to LaunchDarkly, matching the same parameter of the LaunchDarkly SDK's `LDClient`, and defaulting to the same five seconds. A positive value bounds the whole of initialization: the provider constructor blocks for up to that long, and OpenFeature initialization then completes immediately, reporting a failed initialization if the client did not become ready in time. Zero
|
|
73
|
+
The optional `start_wait` parameter is the number of seconds to wait for a successful connection to LaunchDarkly, matching the same parameter of the LaunchDarkly SDK's `LDClient`, and defaulting to the same five seconds. A positive value bounds the whole of initialization: the provider constructor blocks for up to that long, and OpenFeature initialization then completes immediately, reporting a failed initialization if the client did not become ready in time. Zero waits nowhere, so initialization fails unless the client is already ready, and the application learns when the client becomes usable from provider events. `None` requests waiting indefinitely: the constructor does not block, and initialization waits without a deadline for the data source to become valid or to fail permanently. Since LaunchDarkly Python SDK 9.18.0 a rejected SDK key is retried rather than treated as a permanent failure, so with `None`, initialization does not complete while the key is rejected.
|
|
74
|
+
|
|
75
|
+
`api.set_provider` registers the provider without waiting for it to initialize, so the first evaluations may use fallback values while the LaunchDarkly client is still connecting. Use `api.set_provider_and_wait` instead when the application needs registration to block until initialization has succeeded or failed.
|
|
74
76
|
|
|
75
77
|
Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/python) for instructions on getting started with using the SDK.
|
|
76
78
|
|
|
@@ -10,7 +10,7 @@ from openfeature.exception import ErrorCode, ProviderNotReadyError
|
|
|
10
10
|
from openfeature.flag_evaluation import FlagResolutionDetails, FlagType, FlagValueType, Reason
|
|
11
11
|
from openfeature.hook import Hook
|
|
12
12
|
from openfeature.provider.metadata import Metadata
|
|
13
|
-
from openfeature.provider import AbstractProvider
|
|
13
|
+
from openfeature.provider import AbstractProvider, ProviderStatus
|
|
14
14
|
from openfeature.event import ProviderEventDetails
|
|
15
15
|
from openfeature.track import TrackingEventDetails
|
|
16
16
|
|
|
@@ -26,7 +26,7 @@ logger = getLogger("launchdarkly-openfeature-server")
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class LaunchDarklyProvider(AbstractProvider):
|
|
29
|
-
def __init__(self, config: Config, start_wait: float = 5):
|
|
29
|
+
def __init__(self, config: Config, start_wait: Optional[float] = 5):
|
|
30
30
|
"""
|
|
31
31
|
Create a provider backed by a LaunchDarkly client.
|
|
32
32
|
|
|
@@ -34,18 +34,20 @@ class LaunchDarklyProvider(AbstractProvider):
|
|
|
34
34
|
:param start_wait: The number of seconds to wait for a successful connection to LaunchDarkly, matching
|
|
35
35
|
the same parameter of :class:`ldclient.LDClient`. A positive value bounds the whole of initialization:
|
|
36
36
|
this constructor blocks for up to that long, and ``initialize`` then completes immediately, reporting
|
|
37
|
-
a failed initialization if the client did not become ready in time. Zero
|
|
38
|
-
|
|
39
|
-
valid or
|
|
37
|
+
a failed initialization if the client did not become ready in time. Zero waits nowhere, so
|
|
38
|
+
``initialize`` reports a failed initialization unless the client is already ready. ``None`` waits
|
|
39
|
+
only in ``initialize``, without a deadline, until the data source becomes valid or fails permanently.
|
|
40
40
|
"""
|
|
41
|
-
self.__client = LDClient(config.with_wrapper_information(WRAPPER_NAME, VERSION),
|
|
41
|
+
self.__client = LDClient(config.with_wrapper_information(WRAPPER_NAME, VERSION),
|
|
42
|
+
0 if start_wait is None else start_wait)
|
|
42
43
|
self.__start_wait = start_wait
|
|
44
|
+
self.__initialization_complete = False
|
|
45
|
+
self.__status_lock = threading.Lock()
|
|
46
|
+
self.__provider_status = ProviderStatus.NOT_READY
|
|
43
47
|
|
|
44
48
|
self.__context_converter = EvaluationContextConverter()
|
|
45
49
|
self.__details_converter = ResolutionDetailsConverter()
|
|
46
50
|
|
|
47
|
-
self.__status_lock = threading.Lock()
|
|
48
|
-
self.__last_emitted_state: Optional[DataSourceState] = None
|
|
49
51
|
|
|
50
52
|
@property
|
|
51
53
|
def client(self) -> LDClient:
|
|
@@ -56,39 +58,40 @@ class LaunchDarklyProvider(AbstractProvider):
|
|
|
56
58
|
"""
|
|
57
59
|
return self.__client
|
|
58
60
|
|
|
59
|
-
def
|
|
61
|
+
def __set_status(self, status: ProviderStatus) -> bool:
|
|
60
62
|
"""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
Record the provider status and report whether it changed, and whether the change should be emitted.
|
|
64
|
+
|
|
65
|
+
The OpenFeature client emits an event for the status change which completes initialization, so that
|
|
66
|
+
change is not emitted here. Changes after initialization has completed, including after it has failed,
|
|
67
|
+
are emitted.
|
|
63
68
|
"""
|
|
64
69
|
with self.__status_lock:
|
|
65
|
-
if
|
|
70
|
+
if status == self.__provider_status:
|
|
66
71
|
return False
|
|
67
|
-
|
|
68
|
-
self.
|
|
69
|
-
return True
|
|
72
|
+
self.__provider_status = status
|
|
73
|
+
return self.__initialization_complete
|
|
70
74
|
|
|
71
75
|
def __handle_data_source_status(self, status: DataSourceStatus):
|
|
72
76
|
state = status.state
|
|
73
77
|
if state == DataSourceState.INITIALIZING:
|
|
74
78
|
return
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if state == DataSourceState.VALID:
|
|
80
|
-
self.emit_provider_ready(ProviderEventDetails())
|
|
79
|
+
elif state == DataSourceState.VALID:
|
|
80
|
+
if self.__set_status(ProviderStatus.READY):
|
|
81
|
+
self.emit_provider_ready(ProviderEventDetails())
|
|
81
82
|
elif state == DataSourceState.OFF:
|
|
82
83
|
error_message = self.__get_message(status,
|
|
83
84
|
"the provider has encountered a permanent error or has been shutdown")
|
|
84
85
|
# This is not reported as a fatal error. A fatal provider prevents the OpenFeature client
|
|
85
86
|
# from evaluating flags at all, but the LaunchDarkly client can keep evaluating the flag
|
|
86
87
|
# data it already has.
|
|
87
|
-
self.
|
|
88
|
-
|
|
88
|
+
if self.__set_status(ProviderStatus.ERROR):
|
|
89
|
+
self.emit_provider_error(ProviderEventDetails(error_code=ErrorCode.GENERAL,
|
|
90
|
+
message=error_message))
|
|
89
91
|
elif state == DataSourceState.INTERRUPTED:
|
|
90
92
|
error_message = self.__get_message(status, "encountered an unknown error")
|
|
91
|
-
self.
|
|
93
|
+
if self.__set_status(ProviderStatus.STALE):
|
|
94
|
+
self.emit_provider_stale(ProviderEventDetails(message=error_message))
|
|
92
95
|
|
|
93
96
|
# For now treat an unknown state as no change.
|
|
94
97
|
|
|
@@ -105,7 +108,11 @@ class LaunchDarklyProvider(AbstractProvider):
|
|
|
105
108
|
elif status.state == DataSourceState.OFF:
|
|
106
109
|
ready_event.set()
|
|
107
110
|
|
|
108
|
-
# We listen
|
|
111
|
+
# We listen for status changes before checking the current state, so that a change in between is not
|
|
112
|
+
# missed. Emission is suppressed until initialization has completed, so the listeners stay registered
|
|
113
|
+
# even when initialization fails and the client later recovers.
|
|
114
|
+
self.__client.data_source_status_provider.add_listener(self.__handle_data_source_status)
|
|
115
|
+
self.__client.flag_tracker.add_listener(self.__handle_flag_change)
|
|
109
116
|
self.__client.data_source_status_provider.add_listener(ready_handler)
|
|
110
117
|
|
|
111
118
|
# Check for conditions that may have happened before we added the listener.
|
|
@@ -116,17 +123,22 @@ class LaunchDarklyProvider(AbstractProvider):
|
|
|
116
123
|
ready_event.set()
|
|
117
124
|
|
|
118
125
|
# With a start wait the client constructor has already waited, so the outcome is whatever it is now.
|
|
119
|
-
if self.__start_wait
|
|
126
|
+
if self.__start_wait is None:
|
|
120
127
|
ready_event.wait()
|
|
121
128
|
|
|
122
129
|
self.__client.data_source_status_provider.remove_listener(ready_handler)
|
|
123
130
|
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
# The OpenFeature client reports the outcome of initialization itself, so the provider records the
|
|
132
|
+
# resulting status without emitting an event for it. The outcome is read and recorded, and initialization
|
|
133
|
+
# is completed, under the lock, so a concurrent status change either precedes the outcome it is included
|
|
134
|
+
# in or is emitted.
|
|
135
|
+
with self.__status_lock:
|
|
136
|
+
initialized = self.__client.is_initialized()
|
|
137
|
+
self.__provider_status = ProviderStatus.READY if initialized else ProviderStatus.ERROR
|
|
138
|
+
self.__initialization_complete = True
|
|
126
139
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
self.__client.flag_tracker.add_listener(self.__handle_flag_change)
|
|
140
|
+
if not initialized:
|
|
141
|
+
raise ProviderNotReadyError(error_message="launchdarkly client initialization failed")
|
|
130
142
|
|
|
131
143
|
def shutdown(self):
|
|
132
144
|
self.__client.data_source_status_provider.remove_listener(self.__handle_data_source_status)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.9.0" # x-release-please-version
|
{launchdarkly_openfeature_server-0.8.2 → launchdarkly_openfeature_server-0.9.0}/pyproject.toml
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "launchdarkly-openfeature-server"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.0"
|
|
4
4
|
description = "An OpenFeature provider for the LaunchDarkly Python server SDK"
|
|
5
5
|
authors = ["LaunchDarkly <dev@launchdarkly.com>"]
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -16,6 +16,7 @@ classifiers = [
|
|
|
16
16
|
"Programming Language :: Python :: 3.11",
|
|
17
17
|
"Programming Language :: Python :: 3.12",
|
|
18
18
|
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
19
20
|
"Topic :: Software Development",
|
|
20
21
|
"Topic :: Software Development :: Libraries",
|
|
21
22
|
]
|
|
@@ -27,7 +28,7 @@ packages = [
|
|
|
27
28
|
[tool.poetry.dependencies]
|
|
28
29
|
python = "^3.10"
|
|
29
30
|
openfeature-sdk = ">=0.9.0,<1"
|
|
30
|
-
launchdarkly-server-sdk = ">=9.
|
|
31
|
+
launchdarkly-server-sdk = ">=9.18.3,<10"
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
[tool.poetry.group.dev.dependencies]
|
|
@@ -44,7 +45,6 @@ optional = true
|
|
|
44
45
|
[tool.poetry.group.docs.dependencies]
|
|
45
46
|
sphinx = "^6.0.0"
|
|
46
47
|
sphinx-rtd-theme = ">=1.3,<3.0"
|
|
47
|
-
certifi = ">=2018.4.16"
|
|
48
48
|
expiringdict = ">=1.1.4"
|
|
49
49
|
pyrfc3339 = ">=1.0"
|
|
50
50
|
jsonpickle = ">1.4.1"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = "0.8.2" # x-release-please-version
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|