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.
Files changed (30) 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 +0 -8
  4. devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
  5. devcycle_python_sdk/cloud_client.py +51 -4
  6. devcycle_python_sdk/local_client.py +65 -14
  7. devcycle_python_sdk/managers/eval_hooks_manager.py +73 -0
  8. devcycle_python_sdk/managers/sse_manager.py +1 -1
  9. devcycle_python_sdk/models/bucketed_config.py +0 -1
  10. devcycle_python_sdk/models/eval_hook.py +18 -0
  11. devcycle_python_sdk/models/eval_hook_context.py +10 -0
  12. devcycle_python_sdk/models/eval_reason.py +40 -0
  13. devcycle_python_sdk/models/user.py +16 -4
  14. devcycle_python_sdk/models/variable.py +44 -6
  15. devcycle_python_sdk/open_feature_provider/provider.py +10 -5
  16. devcycle_python_sdk/options.py +8 -1
  17. devcycle_python_sdk/protobuf/utils.py +18 -0
  18. devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +43 -30
  19. devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi +100 -78
  20. {devcycle_python_server_sdk-3.11.3.dist-info → devcycle_python_server_sdk-3.12.0.dist-info}/METADATA +1 -1
  21. {devcycle_python_server_sdk-3.11.3.dist-info → devcycle_python_server_sdk-3.12.0.dist-info}/RECORD +30 -26
  22. test/api/test_local_bucketing.py +11 -0
  23. test/models/test_user.py +90 -0
  24. test/openfeature_test/test_provider.py +79 -2
  25. test/openfeature_test/test_provider_local_sdk.py +25 -0
  26. test/test_cloud_client.py +89 -0
  27. test/test_local_client.py +97 -1
  28. {devcycle_python_server_sdk-3.11.3.dist-info → devcycle_python_server_sdk-3.12.0.dist-info}/WHEEL +0 -0
  29. {devcycle_python_server_sdk-3.11.3.dist-info → devcycle_python_server_sdk-3.12.0.dist-info}/licenses/LICENSE +0 -0
  30. {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", default_value=False
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.TARGETING_MATCH)
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")