devcycle-python-server-sdk 3.3.6__py3-none-any.whl → 3.6.0__py3-none-any.whl
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.
- devcycle_python_sdk/VERSION.txt +1 -1
- devcycle_python_sdk/__init__.py +1 -0
- devcycle_python_sdk/api/config_client.py +18 -6
- devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
- devcycle_python_sdk/cloud_client.py +19 -2
- devcycle_python_sdk/devcycle_client.py +40 -0
- devcycle_python_sdk/local_client.py +16 -2
- devcycle_python_sdk/managers/config_manager.py +7 -2
- devcycle_python_sdk/models/bucketed_config.py +10 -6
- devcycle_python_sdk/models/user.py +86 -3
- devcycle_python_sdk/open_feature_provider/__init__.py +0 -0
- devcycle_python_sdk/open_feature_provider/provider.py +125 -0
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/METADATA +3 -2
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/RECORD +28 -20
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/WHEEL +1 -1
- example/local_bucketing_client_example.py +1 -1
- example/openfeature_example.py +77 -0
- test/api/test_config_client.py +24 -6
- test/api/test_local_bucketing.py +15 -12
- test/managers/test_config_manager.py +32 -10
- test/models/test_user.py +188 -0
- test/openfeature_test/__init__.py +0 -0
- test/openfeature_test/test_provider.py +219 -0
- test/openfeature_test/test_provider_local_sdk.py +136 -0
- test/test_local_client.py +10 -0
- test/util/test_version.py +1 -1
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/LICENSE +0 -0
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import MagicMock
|
|
4
|
+
|
|
5
|
+
from openfeature.provider import EvaluationContext
|
|
6
|
+
from openfeature.flag_evaluation import Reason
|
|
7
|
+
from openfeature.exception import (
|
|
8
|
+
ErrorCode,
|
|
9
|
+
TargetingKeyMissingError,
|
|
10
|
+
InvalidContextError,
|
|
11
|
+
TypeMismatchError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from devcycle_python_sdk.models.variable import Variable, TypeEnum
|
|
15
|
+
|
|
16
|
+
from devcycle_python_sdk.open_feature_provider.provider import (
|
|
17
|
+
DevCycleProvider,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DevCycleProviderTest(unittest.TestCase):
|
|
25
|
+
def setUp(self):
|
|
26
|
+
self.client = MagicMock()
|
|
27
|
+
self.client.is_initialized.return_value = True
|
|
28
|
+
self.provider = DevCycleProvider(self.client)
|
|
29
|
+
|
|
30
|
+
def test_resolve_details_client_not_ready(self):
|
|
31
|
+
self.client.is_initialized.return_value = False
|
|
32
|
+
|
|
33
|
+
details = self.provider._resolve(
|
|
34
|
+
"test-flag", False, EvaluationContext("test-user")
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
self.assertIsNotNone(details)
|
|
38
|
+
self.assertEqual(details.value, False)
|
|
39
|
+
self.assertEqual(details.reason, Reason.ERROR)
|
|
40
|
+
self.assertEqual(details.error_code, ErrorCode.PROVIDER_NOT_READY)
|
|
41
|
+
|
|
42
|
+
def test_resolve_details_client_no_user_in_context(self):
|
|
43
|
+
context = EvaluationContext(targeting_key=None, attributes={})
|
|
44
|
+
|
|
45
|
+
with self.assertRaises(TargetingKeyMissingError):
|
|
46
|
+
self.provider._resolve("test-flag", False, context)
|
|
47
|
+
|
|
48
|
+
def test_resolve_details_no_key(self):
|
|
49
|
+
self.client.variable.side_effect = ValueError("test error")
|
|
50
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
51
|
+
with self.assertRaises(InvalidContextError):
|
|
52
|
+
self.provider._resolve(None, False, context)
|
|
53
|
+
|
|
54
|
+
def test_resolve_details_no_default_value(self):
|
|
55
|
+
self.client.variable.side_effect = ValueError("test error")
|
|
56
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
57
|
+
with self.assertRaises(InvalidContextError):
|
|
58
|
+
self.provider._resolve("test-flag", None, context)
|
|
59
|
+
|
|
60
|
+
def test_resolve_details_client_returns_none(self):
|
|
61
|
+
self.client.variable.return_value = None
|
|
62
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
63
|
+
|
|
64
|
+
details = self.provider._resolve("test-flag", False, context)
|
|
65
|
+
|
|
66
|
+
self.assertIsNotNone(details)
|
|
67
|
+
self.assertEqual(details.value, False)
|
|
68
|
+
self.assertEqual(details.reason, Reason.DEFAULT)
|
|
69
|
+
|
|
70
|
+
def test_resolve_details_client_returns_default_variable(self):
|
|
71
|
+
self.client.variable.return_value = Variable.create_default_variable(
|
|
72
|
+
key="test-flag", default_value=False
|
|
73
|
+
)
|
|
74
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
75
|
+
details = self.provider._resolve("test-flag", False, context)
|
|
76
|
+
|
|
77
|
+
self.assertIsNotNone(details)
|
|
78
|
+
self.assertEqual(details.value, False)
|
|
79
|
+
self.assertEqual(details.reason, Reason.DEFAULT)
|
|
80
|
+
|
|
81
|
+
def test_resolve_boolean_details(self):
|
|
82
|
+
key = "test-flag"
|
|
83
|
+
variable_value = True
|
|
84
|
+
default_value = False
|
|
85
|
+
|
|
86
|
+
self.client.variable.return_value = Variable(
|
|
87
|
+
_id=None,
|
|
88
|
+
value=variable_value,
|
|
89
|
+
key=key,
|
|
90
|
+
type=TypeEnum.BOOLEAN,
|
|
91
|
+
isDefaulted=False,
|
|
92
|
+
defaultValue=False,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
96
|
+
details = self.provider.resolve_boolean_details(key, default_value, context)
|
|
97
|
+
|
|
98
|
+
self.assertIsNotNone(details)
|
|
99
|
+
self.assertEqual(details.value, variable_value)
|
|
100
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
101
|
+
|
|
102
|
+
def test_resolve_string_details(self):
|
|
103
|
+
key = "test-flag"
|
|
104
|
+
variable_value = "some string"
|
|
105
|
+
default_value = "default string"
|
|
106
|
+
|
|
107
|
+
self.client.variable.return_value = Variable(
|
|
108
|
+
_id=None,
|
|
109
|
+
value=variable_value,
|
|
110
|
+
key=key,
|
|
111
|
+
type=TypeEnum.STRING,
|
|
112
|
+
isDefaulted=False,
|
|
113
|
+
defaultValue=False,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
117
|
+
details = self.provider.resolve_string_details(key, default_value, context)
|
|
118
|
+
|
|
119
|
+
self.assertIsNotNone(details)
|
|
120
|
+
self.assertEqual(details.value, variable_value)
|
|
121
|
+
self.assertIsInstance(details.value, str)
|
|
122
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
123
|
+
|
|
124
|
+
def test_resolve_integer_details(self):
|
|
125
|
+
key = "test-flag"
|
|
126
|
+
variable_value = 12345
|
|
127
|
+
default_value = 0
|
|
128
|
+
|
|
129
|
+
self.client.variable.return_value = Variable(
|
|
130
|
+
_id=None,
|
|
131
|
+
value=variable_value,
|
|
132
|
+
key=key,
|
|
133
|
+
type=TypeEnum.STRING,
|
|
134
|
+
isDefaulted=False,
|
|
135
|
+
defaultValue=False,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
139
|
+
details = self.provider.resolve_integer_details(key, default_value, context)
|
|
140
|
+
|
|
141
|
+
self.assertIsNotNone(details)
|
|
142
|
+
self.assertIsInstance(details.value, int)
|
|
143
|
+
self.assertEqual(details.value, variable_value)
|
|
144
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
145
|
+
|
|
146
|
+
def test_resolve_float_details(self):
|
|
147
|
+
key = "test-flag"
|
|
148
|
+
variable_value = 3.145
|
|
149
|
+
default_value = 0.0
|
|
150
|
+
|
|
151
|
+
self.client.variable.return_value = Variable(
|
|
152
|
+
_id=None,
|
|
153
|
+
value=variable_value,
|
|
154
|
+
key=key,
|
|
155
|
+
type=TypeEnum.STRING,
|
|
156
|
+
isDefaulted=False,
|
|
157
|
+
defaultValue=False,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
161
|
+
details = self.provider.resolve_float_details(key, default_value, context)
|
|
162
|
+
|
|
163
|
+
self.assertIsNotNone(details)
|
|
164
|
+
self.assertIsInstance(details.value, float)
|
|
165
|
+
self.assertEqual(details.value, variable_value)
|
|
166
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
167
|
+
|
|
168
|
+
def test_resolve_object_details_verify_default_value(self):
|
|
169
|
+
key = "test-flag"
|
|
170
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
171
|
+
|
|
172
|
+
# Only flat dictionaries are supported as objects
|
|
173
|
+
# lists, primitives and nested objects are not supported
|
|
174
|
+
with self.assertRaises(TypeMismatchError):
|
|
175
|
+
self.provider.resolve_object_details(key, [], context)
|
|
176
|
+
|
|
177
|
+
with self.assertRaises(TypeMismatchError):
|
|
178
|
+
self.provider.resolve_object_details(key, 1234, context)
|
|
179
|
+
|
|
180
|
+
with self.assertRaises(TypeMismatchError):
|
|
181
|
+
self.provider.resolve_object_details(key, 3.14, context)
|
|
182
|
+
|
|
183
|
+
with self.assertRaises(TypeMismatchError):
|
|
184
|
+
self.provider.resolve_object_details(key, False, context)
|
|
185
|
+
|
|
186
|
+
with self.assertRaises(TypeMismatchError):
|
|
187
|
+
self.provider.resolve_object_details(key, "some string", context)
|
|
188
|
+
|
|
189
|
+
# test dictionaries with nested objects
|
|
190
|
+
with self.assertRaises(TypeMismatchError):
|
|
191
|
+
self.provider.resolve_object_details(
|
|
192
|
+
key, {"nestedMap": {"someProp": "some value"}}, context
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def test_resolve_object_details(self):
|
|
196
|
+
key = "test-flag"
|
|
197
|
+
variable_value = {"some": "value"}
|
|
198
|
+
default_value = {}
|
|
199
|
+
|
|
200
|
+
self.client.variable.return_value = Variable(
|
|
201
|
+
_id=None,
|
|
202
|
+
value=variable_value,
|
|
203
|
+
key=key,
|
|
204
|
+
type=TypeEnum.STRING,
|
|
205
|
+
isDefaulted=False,
|
|
206
|
+
defaultValue=False,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
210
|
+
details = self.provider.resolve_object_details(key, default_value, context)
|
|
211
|
+
|
|
212
|
+
self.assertIsNotNone(details)
|
|
213
|
+
self.assertIsInstance(details.value, dict)
|
|
214
|
+
self.assertDictEqual(details.value, variable_value)
|
|
215
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
unittest.main()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
import responses
|
|
6
|
+
|
|
7
|
+
from devcycle_python_sdk import DevCycleLocalClient, DevCycleLocalOptions
|
|
8
|
+
from test.fixture.data import small_config_json
|
|
9
|
+
|
|
10
|
+
from openfeature.provider import EvaluationContext
|
|
11
|
+
from openfeature.flag_evaluation import Reason
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DevCycleProviderWithLocalSDKTest(unittest.TestCase):
|
|
17
|
+
"""
|
|
18
|
+
Tests the DevCycleProvider with actual data from the Local SDK client to confirm data translations work
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def setUp(self) -> None:
|
|
22
|
+
self.sdk_key = "dvc_server_949e4962-c624-4d20-a1ea-7f2501b2ba79"
|
|
23
|
+
self.test_config_json = small_config_json()
|
|
24
|
+
self.test_etag = "2f71454e-3279-4ca7-a8e7-802ce97bef43"
|
|
25
|
+
|
|
26
|
+
config_url = "http://localhost/config/v1/server/" + self.sdk_key + ".json"
|
|
27
|
+
|
|
28
|
+
responses.add(
|
|
29
|
+
responses.GET,
|
|
30
|
+
config_url,
|
|
31
|
+
headers={"ETag": self.test_etag},
|
|
32
|
+
json=self.test_config_json,
|
|
33
|
+
status=200,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.options = DevCycleLocalOptions(
|
|
37
|
+
config_polling_interval_ms=5000,
|
|
38
|
+
config_cdn_uri="http://localhost/",
|
|
39
|
+
disable_custom_event_logging=True,
|
|
40
|
+
disable_automatic_event_logging=True,
|
|
41
|
+
)
|
|
42
|
+
self.client = None
|
|
43
|
+
|
|
44
|
+
def tearDown(self) -> None:
|
|
45
|
+
if self.client:
|
|
46
|
+
self.client.close()
|
|
47
|
+
responses.reset()
|
|
48
|
+
|
|
49
|
+
def setup_client(self):
|
|
50
|
+
self.client = DevCycleLocalClient(self.sdk_key, self.options)
|
|
51
|
+
while not self.client.is_initialized():
|
|
52
|
+
time.sleep(0.05)
|
|
53
|
+
|
|
54
|
+
@responses.activate
|
|
55
|
+
def test_resolve_boolean_details(self):
|
|
56
|
+
self.setup_client()
|
|
57
|
+
provider = self.client.get_openfeature_provider()
|
|
58
|
+
|
|
59
|
+
key = "a-cool-new-feature"
|
|
60
|
+
expected_value = True
|
|
61
|
+
default_value = False
|
|
62
|
+
|
|
63
|
+
context = EvaluationContext(targeting_key="1234")
|
|
64
|
+
details = provider.resolve_boolean_details(key, default_value, context)
|
|
65
|
+
|
|
66
|
+
self.assertIsNotNone(details)
|
|
67
|
+
self.assertEqual(details.value, expected_value)
|
|
68
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
69
|
+
|
|
70
|
+
@responses.activate
|
|
71
|
+
def test_resolve_integer_details(self):
|
|
72
|
+
self.setup_client()
|
|
73
|
+
provider = self.client.get_openfeature_provider()
|
|
74
|
+
|
|
75
|
+
key = "num-var"
|
|
76
|
+
expected_value = 12345
|
|
77
|
+
default_value = 0
|
|
78
|
+
|
|
79
|
+
context = EvaluationContext(targeting_key="1234")
|
|
80
|
+
details = provider.resolve_integer_details(key, default_value, context)
|
|
81
|
+
|
|
82
|
+
self.assertIsNotNone(details)
|
|
83
|
+
self.assertEqual(details.value, expected_value)
|
|
84
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
85
|
+
|
|
86
|
+
@responses.activate
|
|
87
|
+
def test_resolve_float_details(self):
|
|
88
|
+
self.setup_client()
|
|
89
|
+
provider = self.client.get_openfeature_provider()
|
|
90
|
+
|
|
91
|
+
key = "float-var"
|
|
92
|
+
expected_value = 3.14159
|
|
93
|
+
default_value = 0.0001
|
|
94
|
+
|
|
95
|
+
context = EvaluationContext(targeting_key="1234")
|
|
96
|
+
details = provider.resolve_float_details(key, default_value, context)
|
|
97
|
+
|
|
98
|
+
self.assertIsNotNone(details)
|
|
99
|
+
self.assertEqual(details.value, expected_value)
|
|
100
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
101
|
+
|
|
102
|
+
@responses.activate
|
|
103
|
+
def test_resolve_string_details(self):
|
|
104
|
+
self.setup_client()
|
|
105
|
+
provider = self.client.get_openfeature_provider()
|
|
106
|
+
|
|
107
|
+
key = "string-var"
|
|
108
|
+
expected_value = "variationOn"
|
|
109
|
+
default_value = "default_value"
|
|
110
|
+
|
|
111
|
+
context = EvaluationContext(targeting_key="1234")
|
|
112
|
+
details = provider.resolve_string_details(key, default_value, context)
|
|
113
|
+
|
|
114
|
+
self.assertIsNotNone(details)
|
|
115
|
+
self.assertEqual(details.value, expected_value)
|
|
116
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
117
|
+
|
|
118
|
+
@responses.activate
|
|
119
|
+
def test_resolve_object_details(self):
|
|
120
|
+
self.setup_client()
|
|
121
|
+
provider = self.client.get_openfeature_provider()
|
|
122
|
+
|
|
123
|
+
key = "json-var"
|
|
124
|
+
expected_value = {
|
|
125
|
+
"displayText": "This variation is on",
|
|
126
|
+
"showDialog": True,
|
|
127
|
+
"maxUsers": 100,
|
|
128
|
+
}
|
|
129
|
+
default_value = {"default": "value"}
|
|
130
|
+
|
|
131
|
+
context = EvaluationContext(targeting_key="1234")
|
|
132
|
+
details = provider.resolve_string_details(key, default_value, context)
|
|
133
|
+
|
|
134
|
+
self.assertIsNotNone(details)
|
|
135
|
+
self.assertEqual(details.value, expected_value)
|
|
136
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
test/test_local_client.py
CHANGED
|
@@ -217,6 +217,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
217
217
|
("string-var", "default_value", "variationOn", TypeEnum.STRING),
|
|
218
218
|
("a-cool-new-feature", False, True, TypeEnum.BOOLEAN),
|
|
219
219
|
("num-var", 0, 12345, TypeEnum.NUMBER),
|
|
220
|
+
("float-var", 0, 3.14159, TypeEnum.NUMBER),
|
|
220
221
|
(
|
|
221
222
|
"json-var",
|
|
222
223
|
{"default": "value"},
|
|
@@ -333,6 +334,15 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
333
334
|
defaultValue=None,
|
|
334
335
|
evalReason=None,
|
|
335
336
|
),
|
|
337
|
+
"float-var": Variable(
|
|
338
|
+
_id="61200363125123fca69d3a7a",
|
|
339
|
+
key="float-var",
|
|
340
|
+
type="Number",
|
|
341
|
+
value=3.14159,
|
|
342
|
+
isDefaulted=None,
|
|
343
|
+
defaultValue=None,
|
|
344
|
+
evalReason=None,
|
|
345
|
+
),
|
|
336
346
|
}
|
|
337
347
|
self.assertEqual(result, expected_variables)
|
|
338
348
|
|
test/util/test_version.py
CHANGED
|
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
|
|
|
10
10
|
class VersionTest(unittest.TestCase):
|
|
11
11
|
def test_sdk_version(self):
|
|
12
12
|
version = sdk_version()
|
|
13
|
-
self.
|
|
13
|
+
self.assertRegex(version, r"^\d+\.\d+\.\d+$")
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
if __name__ == "__main__":
|
{devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|