devcycle-python-server-sdk 3.11.3__tar.gz → 3.11.4__tar.gz
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.
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/PKG-INFO +1 -1
- devcycle_python_server_sdk-3.11.4/devcycle_python_sdk/VERSION.txt +1 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/cloud_client.py +39 -1
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/local_client.py +45 -9
- devcycle_python_server_sdk-3.11.4/devcycle_python_sdk/managers/eval_hooks_manager.py +73 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/models/bucketed_config.py +0 -1
- devcycle_python_server_sdk-3.11.4/devcycle_python_sdk/models/eval_hook.py +18 -0
- devcycle_python_server_sdk-3.11.4/devcycle_python_sdk/models/eval_hook_context.py +10 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/models/user.py +16 -4
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/options.py +8 -1
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_server_sdk.egg-info/PKG-INFO +1 -1
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_server_sdk.egg-info/SOURCES.txt +3 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/models/test_user.py +90 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/test_cloud_client.py +82 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/test_local_client.py +81 -0
- devcycle_python_server_sdk-3.11.3/devcycle_python_sdk/VERSION.txt +0 -1
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/LICENSE +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/README.md +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/api/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/api/backoff.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/api/bucketing_client.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/api/config_client.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/api/event_client.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/api/local_bucketing.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/devcycle_client.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/exceptions.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/managers/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/managers/config_manager.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/managers/event_queue_manager.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/managers/sse_manager.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/models/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/models/error_response.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/models/event.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/models/feature.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/models/platform_data.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/models/variable.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/open_feature_provider/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/open_feature_provider/provider.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/protobuf/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/protobuf/utils.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/py.typed +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/util/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/util/strings.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/util/version.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_server_sdk.egg-info/dependency_links.txt +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_server_sdk.egg-info/requires.txt +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_server_sdk.egg-info/top_level.txt +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/example/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/example/cloud_client_example.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/example/local_bucketing_client_example.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/example/openfeature_example.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/pyproject.toml +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/setup.cfg +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/setup.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/api/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/api/test_bucketing_client.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/api/test_config_client.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/api/test_event_client.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/api/test_local_bucketing.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/fixture/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/fixture/data.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/managers/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/managers/test_config_manager.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/managers/test_event_queue_manager.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/models/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/models/test_bucketed_config.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/openfeature_test/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/openfeature_test/test_provider.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/openfeature_test/test_provider_local_sdk.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/util/__init__.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/util/test_strings.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/util/test_utils.py +0 -0
- {devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/util/test_version.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11.4
|
|
@@ -9,6 +9,13 @@ from devcycle_python_sdk.exceptions import (
|
|
|
9
9
|
NotFoundError,
|
|
10
10
|
CloudClientUnauthorizedError,
|
|
11
11
|
)
|
|
12
|
+
from devcycle_python_sdk.managers.eval_hooks_manager import (
|
|
13
|
+
EvalHooksManager,
|
|
14
|
+
BeforeHookError,
|
|
15
|
+
AfterHookError,
|
|
16
|
+
)
|
|
17
|
+
from devcycle_python_sdk.models.eval_hook import EvalHook
|
|
18
|
+
from devcycle_python_sdk.models.eval_hook_context import HookContext
|
|
12
19
|
from devcycle_python_sdk.models.user import DevCycleUser
|
|
13
20
|
from devcycle_python_sdk.models.event import DevCycleEvent
|
|
14
21
|
from devcycle_python_sdk.models.variable import Variable
|
|
@@ -45,6 +52,9 @@ class DevCycleCloudClient(AbstractDevCycleClient):
|
|
|
45
52
|
self.sdk_type = "server"
|
|
46
53
|
self.bucketing_api = BucketingAPIClient(sdk_key, self.options)
|
|
47
54
|
self._openfeature_provider = DevCycleProvider(self)
|
|
55
|
+
self.eval_hooks_manager = EvalHooksManager(
|
|
56
|
+
None if options is None else options.eval_hooks
|
|
57
|
+
)
|
|
48
58
|
|
|
49
59
|
def get_sdk_platform(self) -> str:
|
|
50
60
|
return "Cloud"
|
|
@@ -87,8 +97,24 @@ class DevCycleCloudClient(AbstractDevCycleClient):
|
|
|
87
97
|
if default_value is None:
|
|
88
98
|
raise ValueError("Missing parameter: defaultValue")
|
|
89
99
|
|
|
100
|
+
context = HookContext(key, user, default_value)
|
|
101
|
+
variable = Variable.create_default_variable(
|
|
102
|
+
key=key, default_value=default_value
|
|
103
|
+
)
|
|
104
|
+
|
|
90
105
|
try:
|
|
91
|
-
|
|
106
|
+
before_hook_error = None
|
|
107
|
+
try:
|
|
108
|
+
changed_context = self.eval_hooks_manager.run_before(context)
|
|
109
|
+
if changed_context is not None:
|
|
110
|
+
context = changed_context
|
|
111
|
+
except BeforeHookError as e:
|
|
112
|
+
before_hook_error = e
|
|
113
|
+
variable = self.bucketing_api.variable(key, context.user)
|
|
114
|
+
if before_hook_error is None:
|
|
115
|
+
self.eval_hooks_manager.run_after(context, variable)
|
|
116
|
+
else:
|
|
117
|
+
raise before_hook_error
|
|
92
118
|
except CloudClientUnauthorizedError as e:
|
|
93
119
|
logger.warning("DevCycle: SDK key is invalid, unable to make cloud request")
|
|
94
120
|
raise e
|
|
@@ -97,11 +123,17 @@ class DevCycleCloudClient(AbstractDevCycleClient):
|
|
|
97
123
|
return Variable.create_default_variable(
|
|
98
124
|
key=key, default_value=default_value
|
|
99
125
|
)
|
|
126
|
+
except BeforeHookError as e:
|
|
127
|
+
self.eval_hooks_manager.run_error(context, e)
|
|
128
|
+
except AfterHookError as e:
|
|
129
|
+
self.eval_hooks_manager.run_error(context, e)
|
|
100
130
|
except Exception as e:
|
|
101
131
|
logger.error(f"DevCycle: Error evaluating variable: {e}")
|
|
102
132
|
return Variable.create_default_variable(
|
|
103
133
|
key=key, default_value=default_value
|
|
104
134
|
)
|
|
135
|
+
finally:
|
|
136
|
+
self.eval_hooks_manager.run_finally(context, variable)
|
|
105
137
|
|
|
106
138
|
variable.defaultValue = default_value
|
|
107
139
|
|
|
@@ -189,6 +221,12 @@ class DevCycleCloudClient(AbstractDevCycleClient):
|
|
|
189
221
|
# Cloud client doesn't need to release any resources
|
|
190
222
|
logger.debug("DevCycle: Cloud client closed")
|
|
191
223
|
|
|
224
|
+
def add_hook(self, hook: EvalHook) -> None:
|
|
225
|
+
self.eval_hooks_manager.add_hook(hook)
|
|
226
|
+
|
|
227
|
+
def clear_hooks(self) -> None:
|
|
228
|
+
self.eval_hooks_manager.clear_hooks()
|
|
229
|
+
|
|
192
230
|
|
|
193
231
|
def _validate_sdk_key(sdk_key: str) -> None:
|
|
194
232
|
if sdk_key is None or len(sdk_key) == 0:
|
|
@@ -8,8 +8,15 @@ from devcycle_python_sdk import DevCycleLocalOptions, AbstractDevCycleClient
|
|
|
8
8
|
from devcycle_python_sdk.api.local_bucketing import LocalBucketing
|
|
9
9
|
from devcycle_python_sdk.exceptions import VariableTypeMismatchError
|
|
10
10
|
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
|
+
)
|
|
11
16
|
from devcycle_python_sdk.managers.event_queue_manager import EventQueueManager
|
|
12
17
|
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
|
|
13
20
|
from devcycle_python_sdk.models.event import DevCycleEvent, EventType
|
|
14
21
|
from devcycle_python_sdk.models.feature import Feature
|
|
15
22
|
from devcycle_python_sdk.models.platform_data import default_platform_data
|
|
@@ -51,6 +58,7 @@ class DevCycleLocalClient(AbstractDevCycleClient):
|
|
|
51
58
|
)
|
|
52
59
|
|
|
53
60
|
self._openfeature_provider: Optional[DevCycleProvider] = None
|
|
61
|
+
self.eval_hooks_manager = EvalHooksManager(self.options.eval_hooks)
|
|
54
62
|
|
|
55
63
|
def get_sdk_platform(self) -> str:
|
|
56
64
|
return "Local"
|
|
@@ -133,18 +141,44 @@ class DevCycleLocalClient(AbstractDevCycleClient):
|
|
|
133
141
|
)
|
|
134
142
|
return Variable.create_default_variable(key, default_value)
|
|
135
143
|
|
|
144
|
+
context = HookContext(key, user, default_value)
|
|
145
|
+
variable = Variable.create_default_variable(
|
|
146
|
+
key=key, default_value=default_value
|
|
147
|
+
)
|
|
148
|
+
|
|
136
149
|
try:
|
|
137
|
-
|
|
150
|
+
before_hook_error = None
|
|
151
|
+
try:
|
|
152
|
+
changed_context = self.eval_hooks_manager.run_before(context)
|
|
153
|
+
if changed_context is not None:
|
|
154
|
+
context = changed_context
|
|
155
|
+
except BeforeHookError as e:
|
|
156
|
+
before_hook_error = e
|
|
157
|
+
bucketed_variable = self.local_bucketing.get_variable_for_user_protobuf(
|
|
138
158
|
user, key, default_value
|
|
139
159
|
)
|
|
140
|
-
if
|
|
141
|
-
|
|
160
|
+
if bucketed_variable is not None:
|
|
161
|
+
variable = bucketed_variable
|
|
162
|
+
|
|
163
|
+
if before_hook_error is None:
|
|
164
|
+
self.eval_hooks_manager.run_after(context, variable)
|
|
165
|
+
else:
|
|
166
|
+
raise before_hook_error
|
|
142
167
|
except VariableTypeMismatchError:
|
|
143
168
|
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
|
|
144
176
|
except Exception as e:
|
|
145
177
|
logger.warning(f"DevCycle: Error retrieving variable for user: {e}")
|
|
146
|
-
|
|
147
|
-
|
|
178
|
+
return variable
|
|
179
|
+
finally:
|
|
180
|
+
self.eval_hooks_manager.run_finally(context, variable)
|
|
181
|
+
return variable
|
|
148
182
|
|
|
149
183
|
def _generate_bucketed_config(self, user: DevCycleUser) -> BucketedConfig:
|
|
150
184
|
"""
|
|
@@ -169,8 +203,6 @@ class DevCycleLocalClient(AbstractDevCycleClient):
|
|
|
169
203
|
)
|
|
170
204
|
return {}
|
|
171
205
|
|
|
172
|
-
variable_map: Dict[str, Variable] = {}
|
|
173
|
-
|
|
174
206
|
try:
|
|
175
207
|
return self.local_bucketing.generate_bucketed_config(user).variables
|
|
176
208
|
except Exception as e:
|
|
@@ -179,8 +211,6 @@ class DevCycleLocalClient(AbstractDevCycleClient):
|
|
|
179
211
|
)
|
|
180
212
|
return {}
|
|
181
213
|
|
|
182
|
-
return variable_map
|
|
183
|
-
|
|
184
214
|
def all_features(self, user: DevCycleUser) -> Dict[str, Feature]:
|
|
185
215
|
"""
|
|
186
216
|
Returns all segmented and bucketed features for a user. This method will return an empty map if the client has not been initialized or if the user is not bucketed into any features
|
|
@@ -234,6 +264,12 @@ class DevCycleLocalClient(AbstractDevCycleClient):
|
|
|
234
264
|
self.config_manager.close()
|
|
235
265
|
self.event_queue_manager.close()
|
|
236
266
|
|
|
267
|
+
def add_hook(self, eval_hook: EvalHook) -> None:
|
|
268
|
+
self.eval_hooks_manager.add_hook(eval_hook)
|
|
269
|
+
|
|
270
|
+
def clear_hooks(self) -> None:
|
|
271
|
+
self.eval_hooks_manager.clear_hooks()
|
|
272
|
+
|
|
237
273
|
|
|
238
274
|
def _validate_sdk_key(sdk_key: str) -> None:
|
|
239
275
|
if sdk_key is None or len(sdk_key) == 0:
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from devcycle_python_sdk.models.eval_hook import EvalHook
|
|
4
|
+
from devcycle_python_sdk.models.eval_hook_context import HookContext
|
|
5
|
+
from devcycle_python_sdk.models.variable import Variable
|
|
6
|
+
from devcycle_python_sdk.options import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BeforeHookError(Exception):
|
|
10
|
+
"""Exception raised when a before hook fails"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message: str, original_error: Exception):
|
|
13
|
+
self.message = message
|
|
14
|
+
self.original_error = original_error
|
|
15
|
+
super().__init__(self.message)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AfterHookError(Exception):
|
|
19
|
+
"""Exception raised when an after hook fails"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, original_error: Exception):
|
|
22
|
+
self.message = message
|
|
23
|
+
self.original_error = original_error
|
|
24
|
+
super().__init__(self.message)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EvalHooksManager:
|
|
28
|
+
def __init__(self, hooks: Optional[List[EvalHook]] = None):
|
|
29
|
+
self.hooks: List[EvalHook] = hooks if hooks is not None else []
|
|
30
|
+
|
|
31
|
+
def add_hook(self, hook: EvalHook) -> None:
|
|
32
|
+
"""Add an evaluation hook to be executed"""
|
|
33
|
+
self.hooks.append(hook)
|
|
34
|
+
|
|
35
|
+
def clear_hooks(self) -> None:
|
|
36
|
+
"""Clear all evaluation hooks"""
|
|
37
|
+
self.hooks = []
|
|
38
|
+
|
|
39
|
+
def run_before(self, context: HookContext) -> Optional[HookContext]:
|
|
40
|
+
"""Run before hooks and return modified context if any"""
|
|
41
|
+
modified_context = context
|
|
42
|
+
for hook in self.hooks:
|
|
43
|
+
try:
|
|
44
|
+
result = hook.before(modified_context)
|
|
45
|
+
if result:
|
|
46
|
+
modified_context = result
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise BeforeHookError(f"Before hook failed: {e}", e)
|
|
49
|
+
return modified_context
|
|
50
|
+
|
|
51
|
+
def run_after(self, context: HookContext, variable: Variable) -> None:
|
|
52
|
+
"""Run after hooks with the evaluation result"""
|
|
53
|
+
for hook in self.hooks:
|
|
54
|
+
try:
|
|
55
|
+
hook.after(context, variable)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
raise AfterHookError(f"After hook failed: {e}", e)
|
|
58
|
+
|
|
59
|
+
def run_finally(self, context: HookContext, variable: Optional[Variable]) -> None:
|
|
60
|
+
"""Run finally hooks after evaluation completes"""
|
|
61
|
+
for hook in self.hooks:
|
|
62
|
+
try:
|
|
63
|
+
hook.on_finally(context, variable)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Error running finally hook: {e}")
|
|
66
|
+
|
|
67
|
+
def run_error(self, context: HookContext, error: Exception) -> None:
|
|
68
|
+
"""Run error hooks when an error occurs"""
|
|
69
|
+
for hook in self.hooks:
|
|
70
|
+
try:
|
|
71
|
+
hook.error(context, error)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Error running error hook: {e}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Callable, Optional
|
|
2
|
+
|
|
3
|
+
from devcycle_python_sdk.models.eval_hook_context import HookContext
|
|
4
|
+
from devcycle_python_sdk.models.variable import Variable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EvalHook:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
before: Callable[[HookContext], Optional[HookContext]],
|
|
11
|
+
after: Callable[[HookContext, Variable], None],
|
|
12
|
+
on_finally: Callable[[HookContext, Optional[Variable]], None],
|
|
13
|
+
error: Callable[[HookContext, Exception], None],
|
|
14
|
+
):
|
|
15
|
+
self.before = before
|
|
16
|
+
self.after = after
|
|
17
|
+
self.on_finally = on_finally
|
|
18
|
+
self.error = error
|
|
@@ -101,22 +101,33 @@ class DevCycleUser:
|
|
|
101
101
|
) -> "DevCycleUser":
|
|
102
102
|
"""
|
|
103
103
|
Builds a DevCycleUser instance from the evaluation context. Will raise a TargetingKeyMissingError if
|
|
104
|
-
the context does not contain a valid targeting key or
|
|
104
|
+
the context does not contain a valid targeting key, user_id, or userId attribute
|
|
105
105
|
|
|
106
106
|
:param context: The evaluation context to build the user from
|
|
107
107
|
:return: A DevCycleUser instance
|
|
108
108
|
"""
|
|
109
109
|
user_id = None
|
|
110
|
+
user_id_source = None
|
|
110
111
|
|
|
111
112
|
if context:
|
|
112
113
|
if context.targeting_key:
|
|
113
114
|
user_id = context.targeting_key
|
|
115
|
+
user_id_source = "targeting_key"
|
|
114
116
|
elif context.attributes and "user_id" in context.attributes.keys():
|
|
115
117
|
user_id = context.attributes["user_id"]
|
|
118
|
+
user_id_source = "user_id"
|
|
119
|
+
elif context.attributes and "userId" in context.attributes.keys():
|
|
120
|
+
user_id = context.attributes["userId"]
|
|
121
|
+
user_id_source = "userId"
|
|
116
122
|
|
|
117
|
-
if not user_id
|
|
123
|
+
if not user_id:
|
|
118
124
|
raise TargetingKeyMissingError(
|
|
119
|
-
"DevCycle: Evaluation context does not contain a valid targeting key or
|
|
125
|
+
"DevCycle: Evaluation context does not contain a valid targeting key, user_id, or userId attribute"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if not isinstance(user_id, str):
|
|
129
|
+
raise TargetingKeyMissingError(
|
|
130
|
+
f"DevCycle: {user_id_source} must be a string, got {type(user_id).__name__}"
|
|
120
131
|
)
|
|
121
132
|
|
|
122
133
|
user = DevCycleUser(user_id=user_id)
|
|
@@ -124,7 +135,8 @@ class DevCycleUser:
|
|
|
124
135
|
private_custom_data: Dict[str, Any] = {}
|
|
125
136
|
if context and context.attributes:
|
|
126
137
|
for key, value in context.attributes.items():
|
|
127
|
-
|
|
138
|
+
# Skip user_id, userId, and targeting_key - these are reserved for user ID mapping
|
|
139
|
+
if key in ("user_id", "userId", "targeting_key"):
|
|
128
140
|
continue
|
|
129
141
|
|
|
130
142
|
if value is not None:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Callable, Optional, Dict, Any
|
|
2
|
+
from typing import Callable, Optional, Dict, Any, List
|
|
3
|
+
|
|
4
|
+
from devcycle_python_sdk.models.eval_hook import EvalHook
|
|
3
5
|
|
|
4
6
|
logger = logging.getLogger(__name__)
|
|
5
7
|
|
|
@@ -16,12 +18,14 @@ class DevCycleCloudOptions:
|
|
|
16
18
|
request_timeout: int = 5, # seconds
|
|
17
19
|
request_retries: int = 5,
|
|
18
20
|
retry_delay: int = 200, # milliseconds
|
|
21
|
+
eval_hooks: Optional[List[EvalHook]] = None,
|
|
19
22
|
):
|
|
20
23
|
self.enable_edge_db = enable_edge_db
|
|
21
24
|
self.bucketing_api_uri = bucketing_api_uri
|
|
22
25
|
self.request_timeout = request_timeout
|
|
23
26
|
self.request_retries = request_retries
|
|
24
27
|
self.retry_delay = retry_delay
|
|
28
|
+
self.eval_hooks = eval_hooks if eval_hooks is not None else []
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
class DevCycleLocalOptions:
|
|
@@ -47,6 +51,7 @@ class DevCycleLocalOptions:
|
|
|
47
51
|
disable_custom_event_logging: bool = False,
|
|
48
52
|
enable_beta_realtime_updates: bool = False,
|
|
49
53
|
disable_realtime_updates: bool = False,
|
|
54
|
+
eval_hooks: Optional[List[EvalHook]] = None,
|
|
50
55
|
):
|
|
51
56
|
self.events_api_uri = events_api_uri
|
|
52
57
|
self.config_cdn_uri = config_cdn_uri
|
|
@@ -69,6 +74,8 @@ class DevCycleLocalOptions:
|
|
|
69
74
|
"DevCycle: `enable_beta_realtime_updates` is deprecated and will be removed in a future release.",
|
|
70
75
|
)
|
|
71
76
|
|
|
77
|
+
self.eval_hooks = eval_hooks if eval_hooks is not None else []
|
|
78
|
+
|
|
72
79
|
if self.flush_event_queue_size >= self.max_event_queue_size:
|
|
73
80
|
logger.warning(
|
|
74
81
|
f"DevCycle: flush_event_queue_size: {self.flush_event_queue_size} must be smaller than max_event_queue_size: {self.max_event_queue_size}"
|
|
@@ -20,11 +20,14 @@ devcycle_python_sdk/api/event_client.py
|
|
|
20
20
|
devcycle_python_sdk/api/local_bucketing.py
|
|
21
21
|
devcycle_python_sdk/managers/__init__.py
|
|
22
22
|
devcycle_python_sdk/managers/config_manager.py
|
|
23
|
+
devcycle_python_sdk/managers/eval_hooks_manager.py
|
|
23
24
|
devcycle_python_sdk/managers/event_queue_manager.py
|
|
24
25
|
devcycle_python_sdk/managers/sse_manager.py
|
|
25
26
|
devcycle_python_sdk/models/__init__.py
|
|
26
27
|
devcycle_python_sdk/models/bucketed_config.py
|
|
27
28
|
devcycle_python_sdk/models/error_response.py
|
|
29
|
+
devcycle_python_sdk/models/eval_hook.py
|
|
30
|
+
devcycle_python_sdk/models/eval_hook_context.py
|
|
28
31
|
devcycle_python_sdk/models/event.py
|
|
29
32
|
devcycle_python_sdk/models/feature.py
|
|
30
33
|
devcycle_python_sdk/models/platform_data.py
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/models/test_user.py
RENAMED
|
@@ -53,6 +53,96 @@ class DevCycleUserTest(unittest.TestCase):
|
|
|
53
53
|
self.assertIsNotNone(user)
|
|
54
54
|
self.assertEqual(user.user_id, test_user_id)
|
|
55
55
|
|
|
56
|
+
def test_create_user_from_context_with_user_id_attribute(self):
|
|
57
|
+
"""Comprehensive test for userId attribute support including priority, functionality, and error cases"""
|
|
58
|
+
targeting_key_id = "targeting-12345"
|
|
59
|
+
user_id = "user_id-12345"
|
|
60
|
+
user_id_attr = "userId-12345"
|
|
61
|
+
|
|
62
|
+
# Test 1: userId as the only user ID source
|
|
63
|
+
context = EvaluationContext(
|
|
64
|
+
targeting_key=None, attributes={"userId": user_id_attr}
|
|
65
|
+
)
|
|
66
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
67
|
+
self.assertIsNotNone(user)
|
|
68
|
+
self.assertEqual(user.user_id, user_id_attr)
|
|
69
|
+
|
|
70
|
+
# Test 2: Priority order - targeting_key > user_id > userId
|
|
71
|
+
context = EvaluationContext(
|
|
72
|
+
targeting_key=targeting_key_id,
|
|
73
|
+
attributes={"user_id": user_id, "userId": user_id_attr},
|
|
74
|
+
)
|
|
75
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
76
|
+
self.assertEqual(user.user_id, targeting_key_id)
|
|
77
|
+
|
|
78
|
+
context = EvaluationContext(
|
|
79
|
+
targeting_key=None, attributes={"user_id": user_id, "userId": user_id_attr}
|
|
80
|
+
)
|
|
81
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
82
|
+
self.assertEqual(user.user_id, user_id)
|
|
83
|
+
|
|
84
|
+
# Test 3: Error cases - invalid userId types
|
|
85
|
+
with self.assertRaises(TargetingKeyMissingError):
|
|
86
|
+
DevCycleUser.create_user_from_context(
|
|
87
|
+
EvaluationContext(targeting_key=None, attributes={"userId": 12345})
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
with self.assertRaises(TargetingKeyMissingError):
|
|
91
|
+
DevCycleUser.create_user_from_context(
|
|
92
|
+
EvaluationContext(targeting_key=None, attributes={"userId": None})
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def test_create_user_from_context_user_id_exclusion(self):
|
|
96
|
+
"""Test that all user ID fields (targeting_key, user_id, userId) are excluded from custom data"""
|
|
97
|
+
targeting_key_id = "targeting-12345"
|
|
98
|
+
user_id = "user_id-12345"
|
|
99
|
+
user_id_attr = "userId-12345"
|
|
100
|
+
|
|
101
|
+
# Test exclusion when targeting_key is used
|
|
102
|
+
context = EvaluationContext(
|
|
103
|
+
targeting_key=targeting_key_id,
|
|
104
|
+
attributes={
|
|
105
|
+
"user_id": user_id,
|
|
106
|
+
"userId": user_id_attr,
|
|
107
|
+
"targeting_key": "should-be-excluded",
|
|
108
|
+
"other_field": "value",
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
112
|
+
self.assertEqual(user.user_id, targeting_key_id)
|
|
113
|
+
self.assertIsNotNone(user.customData)
|
|
114
|
+
self.assertNotIn("user_id", user.customData)
|
|
115
|
+
self.assertNotIn("userId", user.customData)
|
|
116
|
+
self.assertNotIn("targeting_key", user.customData)
|
|
117
|
+
self.assertEqual(user.customData["other_field"], "value")
|
|
118
|
+
|
|
119
|
+
# Test exclusion when user_id is used
|
|
120
|
+
context = EvaluationContext(
|
|
121
|
+
targeting_key=None,
|
|
122
|
+
attributes={
|
|
123
|
+
"user_id": user_id,
|
|
124
|
+
"userId": user_id_attr,
|
|
125
|
+
"other_field": "value",
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
129
|
+
self.assertEqual(user.user_id, user_id)
|
|
130
|
+
self.assertIsNotNone(user.customData)
|
|
131
|
+
self.assertNotIn("user_id", user.customData)
|
|
132
|
+
self.assertNotIn("userId", user.customData)
|
|
133
|
+
self.assertEqual(user.customData["other_field"], "value")
|
|
134
|
+
|
|
135
|
+
# Test exclusion when userId is used
|
|
136
|
+
context = EvaluationContext(
|
|
137
|
+
targeting_key=None,
|
|
138
|
+
attributes={"userId": user_id_attr, "other_field": "value"},
|
|
139
|
+
)
|
|
140
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
141
|
+
self.assertEqual(user.user_id, user_id_attr)
|
|
142
|
+
self.assertIsNotNone(user.customData)
|
|
143
|
+
self.assertNotIn("userId", user.customData)
|
|
144
|
+
self.assertEqual(user.customData["other_field"], "value")
|
|
145
|
+
|
|
56
146
|
def test_create_user_from_context_with_attributes(self):
|
|
57
147
|
test_user_id = "12345"
|
|
58
148
|
context = EvaluationContext(
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/test_cloud_client.py
RENAMED
|
@@ -6,6 +6,7 @@ from time import time
|
|
|
6
6
|
from unittest.mock import patch
|
|
7
7
|
|
|
8
8
|
from devcycle_python_sdk import DevCycleCloudClient, DevCycleCloudOptions
|
|
9
|
+
from devcycle_python_sdk.models.eval_hook import EvalHook
|
|
9
10
|
from devcycle_python_sdk.models.user import DevCycleUser
|
|
10
11
|
from devcycle_python_sdk.models.variable import Variable, TypeEnum
|
|
11
12
|
from devcycle_python_sdk.models.event import DevCycleEvent
|
|
@@ -27,6 +28,7 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
27
28
|
self.test_user_empty_id = DevCycleUser(user_id="")
|
|
28
29
|
|
|
29
30
|
def tearDown(self) -> None:
|
|
31
|
+
self.test_client.clear_hooks()
|
|
30
32
|
pass
|
|
31
33
|
|
|
32
34
|
def test_create_client_invalid_sdk_key(self):
|
|
@@ -281,6 +283,86 @@ class DevCycleCloudClientTest(unittest.TestCase):
|
|
|
281
283
|
),
|
|
282
284
|
)
|
|
283
285
|
|
|
286
|
+
@patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable")
|
|
287
|
+
def test_hooks(self, mock_variable_call):
|
|
288
|
+
mock_variable_call.return_value = Variable(
|
|
289
|
+
_id="123", key="strKey", value=999, type=TypeEnum.NUMBER
|
|
290
|
+
)
|
|
291
|
+
# Test adding hooks
|
|
292
|
+
hook_called = {
|
|
293
|
+
"before": False,
|
|
294
|
+
"after": False,
|
|
295
|
+
"finally": False,
|
|
296
|
+
"error": False,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
def before_hook(context):
|
|
300
|
+
hook_called["before"] = True
|
|
301
|
+
return context
|
|
302
|
+
|
|
303
|
+
def after_hook(context, variable):
|
|
304
|
+
hook_called["after"] = True
|
|
305
|
+
|
|
306
|
+
def finally_hook(context, variable):
|
|
307
|
+
hook_called["finally"] = True
|
|
308
|
+
|
|
309
|
+
def error_hook(context, error):
|
|
310
|
+
hook_called["error"] = True
|
|
311
|
+
|
|
312
|
+
self.test_client.add_hook(
|
|
313
|
+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Test hooks called during variable evaluation
|
|
317
|
+
variable = self.test_client.variable(self.test_user, "strKey", 42)
|
|
318
|
+
self.assertTrue(variable.value == 999)
|
|
319
|
+
self.assertFalse(variable.isDefaulted)
|
|
320
|
+
|
|
321
|
+
self.assertTrue(hook_called["before"])
|
|
322
|
+
self.assertTrue(hook_called["after"])
|
|
323
|
+
self.assertTrue(hook_called["finally"])
|
|
324
|
+
self.assertFalse(hook_called["error"])
|
|
325
|
+
|
|
326
|
+
@patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable")
|
|
327
|
+
def test_hook_exceptions(self, mock_variable_call):
|
|
328
|
+
mock_variable_call.return_value = Variable(
|
|
329
|
+
_id="123", key="strKey", value=999, type=TypeEnum.NUMBER
|
|
330
|
+
)
|
|
331
|
+
# Test adding hooks
|
|
332
|
+
hook_called = {
|
|
333
|
+
"before": False,
|
|
334
|
+
"after": False,
|
|
335
|
+
"finally": False,
|
|
336
|
+
"error": False,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
def before_hook(context):
|
|
340
|
+
hook_called["before"] = True
|
|
341
|
+
raise Exception("Before hook failed")
|
|
342
|
+
|
|
343
|
+
def after_hook(context, variable):
|
|
344
|
+
hook_called["after"] = True
|
|
345
|
+
|
|
346
|
+
def finally_hook(context, variable):
|
|
347
|
+
hook_called["finally"] = True
|
|
348
|
+
|
|
349
|
+
def error_hook(context, error):
|
|
350
|
+
hook_called["error"] = True
|
|
351
|
+
|
|
352
|
+
self.test_client.add_hook(
|
|
353
|
+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Test hooks called during variable evaluation
|
|
357
|
+
variable = self.test_client.variable(self.test_user, "strKey", 42)
|
|
358
|
+
self.assertTrue(variable.value == 999)
|
|
359
|
+
self.assertFalse(variable.isDefaulted)
|
|
360
|
+
|
|
361
|
+
self.assertTrue(hook_called["before"])
|
|
362
|
+
self.assertFalse(hook_called["after"])
|
|
363
|
+
self.assertTrue(hook_called["finally"])
|
|
364
|
+
self.assertTrue(hook_called["error"])
|
|
365
|
+
|
|
284
366
|
|
|
285
367
|
if __name__ == "__main__":
|
|
286
368
|
unittest.main()
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/test_local_client.py
RENAMED
|
@@ -9,6 +9,7 @@ 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
|
|
12
13
|
from devcycle_python_sdk.models.event import DevCycleEvent
|
|
13
14
|
from devcycle_python_sdk.models.feature import Feature
|
|
14
15
|
from devcycle_python_sdk.api.local_bucketing import LocalBucketing
|
|
@@ -361,6 +362,86 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
361
362
|
result = self.client.all_variables(user)
|
|
362
363
|
self.assertEqual(result, {})
|
|
363
364
|
|
|
365
|
+
@responses.activate
|
|
366
|
+
def test_hooks(self):
|
|
367
|
+
self.setup_client()
|
|
368
|
+
# Test adding hooks
|
|
369
|
+
hook_called = {
|
|
370
|
+
"before": False,
|
|
371
|
+
"after": False,
|
|
372
|
+
"finally": False,
|
|
373
|
+
"error": False,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
def before_hook(context):
|
|
377
|
+
hook_called["before"] = True
|
|
378
|
+
return context
|
|
379
|
+
|
|
380
|
+
def after_hook(context, variable):
|
|
381
|
+
hook_called["after"] = True
|
|
382
|
+
|
|
383
|
+
def finally_hook(context, variable):
|
|
384
|
+
hook_called["finally"] = True
|
|
385
|
+
|
|
386
|
+
def error_hook(context, error):
|
|
387
|
+
hook_called["error"] = True
|
|
388
|
+
|
|
389
|
+
self.client.add_hook(
|
|
390
|
+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
user = DevCycleUser(user_id="1234")
|
|
394
|
+
|
|
395
|
+
# Test hooks called during variable evaluation
|
|
396
|
+
variable = self.client.variable(user, "num-var", 42)
|
|
397
|
+
self.assertTrue(variable.value == 12345)
|
|
398
|
+
self.assertFalse(variable.isDefaulted)
|
|
399
|
+
|
|
400
|
+
self.assertTrue(hook_called["before"])
|
|
401
|
+
self.assertTrue(hook_called["after"])
|
|
402
|
+
self.assertTrue(hook_called["finally"])
|
|
403
|
+
self.assertFalse(hook_called["error"])
|
|
404
|
+
|
|
405
|
+
@responses.activate
|
|
406
|
+
def test_hook_exceptions(self):
|
|
407
|
+
self.setup_client()
|
|
408
|
+
# Test adding hooks
|
|
409
|
+
hook_called = {
|
|
410
|
+
"before": False,
|
|
411
|
+
"after": False,
|
|
412
|
+
"finally": False,
|
|
413
|
+
"error": False,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
def before_hook(context):
|
|
417
|
+
hook_called["before"] = True
|
|
418
|
+
raise Exception("Before hook failed")
|
|
419
|
+
|
|
420
|
+
def after_hook(context, variable):
|
|
421
|
+
hook_called["after"] = True
|
|
422
|
+
|
|
423
|
+
def finally_hook(context, variable):
|
|
424
|
+
hook_called["finally"] = True
|
|
425
|
+
|
|
426
|
+
def error_hook(context, error):
|
|
427
|
+
hook_called["error"] = True
|
|
428
|
+
|
|
429
|
+
self.client.add_hook(
|
|
430
|
+
EvalHook(before_hook, after_hook, finally_hook, error_hook)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
user = DevCycleUser(user_id="1234")
|
|
434
|
+
|
|
435
|
+
# Test hooks called during variable evaluation
|
|
436
|
+
variable = self.client.variable(user, "num-var", 42)
|
|
437
|
+
self.assertTrue(variable.value == 12345)
|
|
438
|
+
self.assertFalse(variable.isDefaulted)
|
|
439
|
+
|
|
440
|
+
self.assertTrue(hook_called["before"])
|
|
441
|
+
self.assertFalse(hook_called["after"])
|
|
442
|
+
self.assertTrue(hook_called["finally"])
|
|
443
|
+
self.assertTrue(hook_called["error"])
|
|
444
|
+
|
|
364
445
|
|
|
365
446
|
def _benchmark_variable_call(client: DevCycleLocalClient, user: DevCycleUser, key: str):
|
|
366
447
|
return client.variable(user, key, "default_value")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
3.11.3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/devcycle_python_sdk/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/api/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/fixture/__init__.py
RENAMED
|
File without changes
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/fixture/data.py
RENAMED
|
File without changes
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/managers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/models/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/util/__init__.py
RENAMED
|
File without changes
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/util/test_strings.py
RENAMED
|
File without changes
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/util/test_utils.py
RENAMED
|
File without changes
|
{devcycle_python_server_sdk-3.11.3 → devcycle_python_server_sdk-3.11.4}/test/util/test_version.py
RENAMED
|
File without changes
|