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.
Files changed (34) hide show
  1. devcycle_python_sdk/VERSION.txt +1 -1
  2. devcycle_python_sdk/api/bucketing_client.py +12 -0
  3. devcycle_python_sdk/api/local_bucketing.py +19 -12
  4. devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
  5. devcycle_python_sdk/cloud_client.py +14 -5
  6. devcycle_python_sdk/local_client.py +41 -18
  7. devcycle_python_sdk/managers/config_manager.py +4 -0
  8. devcycle_python_sdk/managers/eval_hooks_manager.py +15 -4
  9. devcycle_python_sdk/managers/event_queue_manager.py +0 -1
  10. devcycle_python_sdk/managers/sse_manager.py +1 -1
  11. devcycle_python_sdk/models/config_metadata.py +40 -0
  12. devcycle_python_sdk/models/environment_metadata.py +26 -0
  13. devcycle_python_sdk/models/eval_hook.py +5 -2
  14. devcycle_python_sdk/models/eval_hook_context.py +23 -5
  15. devcycle_python_sdk/models/eval_reason.py +39 -0
  16. devcycle_python_sdk/models/project_metadata.py +24 -0
  17. devcycle_python_sdk/models/variable.py +44 -6
  18. devcycle_python_sdk/models/variable_metadata.py +22 -0
  19. devcycle_python_sdk/open_feature_provider/provider.py +10 -5
  20. devcycle_python_sdk/options.py +1 -1
  21. devcycle_python_sdk/protobuf/utils.py +27 -3
  22. devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +36 -31
  23. devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi +99 -77
  24. {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/METADATA +2 -2
  25. {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/RECORD +34 -29
  26. test/api/test_local_bucketing.py +17 -3
  27. test/models/test_user.py +0 -1
  28. test/openfeature_test/test_provider.py +79 -3
  29. test/openfeature_test/test_provider_local_sdk.py +25 -0
  30. test/test_cloud_client.py +46 -4
  31. test/test_local_client.py +69 -5
  32. {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/WHEEL +0 -0
  33. {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/licenses/LICENSE +0 -0
  34. {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/top_level.txt +0 -0
@@ -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
@@ -6,7 +6,6 @@ from devcycle_python_sdk.models.user import DevCycleUser
6
6
  from openfeature.provider import EvaluationContext
7
7
  from openfeature.exception import TargetingKeyMissingError, InvalidContextError
8
8
 
9
-
10
9
  logger = logging.getLogger(__name__)
11
10
 
12
11
 
@@ -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", default_value=False
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.TARGETING_MATCH)
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")