Skip to content

Commit 236c2dd

Browse files
committed
feat: add variable metadata in after and finally hooks
1 parent 519c414 commit 236c2dd

File tree

11 files changed

+110
-50
lines changed

11 files changed

+110
-50
lines changed

devcycle_python_sdk/api/local_bucketing.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from pathlib import Path
77
from threading import Lock
8-
from typing import Any, cast, Optional, List
8+
from typing import Any, cast, Optional, List, Tuple
99

1010
import wasmtime
1111
from wasmtime import (
@@ -300,7 +300,7 @@ def init_event_queue(self, client_uuid, options_json: str) -> None:
300300

301301
def get_variable_for_user_protobuf(
302302
self, user: DevCycleUser, key: str, default_value: Any
303-
) -> Optional[Variable]:
303+
) -> Tuple[Optional[Variable], Optional[str]]:
304304
var_type = determine_variable_type(default_value)
305305
pb_variable_type = pb_utils.convert_type_enum_to_variable_type(var_type)
306306

@@ -325,7 +325,7 @@ def get_variable_for_user_protobuf(
325325
sdk_variable = pb2.SDKVariable_PB()
326326
sdk_variable.ParseFromString(var_bytes)
327327

328-
return pb_utils.create_variable(sdk_variable, default_value)
328+
return pb_utils.create_variable(sdk_variable, default_value), sdk_variable._feature.value
329329

330330
def generate_bucketed_config(self, user: DevCycleUser) -> BucketedConfig:
331331
user_json = json.dumps(user.to_json())

devcycle_python_sdk/local_client.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from devcycle_python_sdk.models.platform_data import default_platform_data
2727
from devcycle_python_sdk.models.user import DevCycleUser
2828
from devcycle_python_sdk.models.variable import Variable
29+
from devcycle_python_sdk.models.variable_metadata import VariableMetadata
2930
from devcycle_python_sdk.open_feature_provider.provider import DevCycleProvider
3031
from openfeature.provider import AbstractProvider
3132

@@ -151,6 +152,7 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable
151152
)
152153

153154
config_metadata = self.local_bucketing.get_config_metadata()
155+
variable_metadata = None
154156

155157
context = HookContext(key, user, default_value, config_metadata)
156158
variable = Variable.create_default_variable(
@@ -165,9 +167,11 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable
165167
context = changed_context
166168
except BeforeHookError as e:
167169
before_hook_error = e
168-
bucketed_variable = self.local_bucketing.get_variable_for_user_protobuf(
170+
bucketed_variable, feature_id = self.local_bucketing.get_variable_for_user_protobuf(
169171
user, key, default_value
170172
)
173+
if feature_id is not None:
174+
variable_metadata = VariableMetadata(feature_id=feature_id)
171175
if bucketed_variable is not None:
172176
variable = bucketed_variable
173177
else:
@@ -177,7 +181,7 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable
177181
)
178182

179183
if before_hook_error is None:
180-
self.eval_hooks_manager.run_after(context, variable)
184+
self.eval_hooks_manager.run_after(context, variable, variable_metadata)
181185
else:
182186
raise before_hook_error
183187
except Exception as e:
@@ -194,7 +198,7 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable
194198

195199
return variable
196200
finally:
197-
self.eval_hooks_manager.run_finally(context, variable)
201+
self.eval_hooks_manager.run_finally(context, variable, variable_metadata)
198202
return variable
199203

200204
def _generate_bucketed_config(self, user: DevCycleUser) -> BucketedConfig:

devcycle_python_sdk/managers/eval_hooks_manager.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from devcycle_python_sdk.models.eval_hook import EvalHook
44
from devcycle_python_sdk.models.eval_hook_context import HookContext
55
from devcycle_python_sdk.models.variable import Variable
6+
from devcycle_python_sdk.models.variable_metadata import VariableMetadata
67
from devcycle_python_sdk.options import logger
78

89

@@ -48,19 +49,19 @@ def run_before(self, context: HookContext) -> Optional[HookContext]:
4849
raise BeforeHookError(f"Before hook failed: {e}", e)
4950
return modified_context
5051

51-
def run_after(self, context: HookContext, variable: Variable) -> None:
52+
def run_after(self, context: HookContext, variable: Variable, variable_metadata: Optional[VariableMetadata]) -> None:
5253
"""Run after hooks with the evaluation result"""
5354
for hook in self.hooks:
5455
try:
55-
hook.after(context, variable)
56+
hook.after(context, variable, variable_metadata)
5657
except Exception as e:
5758
raise AfterHookError(f"After hook failed: {e}", e)
5859

59-
def run_finally(self, context: HookContext, variable: Optional[Variable]) -> None:
60+
def run_finally(self, context: HookContext, variable: Optional[Variable], variable_metadata: Optional[VariableMetadata]) -> None:
6061
"""Run finally hooks after evaluation completes"""
6162
for hook in self.hooks:
6263
try:
63-
hook.on_finally(context, variable)
64+
hook.on_finally(context, variable, variable_metadata)
6465
except Exception as e:
6566
logger.error(f"Error running finally hook: {e}")
6667

devcycle_python_sdk/models/config_metadata.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
from devcycle_python_sdk.models.environment_metadata import EnvironmentMetadata
22
from devcycle_python_sdk.models.project_metadata import ProjectMetadata
33
from typing import Dict, Any, Optional
4+
from dataclasses import dataclass
45
import json
56

67

8+
@dataclass
79
class ConfigMetadata:
8-
def __init__(
9-
self,
10-
project: ProjectMetadata,
11-
environment: EnvironmentMetadata,
12-
):
13-
self.project = project
14-
self.environment = environment
10+
project: ProjectMetadata
11+
environment: EnvironmentMetadata
1512

16-
def to_json(self) -> str:
17-
return json.dumps(self, default=lambda o: o.__dict__)
13+
def to_json(self):
14+
result = {}
15+
for field_name in self.__dataclass_fields__:
16+
value = getattr(self, field_name)
17+
if value is not None:
18+
if field_name == "project" and isinstance(value, ProjectMetadata):
19+
result[field_name] = value.to_json()
20+
elif field_name == "environment" and isinstance(value, EnvironmentMetadata):
21+
result[field_name] = value.to_json()
22+
else:
23+
result[field_name] = value
24+
return result
1825

1926
@staticmethod
2027
def from_json(json_obj: Optional[Dict[str, Any]]) -> Optional["ConfigMetadata"]:
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
from typing import Dict, Any, Optional
2+
from dataclasses import dataclass
23

34

5+
@dataclass
46
class EnvironmentMetadata:
5-
def __init__(
6-
self,
7-
id: str,
8-
key: str,
9-
):
10-
self.id = id
11-
self.key = key
7+
id: str
8+
key: str
129

1310
@staticmethod
1411
def from_json(
@@ -20,3 +17,10 @@ def from_json(
2017
id=json_obj["id"],
2118
key=json_obj["key"],
2219
)
20+
21+
def to_json(self):
22+
result = {}
23+
for field_name in self.__dataclass_fields__:
24+
value = getattr(self, field_name)
25+
result[field_name] = value
26+
return result

devcycle_python_sdk/models/eval_hook.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
from devcycle_python_sdk.models.eval_hook_context import HookContext
44
from devcycle_python_sdk.models.variable import Variable
5+
from devcycle_python_sdk.models.variable_metadata import VariableMetadata
56

67

78
class EvalHook:
89
def __init__(
910
self,
1011
before: Callable[[HookContext], Optional[HookContext]],
11-
after: Callable[[HookContext, Variable], None],
12-
on_finally: Callable[[HookContext, Optional[Variable]], None],
12+
after: Callable[[HookContext, Variable, Optional[VariableMetadata]], None],
13+
on_finally: Callable[[HookContext, Optional[Variable], Optional[VariableMetadata]], None],
1314
error: Callable[[HookContext, Exception], None],
1415
):
1516
self.before = before
Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
11
from typing import Any, Optional
2+
from dataclasses import dataclass
23

34
from devcycle_python_sdk.models.user import DevCycleUser
45
from devcycle_python_sdk.models.config_metadata import ConfigMetadata
56

67

8+
@dataclass
79
class HookContext:
8-
def __init__(
9-
self,
10-
key: str,
11-
user: DevCycleUser,
12-
default_value: Any,
13-
config_metadata: Optional[ConfigMetadata] = None,
14-
):
15-
self.key = key
16-
self.default_value = default_value
17-
self.user = user
18-
self.config_metadata = config_metadata
10+
key: str
11+
user: DevCycleUser
12+
default_value: Any
13+
config_metadata: Optional[ConfigMetadata] = None
14+
15+
def to_json(self):
16+
result = {}
17+
for field_name in self.__dataclass_fields__:
18+
value = getattr(self, field_name)
19+
if value is not None:
20+
if field_name == "config_metadata" and isinstance(value, ConfigMetadata):
21+
result[field_name] = value.to_json()
22+
elif field_name == "user" and isinstance(value, DevCycleUser):
23+
result[field_name] = value.to_json()
24+
else:
25+
result[field_name] = value
26+
return result
27+
28+
def __str__(self):
29+
return f"HookContext(key={self.key}, user={self.user}, default_value={self.default_value}, config_metadata={self.config_metadata})"
30+
31+
def __repr__(self):
32+
return self.__str__()
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
from typing import Dict, Any, Optional
2+
from dataclasses import dataclass
23

34

5+
@dataclass
46
class ProjectMetadata:
5-
def __init__(
6-
self,
7-
id: str,
8-
key: str,
9-
):
10-
self.id = id
11-
self.key = key
7+
id: str
8+
key: str
129

1310
@staticmethod
1411
def from_json(json_obj: Optional[Dict[str, Any]]) -> Optional["ProjectMetadata"]:
@@ -18,3 +15,10 @@ def from_json(json_obj: Optional[Dict[str, Any]]) -> Optional["ProjectMetadata"]
1815
id=json_obj["id"],
1916
key=json_obj["key"],
2017
)
18+
19+
def to_json(self):
20+
result = {}
21+
for field_name in self.__dataclass_fields__:
22+
value = getattr(self, field_name)
23+
result[field_name] = value
24+
return result
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Dict, Any, Optional
2+
from dataclasses import dataclass
3+
4+
5+
@dataclass
6+
class VariableMetadata:
7+
feature_id: str
8+
9+
@staticmethod
10+
def from_json(json_obj: Optional[Dict[str, Any]]) -> Optional["VariableMetadata"]:
11+
if json_obj is None:
12+
return None
13+
return VariableMetadata(
14+
feature_id=json_obj["feature_id"],
15+
)
16+
17+
def to_json(self):
18+
result = {}
19+
for field_name in self.__dataclass_fields__:
20+
value = getattr(self, field_name)
21+
result[field_name] = value
22+
return result
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
django >= 4.2
2-
devcycle-python-server-sdk >= 3.0.1
2+
-e ../../

0 commit comments

Comments
 (0)