Skip to content

Commit 91d1357

Browse files
committed
chore: added local client implementation for hooks
1 parent cde9b06 commit 91d1357

File tree

2 files changed

+92
-4
lines changed

2 files changed

+92
-4
lines changed

devcycle_python_sdk/local_client.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
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 EvalHooksManager, BeforeHookError, AfterHookError
1112
from devcycle_python_sdk.managers.event_queue_manager import EventQueueManager
1213
from devcycle_python_sdk.models.bucketed_config import BucketedConfig
14+
from devcycle_python_sdk.models.eval_hook import EvalHook
15+
from devcycle_python_sdk.models.eval_hook_context import HookContext
1316
from devcycle_python_sdk.models.event import DevCycleEvent, EventType
1417
from devcycle_python_sdk.models.feature import Feature
1518
from devcycle_python_sdk.models.platform_data import default_platform_data
@@ -51,6 +54,7 @@ def __init__(self, sdk_key: str, options: DevCycleLocalOptions):
5154
)
5255

5356
self._openfeature_provider: Optional[DevCycleProvider] = None
57+
self.eval_hooks_manager = EvalHooksManager(options.eval_hooks)
5458

5559
def get_sdk_platform(self) -> str:
5660
return "Local"
@@ -133,18 +137,35 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable
133137
)
134138
return Variable.create_default_variable(key, default_value)
135139

140+
context = HookContext(key, user, default_value)
141+
variable = Variable.create_default_variable(
142+
key=key, default_value=default_value
143+
)
144+
136145
try:
146+
before_hook_error = None
147+
try:
148+
context = self.eval_hooks_manager.run_before(context)
149+
except BeforeHookError as e:
150+
before_hook_error = e
137151
variable = self.local_bucketing.get_variable_for_user_protobuf(
138152
user, key, default_value
139153
)
140-
if variable:
141-
return variable
154+
if before_hook_error is None:
155+
self.eval_hooks_manager.run_after(context, variable)
156+
else :
157+
raise before_hook_error
142158
except VariableTypeMismatchError:
143159
logger.debug("DevCycle: Variable type mismatch, returning default value")
160+
except BeforeHookError as e:
161+
self.eval_hooks_manager.run_error(context, e)
162+
except AfterHookError as e:
163+
self.eval_hooks_manager.run_error(context, e)
144164
except Exception as e:
145165
logger.warning(f"DevCycle: Error retrieving variable for user: {e}")
146-
147-
return Variable.create_default_variable(key, default_value)
166+
finally:
167+
self.eval_hooks_manager.run_finally(context, variable)
168+
return variable
148169

149170
def _generate_bucketed_config(self, user: DevCycleUser) -> BucketedConfig:
150171
"""
@@ -234,6 +255,12 @@ def close(self) -> None:
234255
self.config_manager.close()
235256
self.event_queue_manager.close()
236257

258+
def add_hook(self, eval_hook: EvalHook) -> None:
259+
self.eval_hooks_manager.add_hook(eval_hook)
260+
261+
def clear_hooks(self) -> None:
262+
self.eval_hooks_manager.clear_hooks()
263+
237264

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

test/test_local_client.py

Lines changed: 61 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,66 @@ 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 = {"before": False, "after": False, "finally": False, "error": False}
369+
370+
def before_hook(context):
371+
hook_called["before"] = True
372+
return context
373+
374+
def after_hook(context, variable):
375+
hook_called["after"] = True
376+
377+
def finally_hook(context, variable):
378+
hook_called["finally"] = True
379+
380+
def error_hook(context, error):
381+
hook_called["error"] = True
382+
383+
self.client.add_hook(EvalHook(before_hook, after_hook, finally_hook, error_hook))
384+
385+
# Test hooks called during variable evaluation
386+
variable = self.client.variable(self.test_user, "strKey", 42)
387+
self.assertTrue(variable.value == 999)
388+
self.assertFalse(variable.isDefaulted)
389+
390+
self.assertTrue(hook_called["before"])
391+
self.assertTrue(hook_called["after"])
392+
self.assertTrue(hook_called["finally"])
393+
self.assertFalse(hook_called["error"])
394+
395+
def test_hook_exceptions(self):
396+
self.setup_client()
397+
# Test adding hooks
398+
hook_called = {"before": False, "after": False, "finally": False, "error": False}
399+
400+
def before_hook(context):
401+
hook_called["before"] = True
402+
raise Exception("Before hook failed")
403+
404+
def after_hook(context, variable):
405+
hook_called["after"] = True
406+
407+
def finally_hook(context, variable):
408+
hook_called["finally"] = True
409+
410+
def error_hook(context, error):
411+
hook_called["error"] = True
412+
413+
self.client.add_hook(EvalHook(before_hook, after_hook, finally_hook, error_hook))
414+
415+
# Test hooks called during variable evaluation
416+
variable = self.client.variable(self.test_user, "strKey", 42)
417+
self.assertTrue(variable.value == 999)
418+
self.assertFalse(variable.isDefaulted)
419+
420+
self.assertTrue(hook_called["before"])
421+
self.assertFalse(hook_called["after"])
422+
self.assertTrue(hook_called["finally"])
423+
self.assertTrue(hook_called["error"])
424+
364425

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

0 commit comments

Comments
 (0)