launchdarkly-openfeature-server 0.2.2__tar.gz → 0.4.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.
Files changed (14) hide show
  1. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/PKG-INFO +5 -6
  2. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/README.md +1 -1
  3. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/ld_openfeature/provider.py +34 -17
  4. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/pyproject.toml +5 -5
  5. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/tests/test_provider.py +39 -27
  6. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/LICENSE +0 -0
  7. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/ld_openfeature/__init__.py +0 -0
  8. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/ld_openfeature/impl/context_converter.py +0 -0
  9. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/ld_openfeature/impl/details_converter.py +0 -0
  10. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/ld_openfeature/py.typed +0 -0
  11. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/tests/__init__.py +0 -0
  12. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/tests/impl/test_context_converter.py +0 -0
  13. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/tests/impl/test_details_converter.py +0 -0
  14. {launchdarkly_openfeature_server-0.2.2 → launchdarkly_openfeature_server-0.4.2}/tests/test_data_sources.py +0 -0
@@ -1,21 +1,20 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: launchdarkly-openfeature-server
3
- Version: 0.2.2
3
+ Version: 0.4.2
4
4
  Summary: An OpenFeature provider for the LaunchDarkly Python server SDK
5
- Home-page: https://github.com/launchdarkly/openfeature-python-server
6
5
  License: Apache-2.0
7
6
  Author: LaunchDarkly
8
7
  Author-email: dev@launchdarkly.com
9
- Requires-Python: >=3.8,<4.0
8
+ Requires-Python: >=3.9,<4.0
10
9
  Classifier: Intended Audience :: Developers
11
10
  Classifier: License :: OSI Approved :: Apache Software License
12
11
  Classifier: Operating System :: OS Independent
13
12
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.8
15
13
  Classifier: Programming Language :: Python :: 3.9
16
14
  Classifier: Programming Language :: Python :: 3.10
17
15
  Classifier: Programming Language :: Python :: 3.11
18
16
  Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
19
18
  Classifier: Topic :: Software Development
20
19
  Classifier: Topic :: Software Development :: Libraries
21
20
  Requires-Dist: launchdarkly-server-sdk (<10)
@@ -50,7 +49,7 @@ This provider is designed primarily for use in multi-user systems such as web se
50
49
 
51
50
  ## Supported Python versions
52
51
 
53
- This version of the LaunchDarkly provider works with Python 3.8 and above.
52
+ This version of the LaunchDarkly provider works with Python 3.9 and above.
54
53
 
55
54
  ## Getting started
56
55
 
@@ -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.8 and above.
27
+ This version of the LaunchDarkly provider works with Python 3.9 and above.
28
28
 
29
29
  ## Getting started
30
30
 
@@ -1,6 +1,7 @@
1
1
  import threading
2
2
  from typing import Any, List, Optional, Union
3
3
 
4
+ from ldclient.evaluation import EvaluationDetail
4
5
  from ldclient import LDClient, Config
5
6
  from ldclient.interfaces import DataSourceStatus, FlagChange, DataSourceState
6
7
  from openfeature.evaluation_context import EvaluationContext
@@ -22,6 +23,15 @@ class LaunchDarklyProvider(AbstractProvider):
22
23
  self.__context_converter = EvaluationContextConverter()
23
24
  self.__details_converter = ResolutionDetailsConverter()
24
25
 
26
+ @property
27
+ def client(self) -> LDClient:
28
+ """
29
+ Access the underlying LaunchDarky client instance backing this provider.
30
+
31
+ This is useful for accessing additional functionality not exposed by the provider.
32
+ """
33
+ return self.__client
34
+
25
35
  def __handle_data_source_status(self, status: DataSourceStatus):
26
36
  state = status.state
27
37
  if state == DataSourceState.INITIALIZING:
@@ -141,24 +151,31 @@ class LaunchDarklyProvider(AbstractProvider):
141
151
  ld_context = self.__context_converter.to_ld_context(evaluation_context)
142
152
  result = self.__client.variation_detail(flag_key, ld_context, default_value)
143
153
 
144
- if flag_type == FlagType.BOOLEAN and not isinstance(result.value, bool):
145
- return self.__mismatched_type_details(default_value)
146
- elif flag_type == FlagType.STRING and not isinstance(result.value, str):
147
- return self.__mismatched_type_details(default_value)
148
- elif flag_type == FlagType.INTEGER and isinstance(result.value, bool):
149
- # Python treats boolean values as instances of int
150
- return self.__mismatched_type_details(default_value)
151
- elif flag_type == FlagType.FLOAT and isinstance(result.value, bool):
152
- # Python treats boolean values as instances of int
154
+ resolved_value = self.__validate_and_cast_value(flag_type, result.value)
155
+ if resolved_value is None:
153
156
  return self.__mismatched_type_details(default_value)
154
- elif flag_type == FlagType.INTEGER and not isinstance(result.value, int):
155
- return self.__mismatched_type_details(default_value)
156
- elif flag_type == FlagType.FLOAT and not isinstance(result.value, float) and not isinstance(result.value, int):
157
- return self.__mismatched_type_details(default_value)
158
- elif flag_type == FlagType.OBJECT and not isinstance(result.value, dict) and not isinstance(result.value, list):
159
- return self.__mismatched_type_details(default_value)
160
-
161
- return self.__details_converter.to_resolution_details(result)
157
+
158
+ resolved_detail = EvaluationDetail(
159
+ value=resolved_value,
160
+ variation_index=result.variation_index,
161
+ reason=result.reason,
162
+ )
163
+
164
+ return self.__details_converter.to_resolution_details(resolved_detail)
165
+
166
+ def __validate_and_cast_value(self, flag_type: FlagType, value: Any):
167
+ """Serializes the raw flag value to the expected type based on flag_type."""
168
+ if flag_type == FlagType.BOOLEAN and isinstance(value, bool):
169
+ return value
170
+ elif flag_type == FlagType.STRING and isinstance(value, str):
171
+ return value
172
+ elif flag_type == FlagType.INTEGER and isinstance(value, (int, float)) and not isinstance(value, bool):
173
+ return int(value) # Float decimals are truncated to int
174
+ elif flag_type == FlagType.FLOAT and isinstance(value, (int, float)) and not isinstance(value, bool):
175
+ return float(value)
176
+ elif flag_type == FlagType.OBJECT and isinstance(value, (dict, list)):
177
+ return value
178
+ return None
162
179
 
163
180
  @staticmethod
164
181
  def __mismatched_type_details(default_value: Any) -> FlagResolutionDetails:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "launchdarkly-openfeature-server"
3
- version = "0.2.2"
3
+ version = "0.4.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"
@@ -12,11 +12,11 @@ classifiers = [
12
12
  "License :: OSI Approved :: Apache Software License",
13
13
  "Operating System :: OS Independent",
14
14
  "Programming Language :: Python :: 3",
15
- "Programming Language :: Python :: 3.8",
16
15
  "Programming Language :: Python :: 3.9",
17
16
  "Programming Language :: Python :: 3.10",
18
17
  "Programming Language :: Python :: 3.11",
19
18
  "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
20
  "Topic :: Software Development",
21
21
  "Topic :: Software Development :: Libraries",
22
22
  ]
@@ -27,7 +27,7 @@ packages = [
27
27
 
28
28
 
29
29
  [tool.poetry.dependencies]
30
- python = "^3.8"
30
+ python = "^3.9"
31
31
  openfeature-sdk = ">=0.7.0,<1"
32
32
  launchdarkly-server-sdk = "<10"
33
33
 
@@ -56,13 +56,13 @@ jinja2 = "3.1.3"
56
56
 
57
57
 
58
58
  [tool.mypy]
59
- python_version = "3.8"
59
+ python_version = "3.9"
60
60
  install_types = true
61
61
  non_interactive = true
62
62
 
63
63
 
64
64
  [tool.isort]
65
- py_version=38
65
+ py_version=39
66
66
 
67
67
 
68
68
  [tool.pytest.ini_options]
@@ -42,6 +42,10 @@ def test_metadata_name_is_correct(provider: LaunchDarklyProvider):
42
42
  assert provider.get_metadata().name == "launchdarkly-openfeature-server"
43
43
 
44
44
 
45
+ def test_ldclient_is_accessible(provider: LaunchDarklyProvider):
46
+ assert type(provider.client) is LDClient
47
+
48
+
45
49
  def test_not_providing_context_returns_error(provider: LaunchDarklyProvider):
46
50
  resolution_details = provider.resolve_boolean_details("flag-key", True, None)
47
51
 
@@ -84,32 +88,38 @@ def test_invalid_types_generate_type_mismatch_results(provider: LaunchDarklyProv
84
88
 
85
89
 
86
90
  @pytest.mark.parametrize(
87
- "default_value,return_value,expected_value,method_name",
88
- [
89
- pytest.param(True, False, False, 'resolve_boolean_details'),
90
- pytest.param(False, True, True, 'resolve_boolean_details'),
91
- pytest.param(False, 1, False, 'resolve_boolean_details'),
92
- pytest.param(False, "True", False, 'resolve_boolean_details'),
93
- pytest.param(True, [], True, 'resolve_boolean_details'),
94
-
95
- pytest.param('default-string', 'return-string', 'return-string', 'resolve_string_details'),
96
- pytest.param('default-string', 1, 'default-string', 'resolve_string_details'),
97
- pytest.param('default-string', True, 'default-string', 'resolve_string_details'),
98
-
99
- pytest.param(1, 2, 2, 'resolve_integer_details'),
100
- pytest.param(1, True, 1, 'resolve_integer_details'),
101
- pytest.param(1, False, 1, 'resolve_integer_details'),
102
- pytest.param(1, "", 1, 'resolve_integer_details'),
103
-
104
- pytest.param(1.0, 2.0, 2.0, 'resolve_float_details'),
105
- pytest.param(1.0, 2, 2.0, 'resolve_float_details'),
106
- pytest.param(1.0, True, 1.0, 'resolve_float_details'),
107
- pytest.param(1.0, 'return-string', 1.0, 'resolve_float_details'),
108
-
109
- pytest.param(['default-value'], ['return-string'], ['return-string'], 'resolve_object_details'),
110
- pytest.param(['default-value'], True, ['default-value'], 'resolve_object_details'),
111
- pytest.param(['default-value'], 1, ['default-value'], 'resolve_object_details'),
112
- pytest.param(['default-value'], 'return-string', ['default-value'], 'resolve_object_details'),
91
+ "default_value,return_value,expected_value,expected_type,method_name",
92
+ [
93
+ pytest.param(True, False, False, bool, 'resolve_boolean_details'),
94
+ pytest.param(False, True, True, bool, 'resolve_boolean_details'),
95
+ pytest.param(False, 1, False, bool, 'resolve_boolean_details'),
96
+ pytest.param(False, "True", False, bool, 'resolve_boolean_details'),
97
+ pytest.param(True, [], True, bool, 'resolve_boolean_details'),
98
+
99
+ pytest.param('default-string', 'return-string', 'return-string', str, 'resolve_string_details'),
100
+ pytest.param('default-string', 1, 'default-string', str, 'resolve_string_details'),
101
+ pytest.param('default-string', True, 'default-string', str, 'resolve_string_details'),
102
+
103
+ pytest.param(1, 2, 2, int, 'resolve_integer_details'),
104
+ pytest.param(1, True, 1, int, 'resolve_integer_details'),
105
+ pytest.param(1, False, 1, int, 'resolve_integer_details'),
106
+ pytest.param(1, "", 1, int, 'resolve_integer_details'),
107
+ pytest.param(1, 2.9, 2, int, 'resolve_integer_details'),
108
+
109
+ pytest.param(1.0, 2.0, 2.0, float, 'resolve_float_details'),
110
+ pytest.param(1.0, 2, 2.0, float, 'resolve_float_details'),
111
+ pytest.param(1.0, True, 1.0, float, 'resolve_float_details'),
112
+ pytest.param(1.0, 'return-string', 1.0, float, 'resolve_float_details'),
113
+
114
+ pytest.param(['default-value'], ['return-string'], ['return-string'], list, 'resolve_object_details'),
115
+ pytest.param(['default-value'], True, ['default-value'], list, 'resolve_object_details'),
116
+ pytest.param(['default-value'], 1, ['default-value'], list, 'resolve_object_details'),
117
+ pytest.param(['default-value'], 'return-string', ['default-value'], list, 'resolve_object_details'),
118
+
119
+ pytest.param({'key': 'default'}, {'key': 'return'}, {'key': 'return'}, dict, 'resolve_object_details'),
120
+ pytest.param({'key': 'default'}, True, {'key': 'default'}, dict, 'resolve_object_details'),
121
+ pytest.param({'key': 'default'}, 1, {'key': 'default'}, dict, 'resolve_object_details'),
122
+ pytest.param({'key': 'default'}, 'return-string', {'key': 'default'}, dict, 'resolve_object_details'),
113
123
  ],
114
124
  )
115
125
  def test_check_method_and_result_match_type(
@@ -117,6 +127,7 @@ def test_check_method_and_result_match_type(
117
127
  default_value: Union[bool, str, int, float, List],
118
128
  return_value: Union[bool, str, int, float, List],
119
129
  expected_value: Union[bool, str, int, float, List],
130
+ expected_type: type,
120
131
  method_name: str,
121
132
  # end of parameterized values
122
133
  test_data_source: TestData,
@@ -126,8 +137,9 @@ def test_check_method_and_result_match_type(
126
137
 
127
138
  method = getattr(provider, method_name)
128
139
  resolution_details = method("check-method-flag", default_value, evaluation_context)
129
-
140
+
130
141
  assert resolution_details.value == expected_value
142
+ #assert isinstance(resolution_details.value, expected_type)
131
143
 
132
144
 
133
145
  def test_logger_changes_should_cascade_to_evaluation_converter(provider: LaunchDarklyProvider, caplog):