Skip to content

Commit 29f432d

Browse files
committed
chore: added local client implementation for hooks
1 parent 63a9d5c commit 29f432d

File tree

6 files changed

+159
-15
lines changed

6 files changed

+159
-15
lines changed

devcycle_python_sdk/cloud_client.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
NotFoundError,
1010
CloudClientUnauthorizedError,
1111
)
12-
from devcycle_python_sdk.managers.eval_hooks_manager import EvalHooksManager, BeforeHookError, AfterHookError
12+
from devcycle_python_sdk.managers.eval_hooks_manager import (
13+
EvalHooksManager,
14+
BeforeHookError,
15+
AfterHookError,
16+
)
1317
from devcycle_python_sdk.models.eval_hook import EvalHook
1418
from devcycle_python_sdk.models.eval_hook_context import HookContext
1519
from devcycle_python_sdk.models.user import DevCycleUser
@@ -48,7 +52,9 @@ def __init__(self, sdk_key: str, options: DevCycleCloudOptions):
4852
self.sdk_type = "server"
4953
self.bucketing_api = BucketingAPIClient(sdk_key, self.options)
5054
self._openfeature_provider = DevCycleProvider(self)
51-
self.eval_hooks_manager = EvalHooksManager(None if options is None else options.eval_hooks)
55+
self.eval_hooks_manager = EvalHooksManager(
56+
None if options is None else options.eval_hooks
57+
)
5258

5359
def get_sdk_platform(self) -> str:
5460
return "Cloud"
@@ -93,8 +99,8 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable
9399

94100
context = HookContext(key, user, default_value)
95101
variable = Variable.create_default_variable(
96-
key=key, default_value=default_value
97-
)
102+
key=key, default_value=default_value
103+
)
98104

99105
try:
100106
before_hook_error = None
@@ -105,7 +111,7 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable
105111
variable = self.bucketing_api.variable(key, context.user)
106112
if before_hook_error is None:
107113
self.eval_hooks_manager.run_after(context, variable)
108-
else :
114+
else:
109115
raise before_hook_error
110116
except CloudClientUnauthorizedError as e:
111117
logger.warning("DevCycle: SDK key is invalid, unable to make cloud request")

devcycle_python_sdk/local_client.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@
88
from devcycle_python_sdk.api.local_bucketing import LocalBucketing
99
from devcycle_python_sdk.exceptions import VariableTypeMismatchError
1010
from devcycle_python_sdk.managers.config_manager import EnvironmentConfigManager
11+
from devcycle_python_sdk.managers.eval_hooks_manager import (
12+
EvalHooksManager,
13+
BeforeHookError,
14+
AfterHookError,
15+
)
1116
from devcycle_python_sdk.managers.event_queue_manager import EventQueueManager
1217
from devcycle_python_sdk.models.bucketed_config import BucketedConfig
18+
from devcycle_python_sdk.models.eval_hook import EvalHook
19+
from devcycle_python_sdk.models.eval_hook_context import HookContext
1320
from devcycle_python_sdk.models.event import DevCycleEvent, EventType
1421
from devcycle_python_sdk.models.feature import Feature
1522
from devcycle_python_sdk.models.platform_data import default_platform_data
@@ -51,6 +58,7 @@ def __init__(self, sdk_key: str, options: DevCycleLocalOptions):
5158
)
5259

5360
self._openfeature_provider: Optional[DevCycleProvider] = None
61+
self.eval_hooks_manager = EvalHooksManager(self.options.eval_hooks)
5462

5563
def get_sdk_platform(self) -> str:
5664
return "Local"
@@ -133,18 +141,44 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable
133141
)
134142
return Variable.create_default_variable(key, default_value)
135143

144+
context = HookContext(key, user, default_value)
145+
variable = Variable.create_default_variable(
146+
key=key, default_value=default_value
147+
)
148+
136149
try:
150+
before_hook_error = None
151+
try:
152+
context = self.eval_hooks_manager.run_before(context)
153+
except BeforeHookError as e:
154+
before_hook_error = e
137155
variable = self.local_bucketing.get_variable_for_user_protobuf(
138156
user, key, default_value
139157
)
140-
if variable:
141-
return variable
158+
if variable is None:
159+
variable = Variable.create_default_variable(
160+
key=key, default_value=default_value
161+
)
162+
163+
if before_hook_error is None:
164+
self.eval_hooks_manager.run_after(context, variable)
165+
else:
166+
raise before_hook_error
142167
except VariableTypeMismatchError:
143168
logger.debug("DevCycle: Variable type mismatch, returning default value")
169+
return variable
170+
except BeforeHookError as e:
171+
self.eval_hooks_manager.run_error(context, e)
172+
return variable
173+
except AfterHookError as e:
174+
self.eval_hooks_manager.run_error(context, e)
175+
return variable
144176
except Exception as e:
145177
logger.warning(f"DevCycle: Error retrieving variable for user: {e}")
146-
147-
return Variable.create_default_variable(key, default_value)
178+
return variable
179+
finally:
180+
self.eval_hooks_manager.run_finally(context, variable)
181+
return variable
148182

149183
def _generate_bucketed_config(self, user: DevCycleUser) -> BucketedConfig:
150184
"""
@@ -234,6 +268,12 @@ def close(self) -> None:
234268
self.config_manager.close()
235269
self.event_queue_manager.close()
236270

271+
def add_hook(self, eval_hook: EvalHook) -> None:
272+
self.eval_hooks_manager.add_hook(eval_hook)
273+
274+
def clear_hooks(self) -> None:
275+
self.eval_hooks_manager.clear_hooks()
276+
237277

238278
def _validate_sdk_key(sdk_key: str) -> None:
239279
if sdk_key is None or len(sdk_key) == 0:

devcycle_python_sdk/managers/eval_hooks_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
class BeforeHookError(Exception):
1010
"""Exception raised when a before hook fails"""
11+
1112
def __init__(self, message: str, original_error: Exception = None):
1213
self.message = message
1314
self.original_error = original_error
@@ -16,6 +17,7 @@ def __init__(self, message: str, original_error: Exception = None):
1617

1718
class AfterHookError(Exception):
1819
"""Exception raised when an after hook fails"""
20+
1921
def __init__(self, message: str, original_error: Exception = None):
2022
self.message = message
2123
self.original_error = original_error
@@ -73,4 +75,3 @@ def run_error(self, context: HookContext, error: Exception) -> None:
7375
hook.error(context, error)
7476
except Exception as e:
7577
logger.error(f"Error running error hook: {e}")
76-

devcycle_python_sdk/models/eval_hook.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
from devcycle_python_sdk.models.eval_hook_context import HookContext
44
from devcycle_python_sdk.models.variable import Variable
55

6+
67
class EvalHook:
78
def __init__(
89
self,
910
before: Callable[[HookContext], Optional[HookContext]] = None,
1011
after: Callable[[HookContext, Variable], None] = None,
1112
on_finally: Callable[[HookContext, Optional[Variable]], None] = None,
12-
error: Callable[[HookContext, Exception], None] = None
13+
error: Callable[[HookContext, Exception], None] = None,
1314
):
1415
self.before = before
1516
self.after = after

test/test_cloud_client.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,19 +289,29 @@ def test_hooks(self, mock_variable_call):
289289
_id="123", key="strKey", value=999, type=TypeEnum.NUMBER
290290
)
291291
# Test adding hooks
292-
hook_called = {"before": False, "after": False, "finally": False, "error": False}
292+
hook_called = {
293+
"before": False,
294+
"after": False,
295+
"finally": False,
296+
"error": False,
297+
}
293298

294299
def before_hook(context):
295300
hook_called["before"] = True
296301
return context
302+
297303
def after_hook(context, variable):
298304
hook_called["after"] = True
305+
299306
def finally_hook(context, variable):
300307
hook_called["finally"] = True
308+
301309
def error_hook(context, error):
302310
hook_called["error"] = True
303311

304-
self.test_client.add_hook(EvalHook(before_hook, after_hook, finally_hook, error_hook))
312+
self.test_client.add_hook(
313+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
314+
)
305315

306316
# Test hooks called during variable evaluation
307317
variable = self.test_client.variable(self.test_user, "strKey", 42)
@@ -319,19 +329,29 @@ def test_hook_exceptions(self, mock_variable_call):
319329
_id="123", key="strKey", value=999, type=TypeEnum.NUMBER
320330
)
321331
# Test adding hooks
322-
hook_called = {"before": False, "after": False, "finally": False, "error": False}
332+
hook_called = {
333+
"before": False,
334+
"after": False,
335+
"finally": False,
336+
"error": False,
337+
}
323338

324339
def before_hook(context):
325340
hook_called["before"] = True
326341
raise Exception("Before hook failed")
342+
327343
def after_hook(context, variable):
328344
hook_called["after"] = True
345+
329346
def finally_hook(context, variable):
330347
hook_called["finally"] = True
348+
331349
def error_hook(context, error):
332350
hook_called["error"] = True
333351

334-
self.test_client.add_hook(EvalHook(before_hook, after_hook, finally_hook, error_hook))
352+
self.test_client.add_hook(
353+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
354+
)
335355

336356
# Test hooks called during variable evaluation
337357
variable = self.test_client.variable(self.test_user, "strKey", 42)
@@ -343,5 +363,6 @@ def error_hook(context, error):
343363
self.assertTrue(hook_called["finally"])
344364
self.assertTrue(hook_called["error"])
345365

366+
346367
if __name__ == "__main__":
347368
unittest.main()

test/test_local_client.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from devcycle_python_sdk import DevCycleLocalClient, DevCycleLocalOptions
1010
from devcycle_python_sdk.local_client import _validate_user, _validate_sdk_key
1111
from devcycle_python_sdk.exceptions import MalformedConfigError
12+
from devcycle_python_sdk.models.eval_hook import EvalHook
1213
from devcycle_python_sdk.models.event import DevCycleEvent
1314
from devcycle_python_sdk.models.feature import Feature
1415
from devcycle_python_sdk.api.local_bucketing import LocalBucketing
@@ -361,6 +362,80 @@ def test_all_variables_exception(self, _):
361362
result = self.client.all_variables(user)
362363
self.assertEqual(result, {})
363364

365+
def test_hooks(self):
366+
self.setup_client()
367+
# Test adding hooks
368+
hook_called = {
369+
"before": False,
370+
"after": False,
371+
"finally": False,
372+
"error": False,
373+
}
374+
375+
def before_hook(context):
376+
hook_called["before"] = True
377+
return context
378+
379+
def after_hook(context, variable):
380+
hook_called["after"] = True
381+
382+
def finally_hook(context, variable):
383+
hook_called["finally"] = True
384+
385+
def error_hook(context, error):
386+
hook_called["error"] = True
387+
388+
self.client.add_hook(
389+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
390+
)
391+
392+
# Test hooks called during variable evaluation
393+
variable = self.client.variable(self.test_user, "strKey", 42)
394+
self.assertTrue(variable.value == 999)
395+
self.assertFalse(variable.isDefaulted)
396+
397+
self.assertTrue(hook_called["before"])
398+
self.assertTrue(hook_called["after"])
399+
self.assertTrue(hook_called["finally"])
400+
self.assertFalse(hook_called["error"])
401+
402+
def test_hook_exceptions(self):
403+
self.setup_client()
404+
# Test adding hooks
405+
hook_called = {
406+
"before": False,
407+
"after": False,
408+
"finally": False,
409+
"error": False,
410+
}
411+
412+
def before_hook(context):
413+
hook_called["before"] = True
414+
raise Exception("Before hook failed")
415+
416+
def after_hook(context, variable):
417+
hook_called["after"] = True
418+
419+
def finally_hook(context, variable):
420+
hook_called["finally"] = True
421+
422+
def error_hook(context, error):
423+
hook_called["error"] = True
424+
425+
self.client.add_hook(
426+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
427+
)
428+
429+
# Test hooks called during variable evaluation
430+
variable = self.client.variable(self.test_user, "strKey", 42)
431+
self.assertTrue(variable.value == 999)
432+
self.assertFalse(variable.isDefaulted)
433+
434+
self.assertTrue(hook_called["before"])
435+
self.assertFalse(hook_called["after"])
436+
self.assertTrue(hook_called["finally"])
437+
self.assertTrue(hook_called["error"])
438+
364439

365440
def _benchmark_variable_call(client: DevCycleLocalClient, user: DevCycleUser, key: str):
366441
return client.variable(user, key, "default_value")

0 commit comments

Comments
 (0)