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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: launchdarkly-openfeature-server
3
- Version: 0.8.2
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.17.0,<10)
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.9 and above.
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; zero applies no timeout and waits until the data source becomes valid or permanently fails. |
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 does not block the constructor at all, and initialization then waits without a deadline for the data source to become valid or to fail permanently.
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.9 and above.
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; zero applies no timeout and waits until the data source becomes valid or permanently fails. |
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 does not block the constructor at all, and initialization then waits without a deadline for the data source to become valid or to fail permanently.
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 does not block this
38
- constructor at all, and ``initialize`` then waits without a deadline for the data source to become
39
- valid or to fail permanently.
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), start_wait)
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 __is_new_status(self, state: DataSourceState) -> bool:
61
+ def __set_status(self, status: ProviderStatus) -> bool:
60
62
  """
61
- Report whether a state changes the provider status. Several data source states can map to the same provider
62
- status, and a repeated status is not a change an application can act on.
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 state == self.__last_emitted_state:
70
+ if status == self.__provider_status:
66
71
  return False
67
-
68
- self.__last_emitted_state = state
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
- if not self.__is_new_status(state):
77
- return
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.emit_provider_error(ProviderEventDetails(error_code=ErrorCode.GENERAL,
88
- message=error_message))
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.emit_provider_stale(ProviderEventDetails(message=error_message))
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 just to handle the ready event. We do not emit events because the client emits them for us.
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 <= 0:
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
- if not self.__client.is_initialized():
125
- raise ProviderNotReadyError(error_message="launchdarkly client initialization failed")
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
- # Listen to new status events and emit them.
128
- self.__client.data_source_status_provider.add_listener(self.__handle_data_source_status)
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "launchdarkly-openfeature-server"
3
- version = "0.8.2"
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.17.0,<10"
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