devcycle-python-server-sdk 3.11.3__py3-none-any.whl → 3.12.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/api/bucketing_client.py +12 -0
- devcycle_python_sdk/api/local_bucketing.py +0 -8
- devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
- devcycle_python_sdk/cloud_client.py +51 -4
- devcycle_python_sdk/local_client.py +65 -14
- devcycle_python_sdk/managers/eval_hooks_manager.py +73 -0
- devcycle_python_sdk/managers/sse_manager.py +1 -1
- devcycle_python_sdk/models/bucketed_config.py +0 -1
- devcycle_python_sdk/models/eval_hook.py +18 -0
- devcycle_python_sdk/models/eval_hook_context.py +10 -0
- devcycle_python_sdk/models/eval_reason.py +40 -0
- devcycle_python_sdk/models/user.py +16 -4
- devcycle_python_sdk/models/variable.py +44 -6
- devcycle_python_sdk/open_feature_provider/provider.py +10 -5
- devcycle_python_sdk/options.py +8 -1
- devcycle_python_sdk/protobuf/utils.py +18 -0
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +43 -30
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi +100 -78
- {devcycle_python_server_sdk-3.11.3.dist-info → devcycle_python_server_sdk-3.12.0.dist-info}/METADATA +1 -1
- {devcycle_python_server_sdk-3.11.3.dist-info → devcycle_python_server_sdk-3.12.0.dist-info}/RECORD +30 -26
- test/api/test_local_bucketing.py +11 -0
- test/models/test_user.py +90 -0
- test/openfeature_test/test_provider.py +79 -2
- test/openfeature_test/test_provider_local_sdk.py +25 -0
- test/test_cloud_client.py +89 -0
- test/test_local_client.py +97 -1
- {devcycle_python_server_sdk-3.11.3.dist-info → devcycle_python_server_sdk-3.12.0.dist-info}/WHEEL +0 -0
- {devcycle_python_server_sdk-3.11.3.dist-info → devcycle_python_server_sdk-3.12.0.dist-info}/licenses/LICENSE +0 -0
- {devcycle_python_server_sdk-3.11.3.dist-info → devcycle_python_server_sdk-3.12.0.dist-info}/top_level.txt +0 -0
|
@@ -12,6 +12,10 @@ from openfeature.exception import (
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
from devcycle_python_sdk.models.variable import Variable, TypeEnum
|
|
15
|
+
from devcycle_python_sdk.models.eval_reason import (
|
|
16
|
+
EvalReason,
|
|
17
|
+
DefaultReasonDetails,
|
|
18
|
+
)
|
|
15
19
|
|
|
16
20
|
from devcycle_python_sdk.open_feature_provider.provider import (
|
|
17
21
|
DevCycleProvider,
|
|
@@ -69,7 +73,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
69
73
|
|
|
70
74
|
def test_resolve_details_client_returns_default_variable(self):
|
|
71
75
|
self.client.variable.return_value = Variable.create_default_variable(
|
|
72
|
-
key="test-flag",
|
|
76
|
+
key="test-flag",
|
|
77
|
+
default_value=False,
|
|
78
|
+
default_reason_detail=DefaultReasonDetails.USER_NOT_TARGETED,
|
|
73
79
|
)
|
|
74
80
|
context = EvaluationContext(targeting_key="user-1234")
|
|
75
81
|
details = self.provider._resolve("test-flag", False, context)
|
|
@@ -77,6 +83,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
77
83
|
self.assertIsNotNone(details)
|
|
78
84
|
self.assertEqual(details.value, False)
|
|
79
85
|
self.assertEqual(details.reason, Reason.DEFAULT)
|
|
86
|
+
self.assertEqual(
|
|
87
|
+
details.flag_metadata["evalReasonDetails"], "User Not Targeted"
|
|
88
|
+
)
|
|
80
89
|
|
|
81
90
|
def test_resolve_boolean_details(self):
|
|
82
91
|
key = "test-flag"
|
|
@@ -90,6 +99,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
90
99
|
type=TypeEnum.BOOLEAN,
|
|
91
100
|
isDefaulted=False,
|
|
92
101
|
defaultValue=False,
|
|
102
|
+
eval=EvalReason(
|
|
103
|
+
reason="TARGETING_MATCH", details="All Users", target_id="targetId"
|
|
104
|
+
),
|
|
93
105
|
)
|
|
94
106
|
|
|
95
107
|
context = EvaluationContext(targeting_key="user-1234")
|
|
@@ -98,6 +110,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
98
110
|
self.assertIsNotNone(details)
|
|
99
111
|
self.assertEqual(details.value, variable_value)
|
|
100
112
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
113
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
114
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users")
|
|
115
|
+
self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId")
|
|
101
116
|
|
|
102
117
|
def test_resolve_string_details(self):
|
|
103
118
|
key = "test-flag"
|
|
@@ -111,6 +126,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
111
126
|
type=TypeEnum.STRING,
|
|
112
127
|
isDefaulted=False,
|
|
113
128
|
defaultValue=False,
|
|
129
|
+
eval=EvalReason(
|
|
130
|
+
reason="TARGETING_MATCH", details="All Users", target_id="targetId"
|
|
131
|
+
),
|
|
114
132
|
)
|
|
115
133
|
|
|
116
134
|
context = EvaluationContext(targeting_key="user-1234")
|
|
@@ -120,6 +138,8 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
120
138
|
self.assertEqual(details.value, variable_value)
|
|
121
139
|
self.assertIsInstance(details.value, str)
|
|
122
140
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
141
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users")
|
|
142
|
+
self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId")
|
|
123
143
|
|
|
124
144
|
def test_resolve_integer_details(self):
|
|
125
145
|
key = "test-flag"
|
|
@@ -133,6 +153,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
133
153
|
type=TypeEnum.STRING,
|
|
134
154
|
isDefaulted=False,
|
|
135
155
|
defaultValue=False,
|
|
156
|
+
eval=EvalReason(
|
|
157
|
+
reason="TARGETING_MATCH", details="All Users", target_id="targetId"
|
|
158
|
+
),
|
|
136
159
|
)
|
|
137
160
|
|
|
138
161
|
context = EvaluationContext(targeting_key="user-1234")
|
|
@@ -142,6 +165,8 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
142
165
|
self.assertIsInstance(details.value, int)
|
|
143
166
|
self.assertEqual(details.value, variable_value)
|
|
144
167
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
168
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users")
|
|
169
|
+
self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId")
|
|
145
170
|
|
|
146
171
|
def test_resolve_float_details(self):
|
|
147
172
|
key = "test-flag"
|
|
@@ -155,6 +180,7 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
155
180
|
type=TypeEnum.STRING,
|
|
156
181
|
isDefaulted=False,
|
|
157
182
|
defaultValue=False,
|
|
183
|
+
eval=EvalReason(reason="SPLIT", details="Rollout", target_id="targetId"),
|
|
158
184
|
)
|
|
159
185
|
|
|
160
186
|
context = EvaluationContext(targeting_key="user-1234")
|
|
@@ -163,7 +189,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
163
189
|
self.assertIsNotNone(details)
|
|
164
190
|
self.assertIsInstance(details.value, float)
|
|
165
191
|
self.assertEqual(details.value, variable_value)
|
|
166
|
-
self.assertEqual(details.reason, Reason.
|
|
192
|
+
self.assertEqual(details.reason, Reason.SPLIT)
|
|
193
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "Rollout")
|
|
194
|
+
self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId")
|
|
167
195
|
|
|
168
196
|
def test_resolve_object_details_verify_default_value(self):
|
|
169
197
|
key = "test-flag"
|
|
@@ -204,6 +232,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
204
232
|
type=TypeEnum.STRING,
|
|
205
233
|
isDefaulted=False,
|
|
206
234
|
defaultValue=False,
|
|
235
|
+
eval=EvalReason(
|
|
236
|
+
reason="TARGETING_MATCH", details="Rollout", target_id="targetId"
|
|
237
|
+
),
|
|
207
238
|
)
|
|
208
239
|
|
|
209
240
|
context = EvaluationContext(targeting_key="user-1234")
|
|
@@ -213,6 +244,52 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
213
244
|
self.assertIsInstance(details.value, dict)
|
|
214
245
|
self.assertDictEqual(details.value, variable_value)
|
|
215
246
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
247
|
+
self.assertIsNotNone(details.flag_metadata)
|
|
248
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "Rollout")
|
|
249
|
+
self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId")
|
|
250
|
+
|
|
251
|
+
def test_resolve_string_details_null_eval(self):
|
|
252
|
+
key = "test-flag"
|
|
253
|
+
variable_value = "some string"
|
|
254
|
+
default_value = "default string"
|
|
255
|
+
|
|
256
|
+
self.client.variable.return_value = Variable(
|
|
257
|
+
_id=None,
|
|
258
|
+
value=variable_value,
|
|
259
|
+
key=key,
|
|
260
|
+
type=TypeEnum.STRING,
|
|
261
|
+
isDefaulted=False,
|
|
262
|
+
defaultValue=False,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
266
|
+
details = self.provider.resolve_string_details(key, default_value, context)
|
|
267
|
+
|
|
268
|
+
self.assertIsNotNone(details)
|
|
269
|
+
self.assertEqual(details.value, variable_value)
|
|
270
|
+
self.assertIsInstance(details.value, str)
|
|
271
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
272
|
+
|
|
273
|
+
def test_default_string_details_null_eval(self):
|
|
274
|
+
key = "test-flag"
|
|
275
|
+
default_value = "default string"
|
|
276
|
+
|
|
277
|
+
self.client.variable.return_value = Variable(
|
|
278
|
+
_id=None,
|
|
279
|
+
value=default_value,
|
|
280
|
+
key=key,
|
|
281
|
+
type=TypeEnum.STRING,
|
|
282
|
+
isDefaulted=True,
|
|
283
|
+
defaultValue=False,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
287
|
+
details = self.provider.resolve_string_details(key, default_value, context)
|
|
288
|
+
|
|
289
|
+
self.assertIsNotNone(details)
|
|
290
|
+
self.assertEqual(details.value, default_value)
|
|
291
|
+
self.assertIsInstance(details.value, str)
|
|
292
|
+
self.assertEqual(details.reason, Reason.DEFAULT)
|
|
216
293
|
|
|
217
294
|
|
|
218
295
|
if __name__ == "__main__":
|
|
@@ -66,6 +66,11 @@ class DevCycleProviderWithLocalSDKTest(unittest.TestCase):
|
|
|
66
66
|
self.assertIsNotNone(details)
|
|
67
67
|
self.assertEqual(details.value, expected_value)
|
|
68
68
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
69
|
+
self.assertIsNotNone(details.flag_metadata)
|
|
70
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users")
|
|
71
|
+
self.assertEqual(
|
|
72
|
+
details.flag_metadata["evalReasonTargetId"], "63125321d31c601f992288bc"
|
|
73
|
+
)
|
|
69
74
|
|
|
70
75
|
@responses.activate
|
|
71
76
|
def test_resolve_integer_details(self):
|
|
@@ -82,6 +87,11 @@ class DevCycleProviderWithLocalSDKTest(unittest.TestCase):
|
|
|
82
87
|
self.assertIsNotNone(details)
|
|
83
88
|
self.assertEqual(details.value, expected_value)
|
|
84
89
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
90
|
+
self.assertIsNotNone(details.flag_metadata)
|
|
91
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users")
|
|
92
|
+
self.assertEqual(
|
|
93
|
+
details.flag_metadata["evalReasonTargetId"], "63125321d31c601f992288bc"
|
|
94
|
+
)
|
|
85
95
|
|
|
86
96
|
@responses.activate
|
|
87
97
|
def test_resolve_float_details(self):
|
|
@@ -98,6 +108,11 @@ class DevCycleProviderWithLocalSDKTest(unittest.TestCase):
|
|
|
98
108
|
self.assertIsNotNone(details)
|
|
99
109
|
self.assertEqual(details.value, expected_value)
|
|
100
110
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
111
|
+
self.assertIsNotNone(details.flag_metadata)
|
|
112
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users")
|
|
113
|
+
self.assertEqual(
|
|
114
|
+
details.flag_metadata["evalReasonTargetId"], "63125321d31c601f992288bc"
|
|
115
|
+
)
|
|
101
116
|
|
|
102
117
|
@responses.activate
|
|
103
118
|
def test_resolve_string_details(self):
|
|
@@ -114,6 +129,11 @@ class DevCycleProviderWithLocalSDKTest(unittest.TestCase):
|
|
|
114
129
|
self.assertIsNotNone(details)
|
|
115
130
|
self.assertEqual(details.value, expected_value)
|
|
116
131
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
132
|
+
self.assertIsNotNone(details.flag_metadata)
|
|
133
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users")
|
|
134
|
+
self.assertEqual(
|
|
135
|
+
details.flag_metadata["evalReasonTargetId"], "63125321d31c601f992288bc"
|
|
136
|
+
)
|
|
117
137
|
|
|
118
138
|
@responses.activate
|
|
119
139
|
def test_resolve_object_details(self):
|
|
@@ -134,3 +154,8 @@ class DevCycleProviderWithLocalSDKTest(unittest.TestCase):
|
|
|
134
154
|
self.assertIsNotNone(details)
|
|
135
155
|
self.assertEqual(details.value, expected_value)
|
|
136
156
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
157
|
+
self.assertIsNotNone(details.flag_metadata)
|
|
158
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users")
|
|
159
|
+
self.assertEqual(
|
|
160
|
+
details.flag_metadata["evalReasonTargetId"], "63125321d31c601f992288bc"
|
|
161
|
+
)
|
test/test_cloud_client.py
CHANGED
|
@@ -5,7 +5,9 @@ import uuid
|
|
|
5
5
|
from time import time
|
|
6
6
|
from unittest.mock import patch
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
from devcycle_python_sdk import DevCycleCloudClient, DevCycleCloudOptions
|
|
10
|
+
from devcycle_python_sdk.models.eval_hook import EvalHook
|
|
9
11
|
from devcycle_python_sdk.models.user import DevCycleUser
|
|
10
12
|
from devcycle_python_sdk.models.variable import Variable, TypeEnum
|
|
11
13
|
from devcycle_python_sdk.models.event import DevCycleEvent
|
|
@@ -27,6 +29,7 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
27
29
|
self.test_user_empty_id = DevCycleUser(user_id="")
|
|
28
30
|
|
|
29
31
|
def tearDown(self) -> None:
|
|
32
|
+
self.test_client.clear_hooks()
|
|
30
33
|
pass
|
|
31
34
|
|
|
32
35
|
def test_create_client_invalid_sdk_key(self):
|
|
@@ -95,6 +98,8 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
95
98
|
self.assertIsNotNone(result)
|
|
96
99
|
self.assertEqual(result.value, "default_value")
|
|
97
100
|
self.assertTrue(result.isDefaulted)
|
|
101
|
+
self.assertEqual(result.eval.reason, "DEFAULT")
|
|
102
|
+
self.assertEqual(result.eval.details, "Missing Variable")
|
|
98
103
|
|
|
99
104
|
# other exception - return default
|
|
100
105
|
mock_variable_call.reset_mock()
|
|
@@ -103,6 +108,8 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
103
108
|
self.assertIsNotNone(result)
|
|
104
109
|
self.assertEqual(result.value, "default_value")
|
|
105
110
|
self.assertTrue(result.isDefaulted)
|
|
111
|
+
self.assertEqual(result.eval.reason, "DEFAULT")
|
|
112
|
+
self.assertEqual(result.eval.details, "Error")
|
|
106
113
|
|
|
107
114
|
@patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable")
|
|
108
115
|
def test_variable_type_mismatch(self, mock_variable_call):
|
|
@@ -114,6 +121,8 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
114
121
|
self.assertIsNotNone(result)
|
|
115
122
|
self.assertEqual(result.value, "default_value")
|
|
116
123
|
self.assertTrue(result.isDefaulted)
|
|
124
|
+
self.assertEqual(result.eval.reason, "DEFAULT")
|
|
125
|
+
self.assertEqual(result.eval.details, "Variable Type Mismatch")
|
|
117
126
|
|
|
118
127
|
@patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable")
|
|
119
128
|
def test_variable_value_defaults(self, mock_variable_call):
|
|
@@ -281,6 +290,86 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
281
290
|
),
|
|
282
291
|
)
|
|
283
292
|
|
|
293
|
+
@patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable")
|
|
294
|
+
def test_hooks(self, mock_variable_call):
|
|
295
|
+
mock_variable_call.return_value = Variable(
|
|
296
|
+
_id="123", key="strKey", value=999, type=TypeEnum.NUMBER
|
|
297
|
+
)
|
|
298
|
+
# Test adding hooks
|
|
299
|
+
hook_called = {
|
|
300
|
+
"before": False,
|
|
301
|
+
"after": False,
|
|
302
|
+
"finally": False,
|
|
303
|
+
"error": False,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
def before_hook(context):
|
|
307
|
+
hook_called["before"] = True
|
|
308
|
+
return context
|
|
309
|
+
|
|
310
|
+
def after_hook(context, variable):
|
|
311
|
+
hook_called["after"] = True
|
|
312
|
+
|
|
313
|
+
def finally_hook(context, variable):
|
|
314
|
+
hook_called["finally"] = True
|
|
315
|
+
|
|
316
|
+
def error_hook(context, error):
|
|
317
|
+
hook_called["error"] = True
|
|
318
|
+
|
|
319
|
+
self.test_client.add_hook(
|
|
320
|
+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Test hooks called during variable evaluation
|
|
324
|
+
variable = self.test_client.variable(self.test_user, "strKey", 42)
|
|
325
|
+
self.assertTrue(variable.value == 999)
|
|
326
|
+
self.assertFalse(variable.isDefaulted)
|
|
327
|
+
|
|
328
|
+
self.assertTrue(hook_called["before"])
|
|
329
|
+
self.assertTrue(hook_called["after"])
|
|
330
|
+
self.assertTrue(hook_called["finally"])
|
|
331
|
+
self.assertFalse(hook_called["error"])
|
|
332
|
+
|
|
333
|
+
@patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable")
|
|
334
|
+
def test_hook_exceptions(self, mock_variable_call):
|
|
335
|
+
mock_variable_call.return_value = Variable(
|
|
336
|
+
_id="123", key="strKey", value=999, type=TypeEnum.NUMBER
|
|
337
|
+
)
|
|
338
|
+
# Test adding hooks
|
|
339
|
+
hook_called = {
|
|
340
|
+
"before": False,
|
|
341
|
+
"after": False,
|
|
342
|
+
"finally": False,
|
|
343
|
+
"error": False,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
def before_hook(context):
|
|
347
|
+
hook_called["before"] = True
|
|
348
|
+
raise Exception("Before hook failed")
|
|
349
|
+
|
|
350
|
+
def after_hook(context, variable):
|
|
351
|
+
hook_called["after"] = True
|
|
352
|
+
|
|
353
|
+
def finally_hook(context, variable):
|
|
354
|
+
hook_called["finally"] = True
|
|
355
|
+
|
|
356
|
+
def error_hook(context, error):
|
|
357
|
+
hook_called["error"] = True
|
|
358
|
+
|
|
359
|
+
self.test_client.add_hook(
|
|
360
|
+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Test hooks called during variable evaluation
|
|
364
|
+
variable = self.test_client.variable(self.test_user, "strKey", 42)
|
|
365
|
+
self.assertTrue(variable.value == 999)
|
|
366
|
+
self.assertFalse(variable.isDefaulted)
|
|
367
|
+
|
|
368
|
+
self.assertTrue(hook_called["before"])
|
|
369
|
+
self.assertFalse(hook_called["after"])
|
|
370
|
+
self.assertTrue(hook_called["finally"])
|
|
371
|
+
self.assertTrue(hook_called["error"])
|
|
372
|
+
|
|
284
373
|
|
|
285
374
|
if __name__ == "__main__":
|
|
286
375
|
unittest.main()
|
test/test_local_client.py
CHANGED
|
@@ -9,6 +9,8 @@ import responses
|
|
|
9
9
|
from devcycle_python_sdk import DevCycleLocalClient, DevCycleLocalOptions
|
|
10
10
|
from devcycle_python_sdk.local_client import _validate_user, _validate_sdk_key
|
|
11
11
|
from devcycle_python_sdk.exceptions import MalformedConfigError
|
|
12
|
+
from devcycle_python_sdk.models.eval_hook import EvalHook
|
|
13
|
+
from devcycle_python_sdk.models.eval_reason import EvalReason
|
|
12
14
|
from devcycle_python_sdk.models.event import DevCycleEvent
|
|
13
15
|
from devcycle_python_sdk.models.feature import Feature
|
|
14
16
|
from devcycle_python_sdk.api.local_bucketing import LocalBucketing
|
|
@@ -207,6 +209,8 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
207
209
|
self.assertEqual(result.defaultValue, test_value)
|
|
208
210
|
self.assertEqual(result.value, test_value)
|
|
209
211
|
self.assertEqual(result.type, value_type)
|
|
212
|
+
self.assertEqual(result.eval.reason, "DEFAULT")
|
|
213
|
+
self.assertEqual(result.eval.details, "User Not Targeted")
|
|
210
214
|
|
|
211
215
|
@responses.activate
|
|
212
216
|
def test_variable_with_bucketing(self):
|
|
@@ -240,6 +244,9 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
240
244
|
self.assertDictEqual(result.value, expected, msg="Test key: " + key)
|
|
241
245
|
else:
|
|
242
246
|
self.assertEqual(result.value, expected, msg="Test key: " + key)
|
|
247
|
+
self.assertEqual(result.eval.reason, "TARGETING_MATCH")
|
|
248
|
+
self.assertEqual(result.eval.details, "All Users")
|
|
249
|
+
self.assertEqual(result.eval.target_id, "63125321d31c601f992288bc")
|
|
243
250
|
|
|
244
251
|
@responses.activate
|
|
245
252
|
def test_variable_with_events(self):
|
|
@@ -293,7 +300,11 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
293
300
|
user = DevCycleUser(user_id="1234")
|
|
294
301
|
|
|
295
302
|
result = self.client.all_variables(user)
|
|
296
|
-
|
|
303
|
+
expected_eval = EvalReason(
|
|
304
|
+
reason="TARGETING_MATCH",
|
|
305
|
+
details="All Users",
|
|
306
|
+
target_id="63125321d31c601f992288bc",
|
|
307
|
+
)
|
|
297
308
|
expected_variables = {
|
|
298
309
|
"a-cool-new-feature": Variable(
|
|
299
310
|
_id="62fbf6566f1ba302829f9e34",
|
|
@@ -303,6 +314,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
303
314
|
isDefaulted=None,
|
|
304
315
|
defaultValue=None,
|
|
305
316
|
evalReason=None,
|
|
317
|
+
eval=expected_eval,
|
|
306
318
|
),
|
|
307
319
|
"string-var": Variable(
|
|
308
320
|
_id="63125320a4719939fd57cb2b",
|
|
@@ -312,6 +324,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
312
324
|
isDefaulted=None,
|
|
313
325
|
defaultValue=None,
|
|
314
326
|
evalReason=None,
|
|
327
|
+
eval=expected_eval,
|
|
315
328
|
),
|
|
316
329
|
"json-var": Variable(
|
|
317
330
|
_id="64372363125123fca69d3f7b",
|
|
@@ -325,6 +338,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
325
338
|
isDefaulted=None,
|
|
326
339
|
defaultValue=None,
|
|
327
340
|
evalReason=None,
|
|
341
|
+
eval=expected_eval,
|
|
328
342
|
),
|
|
329
343
|
"num-var": Variable(
|
|
330
344
|
_id="65272363125123fca69d3a7d",
|
|
@@ -334,6 +348,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
334
348
|
isDefaulted=None,
|
|
335
349
|
defaultValue=None,
|
|
336
350
|
evalReason=None,
|
|
351
|
+
eval=expected_eval,
|
|
337
352
|
),
|
|
338
353
|
"float-var": Variable(
|
|
339
354
|
_id="61200363125123fca69d3a7a",
|
|
@@ -343,6 +358,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
343
358
|
isDefaulted=None,
|
|
344
359
|
defaultValue=None,
|
|
345
360
|
evalReason=None,
|
|
361
|
+
eval=expected_eval,
|
|
346
362
|
),
|
|
347
363
|
}
|
|
348
364
|
self.assertEqual(result, expected_variables)
|
|
@@ -361,6 +377,86 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
361
377
|
result = self.client.all_variables(user)
|
|
362
378
|
self.assertEqual(result, {})
|
|
363
379
|
|
|
380
|
+
@responses.activate
|
|
381
|
+
def test_hooks(self):
|
|
382
|
+
self.setup_client()
|
|
383
|
+
# Test adding hooks
|
|
384
|
+
hook_called = {
|
|
385
|
+
"before": False,
|
|
386
|
+
"after": False,
|
|
387
|
+
"finally": False,
|
|
388
|
+
"error": False,
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
def before_hook(context):
|
|
392
|
+
hook_called["before"] = True
|
|
393
|
+
return context
|
|
394
|
+
|
|
395
|
+
def after_hook(context, variable):
|
|
396
|
+
hook_called["after"] = True
|
|
397
|
+
|
|
398
|
+
def finally_hook(context, variable):
|
|
399
|
+
hook_called["finally"] = True
|
|
400
|
+
|
|
401
|
+
def error_hook(context, error):
|
|
402
|
+
hook_called["error"] = True
|
|
403
|
+
|
|
404
|
+
self.client.add_hook(
|
|
405
|
+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
user = DevCycleUser(user_id="1234")
|
|
409
|
+
|
|
410
|
+
# Test hooks called during variable evaluation
|
|
411
|
+
variable = self.client.variable(user, "num-var", 42)
|
|
412
|
+
self.assertTrue(variable.value == 12345)
|
|
413
|
+
self.assertFalse(variable.isDefaulted)
|
|
414
|
+
|
|
415
|
+
self.assertTrue(hook_called["before"])
|
|
416
|
+
self.assertTrue(hook_called["after"])
|
|
417
|
+
self.assertTrue(hook_called["finally"])
|
|
418
|
+
self.assertFalse(hook_called["error"])
|
|
419
|
+
|
|
420
|
+
@responses.activate
|
|
421
|
+
def test_hook_exceptions(self):
|
|
422
|
+
self.setup_client()
|
|
423
|
+
# Test adding hooks
|
|
424
|
+
hook_called = {
|
|
425
|
+
"before": False,
|
|
426
|
+
"after": False,
|
|
427
|
+
"finally": False,
|
|
428
|
+
"error": False,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
def before_hook(context):
|
|
432
|
+
hook_called["before"] = True
|
|
433
|
+
raise Exception("Before hook failed")
|
|
434
|
+
|
|
435
|
+
def after_hook(context, variable):
|
|
436
|
+
hook_called["after"] = True
|
|
437
|
+
|
|
438
|
+
def finally_hook(context, variable):
|
|
439
|
+
hook_called["finally"] = True
|
|
440
|
+
|
|
441
|
+
def error_hook(context, error):
|
|
442
|
+
hook_called["error"] = True
|
|
443
|
+
|
|
444
|
+
self.client.add_hook(
|
|
445
|
+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
user = DevCycleUser(user_id="1234")
|
|
449
|
+
|
|
450
|
+
# Test hooks called during variable evaluation
|
|
451
|
+
variable = self.client.variable(user, "num-var", 42)
|
|
452
|
+
self.assertTrue(variable.value == 12345)
|
|
453
|
+
self.assertFalse(variable.isDefaulted)
|
|
454
|
+
|
|
455
|
+
self.assertTrue(hook_called["before"])
|
|
456
|
+
self.assertFalse(hook_called["after"])
|
|
457
|
+
self.assertTrue(hook_called["finally"])
|
|
458
|
+
self.assertTrue(hook_called["error"])
|
|
459
|
+
|
|
364
460
|
|
|
365
461
|
def _benchmark_variable_call(client: DevCycleLocalClient, user: DevCycleUser, key: str):
|
|
366
462
|
return client.variable(user, key, "default_value")
|
{devcycle_python_server_sdk-3.11.3.dist-info → devcycle_python_server_sdk-3.12.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|