launchdarkly-openfeature-server 0.8.0__tar.gz → 0.8.2__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.0
3
+ Version: 0.8.2
4
4
  Summary: An OpenFeature provider for the LaunchDarkly Python server SDK
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
19
20
  Classifier: Topic :: Software Development
20
21
  Classifier: Topic :: Software Development :: Libraries
21
22
  Requires-Dist: launchdarkly-server-sdk (>=9.17.0,<10)
@@ -63,7 +64,7 @@ This matrix mirrors the [feature matrix of the OpenFeature SDK for Python](https
63
64
  | ✅ | Hooks | Hooks are registered on the OpenFeature API and client; the provider requires no additional support and its results are visible to hooks, including [flag metadata](#flag-metadata). |
64
65
  | ✅ | Logging | The provider logs through the logging configuration of the `Config` it is given. |
65
66
  | ✅ | Domains | Domains bind clients to providers in the OpenFeature SDK; a separate provider instance may be registered per domain. |
66
- | ✅ | Eventing | LaunchDarkly data source status changes are emitted as `PROVIDER_READY`, `PROVIDER_STALE` and `PROVIDER_ERROR`; flag changes as `PROVIDER_CONFIGURATION_CHANGED` with the changed flag key. |
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. |
67
68
  | ✅ | Tracking | `track` sends a LaunchDarkly custom event for the evaluation context, with the tracking event value and remaining details attached. |
68
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
70
  | ✅ | Shutdown | `shutdown` closes the LaunchDarkly client; a closed client cannot be restarted, so a new provider instance is required afterward. |
@@ -121,6 +122,8 @@ There are several other attributes which have special functionality within a sin
121
122
  - A key of `anonymous`. Must be a boolean value. [Equivalent to the 'anonymous' builder method in the SDK.](https://launchdarkly-python-sdk.readthedocs.io/en/latest/api-main.html#ldclient.ContextBuilder.anonymous)
122
123
  - A key of `name`. Must be a string. [Equivalent to the 'name' builder method in the SDK.](https://launchdarkly-python-sdk.readthedocs.io/en/latest/api-main.html#ldclient.ContextBuilder.name)
123
124
 
125
+ The LaunchDarkly context model has no date type, so a `datetime` attribute is converted to a string containing its ISO-8601 representation in UTC, which is the format the LaunchDarkly date targeting operators accept. A `datetime` without a timezone is treated as UTC. This conversion is applied to `datetime` values nested in lists and dictionaries as well.
126
+
124
127
  ### Examples
125
128
 
126
129
  #### A single user context
@@ -37,7 +37,7 @@ This matrix mirrors the [feature matrix of the OpenFeature SDK for Python](https
37
37
  | ✅ | Hooks | Hooks are registered on the OpenFeature API and client; the provider requires no additional support and its results are visible to hooks, including [flag metadata](#flag-metadata). |
38
38
  | ✅ | Logging | The provider logs through the logging configuration of the `Config` it is given. |
39
39
  | ✅ | Domains | Domains bind clients to providers in the OpenFeature SDK; a separate provider instance may be registered per domain. |
40
- | ✅ | Eventing | LaunchDarkly data source status changes are emitted as `PROVIDER_READY`, `PROVIDER_STALE` and `PROVIDER_ERROR`; flag changes as `PROVIDER_CONFIGURATION_CHANGED` with the changed flag key. |
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
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. |
43
43
  | ✅ | Shutdown | `shutdown` closes the LaunchDarkly client; a closed client cannot be restarted, so a new provider instance is required afterward. |
@@ -95,6 +95,8 @@ There are several other attributes which have special functionality within a sin
95
95
  - A key of `anonymous`. Must be a boolean value. [Equivalent to the 'anonymous' builder method in the SDK.](https://launchdarkly-python-sdk.readthedocs.io/en/latest/api-main.html#ldclient.ContextBuilder.anonymous)
96
96
  - A key of `name`. Must be a string. [Equivalent to the 'name' builder method in the SDK.](https://launchdarkly-python-sdk.readthedocs.io/en/latest/api-main.html#ldclient.ContextBuilder.name)
97
97
 
98
+ The LaunchDarkly context model has no date type, so a `datetime` attribute is converted to a string containing its ISO-8601 representation in UTC, which is the format the LaunchDarkly date targeting operators accept. A `datetime` without a timezone is treated as UTC. This conversion is applied to `datetime` values nested in lists and dictionaries as well.
99
+
98
100
  ### Examples
99
101
 
100
102
  #### A single user context
@@ -1,5 +1,6 @@
1
+ from datetime import datetime, timezone
1
2
  from logging import getLogger
2
- from typing import Any, Dict, List, Mapping, Optional
3
+ from typing import Any, Dict, List, Mapping, Optional, Sequence
3
4
 
4
5
  from ldclient.context import Context, ContextBuilder, ContextMultiBuilder
5
6
  from openfeature.evaluation_context import EvaluationContext, EvaluationContextAttribute
@@ -8,6 +9,33 @@ from openfeature.evaluation_context import EvaluationContext, EvaluationContextA
8
9
  logger = getLogger("launchdarkly-openfeature-server")
9
10
 
10
11
 
12
+ def _to_ld_value(value: Any) -> Any:
13
+ """
14
+ Convert an OpenFeature attribute value into a value the LaunchDarkly SDK can represent.
15
+
16
+ A datetime becomes its ISO-8601 representation in UTC, because the LaunchDarkly value model has
17
+ no date type. A datetime without a timezone is treated as UTC. A value which cannot be
18
+ converted becomes None.
19
+ """
20
+ if value is None or isinstance(value, (bool, int, float, str)):
21
+ return value
22
+
23
+ if isinstance(value, datetime):
24
+ if value.tzinfo is None:
25
+ value = value.replace(tzinfo=timezone.utc)
26
+
27
+ return value.astimezone(timezone.utc).isoformat().replace('+00:00', 'Z')
28
+
29
+ if isinstance(value, Mapping):
30
+ return {str(k): _to_ld_value(v) for k, v in value.items()}
31
+
32
+ if isinstance(value, Sequence):
33
+ return [_to_ld_value(item) for item in value]
34
+
35
+ logger.error("The attribute value of type %s could not be converted; using None instead", type(value).__name__)
36
+ return None
37
+
38
+
11
39
  class EvaluationContextConverter:
12
40
  def to_ld_context(self, context: EvaluationContext) -> Context:
13
41
  """
@@ -108,6 +136,6 @@ class EvaluationContextConverter:
108
136
  elif k == 'privateAttributes':
109
137
  logger.error("The attribute 'privateAttributes' must be an array")
110
138
  else:
111
- builder.set(k, v)
139
+ builder.set(k, _to_ld_value(v))
112
140
 
113
141
  return builder.build()
@@ -44,6 +44,9 @@ class LaunchDarklyProvider(AbstractProvider):
44
44
  self.__context_converter = EvaluationContextConverter()
45
45
  self.__details_converter = ResolutionDetailsConverter()
46
46
 
47
+ self.__status_lock = threading.Lock()
48
+ self.__last_emitted_state: Optional[DataSourceState] = None
49
+
47
50
  @property
48
51
  def client(self) -> LDClient:
49
52
  """
@@ -53,11 +56,27 @@ class LaunchDarklyProvider(AbstractProvider):
53
56
  """
54
57
  return self.__client
55
58
 
59
+ def __is_new_status(self, state: DataSourceState) -> bool:
60
+ """
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
+ """
64
+ with self.__status_lock:
65
+ if state == self.__last_emitted_state:
66
+ return False
67
+
68
+ self.__last_emitted_state = state
69
+ return True
70
+
56
71
  def __handle_data_source_status(self, status: DataSourceStatus):
57
72
  state = status.state
58
73
  if state == DataSourceState.INITIALIZING:
59
74
  return
60
- elif state == DataSourceState.VALID:
75
+
76
+ if not self.__is_new_status(state):
77
+ return
78
+
79
+ if state == DataSourceState.VALID:
61
80
  self.emit_provider_ready(ProviderEventDetails())
62
81
  elif state == DataSourceState.OFF:
63
82
  error_message = self.__get_message(status,
@@ -0,0 +1 @@
1
+ VERSION = "0.8.2" # x-release-please-version
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "launchdarkly-openfeature-server"
3
- version = "0.8.0"
3
+ version = "0.8.2"
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"
@@ -1 +0,0 @@
1
- VERSION = "0.8.0" # x-release-please-version