devcycle-python-server-sdk 3.11.4__py3-none-any.whl → 3.13.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 +19 -12
- devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
- devcycle_python_sdk/cloud_client.py +14 -5
- devcycle_python_sdk/local_client.py +41 -18
- devcycle_python_sdk/managers/config_manager.py +4 -0
- devcycle_python_sdk/managers/eval_hooks_manager.py +15 -4
- devcycle_python_sdk/managers/event_queue_manager.py +0 -1
- devcycle_python_sdk/managers/sse_manager.py +1 -1
- devcycle_python_sdk/models/config_metadata.py +40 -0
- devcycle_python_sdk/models/environment_metadata.py +26 -0
- devcycle_python_sdk/models/eval_hook.py +5 -2
- devcycle_python_sdk/models/eval_hook_context.py +23 -5
- devcycle_python_sdk/models/eval_reason.py +39 -0
- devcycle_python_sdk/models/project_metadata.py +24 -0
- devcycle_python_sdk/models/variable.py +44 -6
- devcycle_python_sdk/models/variable_metadata.py +22 -0
- devcycle_python_sdk/open_feature_provider/provider.py +10 -5
- devcycle_python_sdk/options.py +1 -1
- devcycle_python_sdk/protobuf/utils.py +27 -3
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +36 -31
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi +99 -77
- {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/METADATA +2 -2
- {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/RECORD +34 -29
- test/api/test_local_bucketing.py +17 -3
- test/models/test_user.py +0 -1
- test/openfeature_test/test_provider.py +79 -3
- test/openfeature_test/test_provider_local_sdk.py +25 -0
- test/test_cloud_client.py +46 -4
- test/test_local_client.py +69 -5
- {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/WHEEL +0 -0
- {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/licenses/LICENSE +0 -0
- {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/top_level.txt +0 -0
test/api/test_local_bucketing.py
CHANGED
|
@@ -10,6 +10,7 @@ from devcycle_python_sdk.models.bucketed_config import (
|
|
|
10
10
|
Project,
|
|
11
11
|
ProjectSettings,
|
|
12
12
|
)
|
|
13
|
+
from devcycle_python_sdk.models.eval_reason import EvalReason
|
|
13
14
|
from devcycle_python_sdk.models.feature import Feature
|
|
14
15
|
from devcycle_python_sdk.models.variable import Variable
|
|
15
16
|
from devcycle_python_sdk.models.platform_data import default_platform_data
|
|
@@ -97,7 +98,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
97
98
|
self.local_bucketing.set_platform_data(platform_json)
|
|
98
99
|
self.local_bucketing.init_event_queue(self.client_uuid, "{}")
|
|
99
100
|
user = DevCycleUser(user_id="test_user_id")
|
|
100
|
-
result = self.local_bucketing.get_variable_for_user_protobuf(
|
|
101
|
+
result, feature_id = self.local_bucketing.get_variable_for_user_protobuf(
|
|
101
102
|
user=user, key="string-var", default_value="default value"
|
|
102
103
|
)
|
|
103
104
|
self.assertIsNotNone(result)
|
|
@@ -105,6 +106,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
105
106
|
self.assertEqual(result.type, TypeEnum.STRING)
|
|
106
107
|
self.assertEqual(result.value, "variationOn")
|
|
107
108
|
self.assertFalse(result.isDefaulted)
|
|
109
|
+
self.assertIsNotNone(feature_id)
|
|
108
110
|
|
|
109
111
|
def test_get_variable_for_user_protobuf_special_characters(self):
|
|
110
112
|
self.local_bucketing.store_config(special_character_config())
|
|
@@ -112,7 +114,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
112
114
|
self.local_bucketing.set_platform_data(platform_json)
|
|
113
115
|
self.local_bucketing.init_event_queue(self.client_uuid, "{}")
|
|
114
116
|
user = DevCycleUser(user_id="test_user_id")
|
|
115
|
-
result = self.local_bucketing.get_variable_for_user_protobuf(
|
|
117
|
+
result, feature_id = self.local_bucketing.get_variable_for_user_protobuf(
|
|
116
118
|
user=user, key="string-var", default_value="default value"
|
|
117
119
|
)
|
|
118
120
|
self.assertIsNotNone(result)
|
|
@@ -120,6 +122,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
120
122
|
self.assertEqual(result.type, TypeEnum.STRING)
|
|
121
123
|
self.assertEqual(result.value, "öé 🐍 ¥ variationOn")
|
|
122
124
|
self.assertFalse(result.isDefaulted)
|
|
125
|
+
self.assertIsNotNone(feature_id)
|
|
123
126
|
|
|
124
127
|
def test_get_variable_for_user_protobuf_type_mismatch(self):
|
|
125
128
|
self.local_bucketing.store_config(small_config())
|
|
@@ -130,10 +133,11 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
130
133
|
|
|
131
134
|
# type mismatch is handled inside the WASM and will return
|
|
132
135
|
# no data if the type is not correct
|
|
133
|
-
result = self.local_bucketing.get_variable_for_user_protobuf(
|
|
136
|
+
result, feature_id = self.local_bucketing.get_variable_for_user_protobuf(
|
|
134
137
|
user=user, key="string-var", default_value=9999
|
|
135
138
|
)
|
|
136
139
|
self.assertIsNone(result)
|
|
140
|
+
self.assertIsNone(feature_id)
|
|
137
141
|
|
|
138
142
|
def test_generate_bucketed_config(self):
|
|
139
143
|
self.local_bucketing.store_config(small_config())
|
|
@@ -155,6 +159,11 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
155
159
|
evalReason=None,
|
|
156
160
|
)
|
|
157
161
|
}
|
|
162
|
+
expected_eval = EvalReason(
|
|
163
|
+
reason="TARGETING_MATCH",
|
|
164
|
+
details="All Users",
|
|
165
|
+
target_id="63125321d31c601f992288bc",
|
|
166
|
+
)
|
|
158
167
|
expected_variables = {
|
|
159
168
|
"a-cool-new-feature": Variable(
|
|
160
169
|
_id="62fbf6566f1ba302829f9e34",
|
|
@@ -164,6 +173,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
164
173
|
isDefaulted=None,
|
|
165
174
|
defaultValue=None,
|
|
166
175
|
evalReason=None,
|
|
176
|
+
eval=expected_eval,
|
|
167
177
|
),
|
|
168
178
|
"string-var": Variable(
|
|
169
179
|
_id="63125320a4719939fd57cb2b",
|
|
@@ -173,6 +183,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
173
183
|
isDefaulted=None,
|
|
174
184
|
defaultValue=None,
|
|
175
185
|
evalReason=None,
|
|
186
|
+
eval=expected_eval,
|
|
176
187
|
),
|
|
177
188
|
"json-var": Variable(
|
|
178
189
|
_id="64372363125123fca69d3f7b",
|
|
@@ -186,6 +197,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
186
197
|
isDefaulted=None,
|
|
187
198
|
defaultValue=None,
|
|
188
199
|
evalReason=None,
|
|
200
|
+
eval=expected_eval,
|
|
189
201
|
),
|
|
190
202
|
"num-var": Variable(
|
|
191
203
|
_id="65272363125123fca69d3a7d",
|
|
@@ -195,6 +207,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
195
207
|
isDefaulted=None,
|
|
196
208
|
defaultValue=None,
|
|
197
209
|
evalReason=None,
|
|
210
|
+
eval=expected_eval,
|
|
198
211
|
),
|
|
199
212
|
"float-var": Variable(
|
|
200
213
|
_id="61200363125123fca69d3a7a",
|
|
@@ -204,6 +217,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
204
217
|
isDefaulted=None,
|
|
205
218
|
defaultValue=None,
|
|
206
219
|
evalReason=None,
|
|
220
|
+
eval=expected_eval,
|
|
207
221
|
),
|
|
208
222
|
}
|
|
209
223
|
|
test/models/test_user.py
CHANGED
|
@@ -12,12 +12,15 @@ 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,
|
|
18
22
|
)
|
|
19
23
|
|
|
20
|
-
|
|
21
24
|
logger = logging.getLogger(__name__)
|
|
22
25
|
|
|
23
26
|
|
|
@@ -69,7 +72,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
69
72
|
|
|
70
73
|
def test_resolve_details_client_returns_default_variable(self):
|
|
71
74
|
self.client.variable.return_value = Variable.create_default_variable(
|
|
72
|
-
key="test-flag",
|
|
75
|
+
key="test-flag",
|
|
76
|
+
default_value=False,
|
|
77
|
+
default_reason_detail=DefaultReasonDetails.USER_NOT_TARGETED,
|
|
73
78
|
)
|
|
74
79
|
context = EvaluationContext(targeting_key="user-1234")
|
|
75
80
|
details = self.provider._resolve("test-flag", False, context)
|
|
@@ -77,6 +82,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
77
82
|
self.assertIsNotNone(details)
|
|
78
83
|
self.assertEqual(details.value, False)
|
|
79
84
|
self.assertEqual(details.reason, Reason.DEFAULT)
|
|
85
|
+
self.assertEqual(
|
|
86
|
+
details.flag_metadata["evalReasonDetails"], "User Not Targeted"
|
|
87
|
+
)
|
|
80
88
|
|
|
81
89
|
def test_resolve_boolean_details(self):
|
|
82
90
|
key = "test-flag"
|
|
@@ -90,6 +98,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
90
98
|
type=TypeEnum.BOOLEAN,
|
|
91
99
|
isDefaulted=False,
|
|
92
100
|
defaultValue=False,
|
|
101
|
+
eval=EvalReason(
|
|
102
|
+
reason="TARGETING_MATCH", details="All Users", target_id="targetId"
|
|
103
|
+
),
|
|
93
104
|
)
|
|
94
105
|
|
|
95
106
|
context = EvaluationContext(targeting_key="user-1234")
|
|
@@ -98,6 +109,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
98
109
|
self.assertIsNotNone(details)
|
|
99
110
|
self.assertEqual(details.value, variable_value)
|
|
100
111
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
112
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
113
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users")
|
|
114
|
+
self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId")
|
|
101
115
|
|
|
102
116
|
def test_resolve_string_details(self):
|
|
103
117
|
key = "test-flag"
|
|
@@ -111,6 +125,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
111
125
|
type=TypeEnum.STRING,
|
|
112
126
|
isDefaulted=False,
|
|
113
127
|
defaultValue=False,
|
|
128
|
+
eval=EvalReason(
|
|
129
|
+
reason="TARGETING_MATCH", details="All Users", target_id="targetId"
|
|
130
|
+
),
|
|
114
131
|
)
|
|
115
132
|
|
|
116
133
|
context = EvaluationContext(targeting_key="user-1234")
|
|
@@ -120,6 +137,8 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
120
137
|
self.assertEqual(details.value, variable_value)
|
|
121
138
|
self.assertIsInstance(details.value, str)
|
|
122
139
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
140
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users")
|
|
141
|
+
self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId")
|
|
123
142
|
|
|
124
143
|
def test_resolve_integer_details(self):
|
|
125
144
|
key = "test-flag"
|
|
@@ -133,6 +152,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
133
152
|
type=TypeEnum.STRING,
|
|
134
153
|
isDefaulted=False,
|
|
135
154
|
defaultValue=False,
|
|
155
|
+
eval=EvalReason(
|
|
156
|
+
reason="TARGETING_MATCH", details="All Users", target_id="targetId"
|
|
157
|
+
),
|
|
136
158
|
)
|
|
137
159
|
|
|
138
160
|
context = EvaluationContext(targeting_key="user-1234")
|
|
@@ -142,6 +164,8 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
142
164
|
self.assertIsInstance(details.value, int)
|
|
143
165
|
self.assertEqual(details.value, variable_value)
|
|
144
166
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
167
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users")
|
|
168
|
+
self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId")
|
|
145
169
|
|
|
146
170
|
def test_resolve_float_details(self):
|
|
147
171
|
key = "test-flag"
|
|
@@ -155,6 +179,7 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
155
179
|
type=TypeEnum.STRING,
|
|
156
180
|
isDefaulted=False,
|
|
157
181
|
defaultValue=False,
|
|
182
|
+
eval=EvalReason(reason="SPLIT", details="Rollout", target_id="targetId"),
|
|
158
183
|
)
|
|
159
184
|
|
|
160
185
|
context = EvaluationContext(targeting_key="user-1234")
|
|
@@ -163,7 +188,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
163
188
|
self.assertIsNotNone(details)
|
|
164
189
|
self.assertIsInstance(details.value, float)
|
|
165
190
|
self.assertEqual(details.value, variable_value)
|
|
166
|
-
self.assertEqual(details.reason, Reason.
|
|
191
|
+
self.assertEqual(details.reason, Reason.SPLIT)
|
|
192
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "Rollout")
|
|
193
|
+
self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId")
|
|
167
194
|
|
|
168
195
|
def test_resolve_object_details_verify_default_value(self):
|
|
169
196
|
key = "test-flag"
|
|
@@ -204,6 +231,9 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
204
231
|
type=TypeEnum.STRING,
|
|
205
232
|
isDefaulted=False,
|
|
206
233
|
defaultValue=False,
|
|
234
|
+
eval=EvalReason(
|
|
235
|
+
reason="TARGETING_MATCH", details="Rollout", target_id="targetId"
|
|
236
|
+
),
|
|
207
237
|
)
|
|
208
238
|
|
|
209
239
|
context = EvaluationContext(targeting_key="user-1234")
|
|
@@ -213,6 +243,52 @@ class DevCycleProviderTest(unittest.TestCase):
|
|
|
213
243
|
self.assertIsInstance(details.value, dict)
|
|
214
244
|
self.assertDictEqual(details.value, variable_value)
|
|
215
245
|
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
246
|
+
self.assertIsNotNone(details.flag_metadata)
|
|
247
|
+
self.assertEqual(details.flag_metadata["evalReasonDetails"], "Rollout")
|
|
248
|
+
self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId")
|
|
249
|
+
|
|
250
|
+
def test_resolve_string_details_null_eval(self):
|
|
251
|
+
key = "test-flag"
|
|
252
|
+
variable_value = "some string"
|
|
253
|
+
default_value = "default string"
|
|
254
|
+
|
|
255
|
+
self.client.variable.return_value = Variable(
|
|
256
|
+
_id=None,
|
|
257
|
+
value=variable_value,
|
|
258
|
+
key=key,
|
|
259
|
+
type=TypeEnum.STRING,
|
|
260
|
+
isDefaulted=False,
|
|
261
|
+
defaultValue=False,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
265
|
+
details = self.provider.resolve_string_details(key, default_value, context)
|
|
266
|
+
|
|
267
|
+
self.assertIsNotNone(details)
|
|
268
|
+
self.assertEqual(details.value, variable_value)
|
|
269
|
+
self.assertIsInstance(details.value, str)
|
|
270
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
271
|
+
|
|
272
|
+
def test_default_string_details_null_eval(self):
|
|
273
|
+
key = "test-flag"
|
|
274
|
+
default_value = "default string"
|
|
275
|
+
|
|
276
|
+
self.client.variable.return_value = Variable(
|
|
277
|
+
_id=None,
|
|
278
|
+
value=default_value,
|
|
279
|
+
key=key,
|
|
280
|
+
type=TypeEnum.STRING,
|
|
281
|
+
isDefaulted=True,
|
|
282
|
+
defaultValue=False,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
286
|
+
details = self.provider.resolve_string_details(key, default_value, context)
|
|
287
|
+
|
|
288
|
+
self.assertIsNotNone(details)
|
|
289
|
+
self.assertEqual(details.value, default_value)
|
|
290
|
+
self.assertIsInstance(details.value, str)
|
|
291
|
+
self.assertEqual(details.reason, Reason.DEFAULT)
|
|
216
292
|
|
|
217
293
|
|
|
218
294
|
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
|
@@ -97,6 +97,8 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
97
97
|
self.assertIsNotNone(result)
|
|
98
98
|
self.assertEqual(result.value, "default_value")
|
|
99
99
|
self.assertTrue(result.isDefaulted)
|
|
100
|
+
self.assertEqual(result.eval.reason, "DEFAULT")
|
|
101
|
+
self.assertEqual(result.eval.details, "Error")
|
|
100
102
|
|
|
101
103
|
# other exception - return default
|
|
102
104
|
mock_variable_call.reset_mock()
|
|
@@ -105,6 +107,8 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
105
107
|
self.assertIsNotNone(result)
|
|
106
108
|
self.assertEqual(result.value, "default_value")
|
|
107
109
|
self.assertTrue(result.isDefaulted)
|
|
110
|
+
self.assertEqual(result.eval.reason, "DEFAULT")
|
|
111
|
+
self.assertEqual(result.eval.details, "Error")
|
|
108
112
|
|
|
109
113
|
@patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable")
|
|
110
114
|
def test_variable_type_mismatch(self, mock_variable_call):
|
|
@@ -116,6 +120,8 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
116
120
|
self.assertIsNotNone(result)
|
|
117
121
|
self.assertEqual(result.value, "default_value")
|
|
118
122
|
self.assertTrue(result.isDefaulted)
|
|
123
|
+
self.assertEqual(result.eval.reason, "DEFAULT")
|
|
124
|
+
self.assertEqual(result.eval.details, "Variable Type Mismatch")
|
|
119
125
|
|
|
120
126
|
@patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable")
|
|
121
127
|
def test_variable_value_defaults(self, mock_variable_call):
|
|
@@ -300,10 +306,10 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
300
306
|
hook_called["before"] = True
|
|
301
307
|
return context
|
|
302
308
|
|
|
303
|
-
def after_hook(context, variable):
|
|
309
|
+
def after_hook(context, variable, variable_metadata):
|
|
304
310
|
hook_called["after"] = True
|
|
305
311
|
|
|
306
|
-
def finally_hook(context, variable):
|
|
312
|
+
def finally_hook(context, variable, variable_metadata):
|
|
307
313
|
hook_called["finally"] = True
|
|
308
314
|
|
|
309
315
|
def error_hook(context, error):
|
|
@@ -340,10 +346,10 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
340
346
|
hook_called["before"] = True
|
|
341
347
|
raise Exception("Before hook failed")
|
|
342
348
|
|
|
343
|
-
def after_hook(context, variable):
|
|
349
|
+
def after_hook(context, variable, variable_metadata):
|
|
344
350
|
hook_called["after"] = True
|
|
345
351
|
|
|
346
|
-
def finally_hook(context, variable):
|
|
352
|
+
def finally_hook(context, variable, variable_metadata):
|
|
347
353
|
hook_called["finally"] = True
|
|
348
354
|
|
|
349
355
|
def error_hook(context, error):
|
|
@@ -363,6 +369,42 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
363
369
|
self.assertTrue(hook_called["finally"])
|
|
364
370
|
self.assertTrue(hook_called["error"])
|
|
365
371
|
|
|
372
|
+
@patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable")
|
|
373
|
+
def test_context_has_null_config_metadata(self, mock_variable_call):
|
|
374
|
+
mock_variable_call.return_value = Variable(
|
|
375
|
+
_id="123", key="strKey", value=999, type=TypeEnum.NUMBER
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
context_received = None
|
|
379
|
+
|
|
380
|
+
def before_hook(context):
|
|
381
|
+
nonlocal context_received
|
|
382
|
+
context_received = context
|
|
383
|
+
return context
|
|
384
|
+
|
|
385
|
+
def after_hook(context, variable, variable_metadata):
|
|
386
|
+
pass
|
|
387
|
+
|
|
388
|
+
def finally_hook(context, variable, variable_metadata):
|
|
389
|
+
pass
|
|
390
|
+
|
|
391
|
+
def error_hook(context, error):
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
self.test_client.add_hook(
|
|
395
|
+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Test that context has config_metadata field but it's null for cloud client
|
|
399
|
+
variable = self.test_client.variable(self.test_user, "strKey", 42)
|
|
400
|
+
|
|
401
|
+
# Verify the variable evaluation worked
|
|
402
|
+
self.assertIsNotNone(variable)
|
|
403
|
+
self.assertIsNotNone(context_received)
|
|
404
|
+
self.assertTrue(hasattr(context_received, "config_metadata"))
|
|
405
|
+
# Cloud client should have null config_metadata since it's not implemented
|
|
406
|
+
self.assertIsNone(context_received.config_metadata)
|
|
407
|
+
|
|
366
408
|
|
|
367
409
|
if __name__ == "__main__":
|
|
368
410
|
unittest.main()
|
test/test_local_client.py
CHANGED
|
@@ -10,6 +10,7 @@ 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
12
|
from devcycle_python_sdk.models.eval_hook import EvalHook
|
|
13
|
+
from devcycle_python_sdk.models.eval_reason import EvalReason
|
|
13
14
|
from devcycle_python_sdk.models.event import DevCycleEvent
|
|
14
15
|
from devcycle_python_sdk.models.feature import Feature
|
|
15
16
|
from devcycle_python_sdk.api.local_bucketing import LocalBucketing
|
|
@@ -208,6 +209,8 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
208
209
|
self.assertEqual(result.defaultValue, test_value)
|
|
209
210
|
self.assertEqual(result.value, test_value)
|
|
210
211
|
self.assertEqual(result.type, value_type)
|
|
212
|
+
self.assertEqual(result.eval.reason, "DEFAULT")
|
|
213
|
+
self.assertEqual(result.eval.details, "User Not Targeted")
|
|
211
214
|
|
|
212
215
|
@responses.activate
|
|
213
216
|
def test_variable_with_bucketing(self):
|
|
@@ -241,6 +244,9 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
241
244
|
self.assertDictEqual(result.value, expected, msg="Test key: " + key)
|
|
242
245
|
else:
|
|
243
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")
|
|
244
250
|
|
|
245
251
|
@responses.activate
|
|
246
252
|
def test_variable_with_events(self):
|
|
@@ -294,7 +300,11 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
294
300
|
user = DevCycleUser(user_id="1234")
|
|
295
301
|
|
|
296
302
|
result = self.client.all_variables(user)
|
|
297
|
-
|
|
303
|
+
expected_eval = EvalReason(
|
|
304
|
+
reason="TARGETING_MATCH",
|
|
305
|
+
details="All Users",
|
|
306
|
+
target_id="63125321d31c601f992288bc",
|
|
307
|
+
)
|
|
298
308
|
expected_variables = {
|
|
299
309
|
"a-cool-new-feature": Variable(
|
|
300
310
|
_id="62fbf6566f1ba302829f9e34",
|
|
@@ -304,6 +314,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
304
314
|
isDefaulted=None,
|
|
305
315
|
defaultValue=None,
|
|
306
316
|
evalReason=None,
|
|
317
|
+
eval=expected_eval,
|
|
307
318
|
),
|
|
308
319
|
"string-var": Variable(
|
|
309
320
|
_id="63125320a4719939fd57cb2b",
|
|
@@ -313,6 +324,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
313
324
|
isDefaulted=None,
|
|
314
325
|
defaultValue=None,
|
|
315
326
|
evalReason=None,
|
|
327
|
+
eval=expected_eval,
|
|
316
328
|
),
|
|
317
329
|
"json-var": Variable(
|
|
318
330
|
_id="64372363125123fca69d3f7b",
|
|
@@ -326,6 +338,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
326
338
|
isDefaulted=None,
|
|
327
339
|
defaultValue=None,
|
|
328
340
|
evalReason=None,
|
|
341
|
+
eval=expected_eval,
|
|
329
342
|
),
|
|
330
343
|
"num-var": Variable(
|
|
331
344
|
_id="65272363125123fca69d3a7d",
|
|
@@ -335,6 +348,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
335
348
|
isDefaulted=None,
|
|
336
349
|
defaultValue=None,
|
|
337
350
|
evalReason=None,
|
|
351
|
+
eval=expected_eval,
|
|
338
352
|
),
|
|
339
353
|
"float-var": Variable(
|
|
340
354
|
_id="61200363125123fca69d3a7a",
|
|
@@ -344,6 +358,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
344
358
|
isDefaulted=None,
|
|
345
359
|
defaultValue=None,
|
|
346
360
|
evalReason=None,
|
|
361
|
+
eval=expected_eval,
|
|
347
362
|
),
|
|
348
363
|
}
|
|
349
364
|
self.assertEqual(result, expected_variables)
|
|
@@ -377,10 +392,10 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
377
392
|
hook_called["before"] = True
|
|
378
393
|
return context
|
|
379
394
|
|
|
380
|
-
def after_hook(context, variable):
|
|
395
|
+
def after_hook(context, variable, variable_metadata):
|
|
381
396
|
hook_called["after"] = True
|
|
382
397
|
|
|
383
|
-
def finally_hook(context, variable):
|
|
398
|
+
def finally_hook(context, variable, variable_metadata):
|
|
384
399
|
hook_called["finally"] = True
|
|
385
400
|
|
|
386
401
|
def error_hook(context, error):
|
|
@@ -417,10 +432,10 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
417
432
|
hook_called["before"] = True
|
|
418
433
|
raise Exception("Before hook failed")
|
|
419
434
|
|
|
420
|
-
def after_hook(context, variable):
|
|
435
|
+
def after_hook(context, variable, variable_metadata):
|
|
421
436
|
hook_called["after"] = True
|
|
422
437
|
|
|
423
|
-
def finally_hook(context, variable):
|
|
438
|
+
def finally_hook(context, variable, variable_metadata):
|
|
424
439
|
hook_called["finally"] = True
|
|
425
440
|
|
|
426
441
|
def error_hook(context, error):
|
|
@@ -442,6 +457,55 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
442
457
|
self.assertTrue(hook_called["finally"])
|
|
443
458
|
self.assertTrue(hook_called["error"])
|
|
444
459
|
|
|
460
|
+
@responses.activate
|
|
461
|
+
def test_context_has_config_metadata(self):
|
|
462
|
+
self.setup_client()
|
|
463
|
+
|
|
464
|
+
context_received = None
|
|
465
|
+
|
|
466
|
+
def before_hook(context):
|
|
467
|
+
nonlocal context_received
|
|
468
|
+
context_received = context
|
|
469
|
+
return context
|
|
470
|
+
|
|
471
|
+
def after_hook(context, variable, variable_metadata):
|
|
472
|
+
pass
|
|
473
|
+
|
|
474
|
+
def finally_hook(context, variable, variable_metadata):
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
def error_hook(context, error):
|
|
478
|
+
pass
|
|
479
|
+
|
|
480
|
+
self.client.add_hook(
|
|
481
|
+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
user = DevCycleUser(user_id="1234")
|
|
485
|
+
|
|
486
|
+
# Test that context has config_metadata field
|
|
487
|
+
variable = self.client.variable(user, "num-var", 42)
|
|
488
|
+
|
|
489
|
+
# Verify the variable evaluation worked
|
|
490
|
+
self.assertIsNotNone(variable)
|
|
491
|
+
self.assertIsNotNone(context_received)
|
|
492
|
+
self.assertTrue(hasattr(context_received, "config_metadata"))
|
|
493
|
+
# For local client, config_metadata should be populated
|
|
494
|
+
self.assertIsNotNone(context_received.config_metadata)
|
|
495
|
+
self.assertTrue(hasattr(context_received.config_metadata, "project"))
|
|
496
|
+
self.assertTrue(hasattr(context_received.config_metadata, "environment"))
|
|
497
|
+
# Verify the project and environment data
|
|
498
|
+
self.assertEqual(
|
|
499
|
+
context_received.config_metadata.project.id, "61f97628ff4afcb6d057dbf0"
|
|
500
|
+
)
|
|
501
|
+
self.assertEqual(context_received.config_metadata.project.key, "emma-project")
|
|
502
|
+
self.assertEqual(
|
|
503
|
+
context_received.config_metadata.environment.id, "61f97628ff4afcb6d057dbf2"
|
|
504
|
+
)
|
|
505
|
+
self.assertEqual(
|
|
506
|
+
context_received.config_metadata.environment.key, "development"
|
|
507
|
+
)
|
|
508
|
+
|
|
445
509
|
|
|
446
510
|
def _benchmark_variable_call(client: DevCycleLocalClient, user: DevCycleUser, key: str):
|
|
447
511
|
return client.variable(user, key, "default_value")
|
{devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|