devcycle-python-server-sdk 3.11.4__py3-none-any.whl → 3.13.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.
- devcycle_python_sdk/VERSION.txt +1 -1
- devcycle_python_sdk/api/bucketing_client.py +12 -0
- devcycle_python_sdk/api/local_bucketing.py +19 -12
- devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
- devcycle_python_sdk/cloud_client.py +14 -5
- devcycle_python_sdk/local_client.py +41 -18
- devcycle_python_sdk/managers/config_manager.py +4 -0
- devcycle_python_sdk/managers/eval_hooks_manager.py +15 -4
- devcycle_python_sdk/managers/event_queue_manager.py +0 -1
- devcycle_python_sdk/managers/sse_manager.py +1 -1
- devcycle_python_sdk/models/config_metadata.py +40 -0
- devcycle_python_sdk/models/environment_metadata.py +26 -0
- devcycle_python_sdk/models/eval_hook.py +5 -2
- devcycle_python_sdk/models/eval_hook_context.py +23 -5
- devcycle_python_sdk/models/eval_reason.py +39 -0
- devcycle_python_sdk/models/project_metadata.py +24 -0
- devcycle_python_sdk/models/variable.py +44 -6
- devcycle_python_sdk/models/variable_metadata.py +22 -0
- devcycle_python_sdk/open_feature_provider/provider.py +10 -5
- devcycle_python_sdk/options.py +1 -1
- devcycle_python_sdk/protobuf/utils.py +27 -3
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +36 -31
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi +99 -77
- {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/METADATA +2 -2
- {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/RECORD +34 -29
- test/api/test_local_bucketing.py +17 -3
- test/models/test_user.py +0 -1
- test/openfeature_test/test_provider.py +79 -3
- test/openfeature_test/test_provider_local_sdk.py +25 -0
- test/test_cloud_client.py +46 -4
- test/test_local_client.py +69 -5
- {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/WHEEL +0 -0
- {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/licenses/LICENSE +0 -0
- {devcycle_python_server_sdk-3.11.4.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/top_level.txt +0 -0
devcycle_python_sdk/VERSION.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.13.0
|
|
@@ -15,6 +15,7 @@ from devcycle_python_sdk.models.event import DevCycleEvent
|
|
|
15
15
|
from devcycle_python_sdk.models.feature import Feature
|
|
16
16
|
from devcycle_python_sdk.models.user import DevCycleUser
|
|
17
17
|
from devcycle_python_sdk.models.variable import Variable
|
|
18
|
+
from devcycle_python_sdk.models.eval_reason import EvalReason
|
|
18
19
|
from devcycle_python_sdk.util.strings import slash_join
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
@@ -91,11 +92,17 @@ class BucketingAPIClient:
|
|
|
91
92
|
def variable(self, key: str, user: DevCycleUser) -> Variable:
|
|
92
93
|
data = self.request("POST", self._url("variables", key), json=user.to_json())
|
|
93
94
|
|
|
95
|
+
eval_data = data.get("eval")
|
|
96
|
+
eval_reason = None
|
|
97
|
+
if eval_data is not None and isinstance(eval_data, dict):
|
|
98
|
+
eval_reason = EvalReason.from_json(eval_data)
|
|
99
|
+
|
|
94
100
|
return Variable(
|
|
95
101
|
_id=data.get("_id"),
|
|
96
102
|
key=data.get("key", ""),
|
|
97
103
|
type=data.get("type", ""),
|
|
98
104
|
value=data.get("value"),
|
|
105
|
+
eval=eval_reason,
|
|
99
106
|
)
|
|
100
107
|
|
|
101
108
|
def variables(self, user: DevCycleUser) -> Dict[str, Variable]:
|
|
@@ -109,6 +116,11 @@ class BucketingAPIClient:
|
|
|
109
116
|
type=str(value.get("type")),
|
|
110
117
|
value=value.get("value"),
|
|
111
118
|
isDefaulted=None,
|
|
119
|
+
eval=(
|
|
120
|
+
EvalReason.from_json(value.get("eval"))
|
|
121
|
+
if value.get("eval")
|
|
122
|
+
else None
|
|
123
|
+
),
|
|
112
124
|
)
|
|
113
125
|
|
|
114
126
|
return result
|
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from threading import Lock
|
|
8
|
-
from typing import Any, cast, Optional, List
|
|
8
|
+
from typing import Any, cast, Optional, List, Tuple
|
|
9
9
|
|
|
10
10
|
import wasmtime
|
|
11
11
|
from wasmtime import (
|
|
@@ -22,13 +22,13 @@ from wasmtime import (
|
|
|
22
22
|
import devcycle_python_sdk.protobuf.utils as pb_utils
|
|
23
23
|
import devcycle_python_sdk.protobuf.variableForUserParams_pb2 as pb2
|
|
24
24
|
from devcycle_python_sdk.exceptions import (
|
|
25
|
-
VariableTypeMismatchError,
|
|
26
25
|
MalformedConfigError,
|
|
27
26
|
)
|
|
28
27
|
from devcycle_python_sdk.models.bucketed_config import BucketedConfig
|
|
29
28
|
from devcycle_python_sdk.models.user import DevCycleUser
|
|
30
29
|
from devcycle_python_sdk.models.variable import Variable, determine_variable_type
|
|
31
30
|
from devcycle_python_sdk.models.event import FlushPayload
|
|
31
|
+
from devcycle_python_sdk.models.config_metadata import ConfigMetadata
|
|
32
32
|
|
|
33
33
|
logger = logging.getLogger(__name__)
|
|
34
34
|
|
|
@@ -141,6 +141,7 @@ class LocalBucketing:
|
|
|
141
141
|
"generateBucketedConfigForUserUTF8"
|
|
142
142
|
)
|
|
143
143
|
self.VariableForUserProtobuf = self._get_export("variableForUser_PB")
|
|
144
|
+
self.getConfigMetadata = self._get_export("getConfigMetadata")
|
|
144
145
|
|
|
145
146
|
# Extract variable type enum values from WASM
|
|
146
147
|
self.variable_type_map = {
|
|
@@ -299,7 +300,7 @@ class LocalBucketing:
|
|
|
299
300
|
|
|
300
301
|
def get_variable_for_user_protobuf(
|
|
301
302
|
self, user: DevCycleUser, key: str, default_value: Any
|
|
302
|
-
) -> Optional[Variable]:
|
|
303
|
+
) -> Tuple[Optional[Variable], Optional[str]]:
|
|
303
304
|
var_type = determine_variable_type(default_value)
|
|
304
305
|
pb_variable_type = pb_utils.convert_type_enum_to_variable_type(var_type)
|
|
305
306
|
|
|
@@ -318,20 +319,18 @@ class LocalBucketing:
|
|
|
318
319
|
variable_addr = self.VariableForUserProtobuf(self.wasm_store, params_addr)
|
|
319
320
|
|
|
320
321
|
if variable_addr == 0:
|
|
321
|
-
return None
|
|
322
|
+
return None, None
|
|
322
323
|
else:
|
|
323
324
|
var_bytes = self._read_assembly_script_byte_array(variable_addr)
|
|
324
325
|
sdk_variable = pb2.SDKVariable_PB()
|
|
325
326
|
sdk_variable.ParseFromString(var_bytes)
|
|
327
|
+
feature_id = (
|
|
328
|
+
sdk_variable._feature.value
|
|
329
|
+
if sdk_variable._feature.value is not None
|
|
330
|
+
else None
|
|
331
|
+
)
|
|
326
332
|
|
|
327
|
-
|
|
328
|
-
# this situation should never actually happen because the WASM handles
|
|
329
|
-
# it internally and returns a null value from the WASM function
|
|
330
|
-
# This check is here just in case that logic changes in the future
|
|
331
|
-
raise VariableTypeMismatchError(
|
|
332
|
-
f"Variable returned does not match requested type: {pb_variable_type}"
|
|
333
|
-
)
|
|
334
|
-
return pb_utils.create_variable(sdk_variable, default_value)
|
|
333
|
+
return pb_utils.create_variable(sdk_variable, default_value), feature_id
|
|
335
334
|
|
|
336
335
|
def generate_bucketed_config(self, user: DevCycleUser) -> BucketedConfig:
|
|
337
336
|
user_json = json.dumps(user.to_json())
|
|
@@ -365,6 +364,14 @@ class LocalBucketing:
|
|
|
365
364
|
config_addr = self._new_assembly_script_byte_array(data)
|
|
366
365
|
self.setConfigDataUTF8(self.wasm_store, self.sdk_key_addr, config_addr)
|
|
367
366
|
|
|
367
|
+
def get_config_metadata(self) -> Optional[ConfigMetadata]:
|
|
368
|
+
with self.wasm_lock:
|
|
369
|
+
config_addr = self.getConfigMetadata(self.wasm_store, self.sdk_key_addr)
|
|
370
|
+
config_bytes = self._read_assembly_script_string(config_addr)
|
|
371
|
+
config_data = json.loads(config_bytes.encode("utf-8"))
|
|
372
|
+
|
|
373
|
+
return ConfigMetadata.from_json(config_data)
|
|
374
|
+
|
|
368
375
|
def set_platform_data(self, platform_json: str) -> None:
|
|
369
376
|
with self.wasm_lock:
|
|
370
377
|
data = platform_json.encode("utf-8")
|
|
Binary file
|
|
@@ -14,6 +14,9 @@ from devcycle_python_sdk.managers.eval_hooks_manager import (
|
|
|
14
14
|
BeforeHookError,
|
|
15
15
|
AfterHookError,
|
|
16
16
|
)
|
|
17
|
+
from devcycle_python_sdk.models.eval_reason import (
|
|
18
|
+
DefaultReasonDetails,
|
|
19
|
+
)
|
|
17
20
|
from devcycle_python_sdk.models.eval_hook import EvalHook
|
|
18
21
|
from devcycle_python_sdk.models.eval_hook_context import HookContext
|
|
19
22
|
from devcycle_python_sdk.models.user import DevCycleUser
|
|
@@ -112,7 +115,7 @@ class DevCycleCloudClient(AbstractDevCycleClient):
|
|
|
112
115
|
before_hook_error = e
|
|
113
116
|
variable = self.bucketing_api.variable(key, context.user)
|
|
114
117
|
if before_hook_error is None:
|
|
115
|
-
self.eval_hooks_manager.run_after(context, variable)
|
|
118
|
+
self.eval_hooks_manager.run_after(context, variable, None)
|
|
116
119
|
else:
|
|
117
120
|
raise before_hook_error
|
|
118
121
|
except CloudClientUnauthorizedError as e:
|
|
@@ -121,7 +124,9 @@ class DevCycleCloudClient(AbstractDevCycleClient):
|
|
|
121
124
|
except NotFoundError:
|
|
122
125
|
logger.warning(f"DevCycle: Variable not found: {key}")
|
|
123
126
|
return Variable.create_default_variable(
|
|
124
|
-
key=key,
|
|
127
|
+
key=key,
|
|
128
|
+
default_value=default_value,
|
|
129
|
+
default_reason_detail=DefaultReasonDetails.ERROR,
|
|
125
130
|
)
|
|
126
131
|
except BeforeHookError as e:
|
|
127
132
|
self.eval_hooks_manager.run_error(context, e)
|
|
@@ -130,10 +135,12 @@ class DevCycleCloudClient(AbstractDevCycleClient):
|
|
|
130
135
|
except Exception as e:
|
|
131
136
|
logger.error(f"DevCycle: Error evaluating variable: {e}")
|
|
132
137
|
return Variable.create_default_variable(
|
|
133
|
-
key=key,
|
|
138
|
+
key=key,
|
|
139
|
+
default_value=default_value,
|
|
140
|
+
default_reason_detail=DefaultReasonDetails.ERROR,
|
|
134
141
|
)
|
|
135
142
|
finally:
|
|
136
|
-
self.eval_hooks_manager.run_finally(context, variable)
|
|
143
|
+
self.eval_hooks_manager.run_finally(context, variable, None)
|
|
137
144
|
|
|
138
145
|
variable.defaultValue = default_value
|
|
139
146
|
|
|
@@ -143,7 +150,9 @@ class DevCycleCloudClient(AbstractDevCycleClient):
|
|
|
143
150
|
f"DevCycle: Variable {key} is type {type(variable.value)}, but default value is type {type(default_value)}",
|
|
144
151
|
)
|
|
145
152
|
return Variable.create_default_variable(
|
|
146
|
-
key=key,
|
|
153
|
+
key=key,
|
|
154
|
+
default_value=default_value,
|
|
155
|
+
default_reason_detail=DefaultReasonDetails.TYPE_MISMATCH,
|
|
147
156
|
)
|
|
148
157
|
|
|
149
158
|
return variable
|
|
@@ -6,7 +6,6 @@ from typing import Any, Dict, Union, Optional
|
|
|
6
6
|
|
|
7
7
|
from devcycle_python_sdk import DevCycleLocalOptions, AbstractDevCycleClient
|
|
8
8
|
from devcycle_python_sdk.api.local_bucketing import LocalBucketing
|
|
9
|
-
from devcycle_python_sdk.exceptions import VariableTypeMismatchError
|
|
10
9
|
from devcycle_python_sdk.managers.config_manager import EnvironmentConfigManager
|
|
11
10
|
from devcycle_python_sdk.managers.eval_hooks_manager import (
|
|
12
11
|
EvalHooksManager,
|
|
@@ -17,11 +16,17 @@ from devcycle_python_sdk.managers.event_queue_manager import EventQueueManager
|
|
|
17
16
|
from devcycle_python_sdk.models.bucketed_config import BucketedConfig
|
|
18
17
|
from devcycle_python_sdk.models.eval_hook import EvalHook
|
|
19
18
|
from devcycle_python_sdk.models.eval_hook_context import HookContext
|
|
19
|
+
from devcycle_python_sdk.models.eval_reason import (
|
|
20
|
+
DefaultReasonDetails,
|
|
21
|
+
EvalReason,
|
|
22
|
+
EvalReasons,
|
|
23
|
+
)
|
|
20
24
|
from devcycle_python_sdk.models.event import DevCycleEvent, EventType
|
|
21
25
|
from devcycle_python_sdk.models.feature import Feature
|
|
22
26
|
from devcycle_python_sdk.models.platform_data import default_platform_data
|
|
23
27
|
from devcycle_python_sdk.models.user import DevCycleUser
|
|
24
28
|
from devcycle_python_sdk.models.variable import Variable
|
|
29
|
+
from devcycle_python_sdk.models.variable_metadata import VariableMetadata
|
|
25
30
|
from devcycle_python_sdk.open_feature_provider.provider import DevCycleProvider
|
|
26
31
|
from openfeature.provider import AbstractProvider
|
|
27
32
|
|
|
@@ -131,7 +136,10 @@ class DevCycleLocalClient(AbstractDevCycleClient):
|
|
|
131
136
|
try:
|
|
132
137
|
self.event_queue_manager.queue_aggregate_event(
|
|
133
138
|
event=DevCycleEvent(
|
|
134
|
-
type=EventType.AggVariableDefaulted,
|
|
139
|
+
type=EventType.AggVariableDefaulted,
|
|
140
|
+
target=key,
|
|
141
|
+
value=1,
|
|
142
|
+
metaData={"evalReason": EvalReasons.DEFAULT},
|
|
135
143
|
),
|
|
136
144
|
bucketed_config=None,
|
|
137
145
|
)
|
|
@@ -139,9 +147,14 @@ class DevCycleLocalClient(AbstractDevCycleClient):
|
|
|
139
147
|
logger.warning(
|
|
140
148
|
f"DevCycle: Unable to track AggVariableDefaulted event for Variable {key}: {e}"
|
|
141
149
|
)
|
|
142
|
-
return Variable.create_default_variable(
|
|
150
|
+
return Variable.create_default_variable(
|
|
151
|
+
key, default_value, DefaultReasonDetails.MISSING_CONFIG
|
|
152
|
+
)
|
|
143
153
|
|
|
144
|
-
|
|
154
|
+
config_metadata = self.local_bucketing.get_config_metadata()
|
|
155
|
+
variable_metadata = None
|
|
156
|
+
|
|
157
|
+
context = HookContext(key, user, default_value, config_metadata)
|
|
145
158
|
variable = Variable.create_default_variable(
|
|
146
159
|
key=key, default_value=default_value
|
|
147
160
|
)
|
|
@@ -154,30 +167,40 @@ class DevCycleLocalClient(AbstractDevCycleClient):
|
|
|
154
167
|
context = changed_context
|
|
155
168
|
except BeforeHookError as e:
|
|
156
169
|
before_hook_error = e
|
|
157
|
-
bucketed_variable =
|
|
158
|
-
|
|
170
|
+
bucketed_variable, feature_id = (
|
|
171
|
+
self.local_bucketing.get_variable_for_user_protobuf(
|
|
172
|
+
user, key, default_value
|
|
173
|
+
)
|
|
159
174
|
)
|
|
175
|
+
if feature_id is not None:
|
|
176
|
+
variable_metadata = VariableMetadata(feature_id=feature_id)
|
|
160
177
|
if bucketed_variable is not None:
|
|
161
178
|
variable = bucketed_variable
|
|
179
|
+
else:
|
|
180
|
+
variable.eval = EvalReason(
|
|
181
|
+
reason=EvalReasons.DEFAULT,
|
|
182
|
+
details=DefaultReasonDetails.USER_NOT_TARGETED,
|
|
183
|
+
)
|
|
162
184
|
|
|
163
185
|
if before_hook_error is None:
|
|
164
|
-
self.eval_hooks_manager.run_after(context, variable)
|
|
186
|
+
self.eval_hooks_manager.run_after(context, variable, variable_metadata)
|
|
165
187
|
else:
|
|
166
188
|
raise before_hook_error
|
|
167
|
-
except VariableTypeMismatchError:
|
|
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
|
|
176
189
|
except Exception as e:
|
|
177
|
-
|
|
190
|
+
variable.eval = EvalReason(
|
|
191
|
+
reason=EvalReasons.DEFAULT, details=DefaultReasonDetails.ERROR
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if isinstance(e, BeforeHookError):
|
|
195
|
+
self.eval_hooks_manager.run_error(context, e)
|
|
196
|
+
elif isinstance(e, AfterHookError):
|
|
197
|
+
self.eval_hooks_manager.run_error(context, e)
|
|
198
|
+
else:
|
|
199
|
+
logger.warning(f"DevCycle: Error retrieving variable for user: {e}")
|
|
200
|
+
|
|
178
201
|
return variable
|
|
179
202
|
finally:
|
|
180
|
-
self.eval_hooks_manager.run_finally(context, variable)
|
|
203
|
+
self.eval_hooks_manager.run_finally(context, variable, variable_metadata)
|
|
181
204
|
return variable
|
|
182
205
|
|
|
183
206
|
def _generate_bucketed_config(self, user: DevCycleUser) -> BucketedConfig:
|
|
@@ -16,6 +16,7 @@ from devcycle_python_sdk.exceptions import (
|
|
|
16
16
|
from wsgiref.handlers import format_date_time
|
|
17
17
|
from devcycle_python_sdk.options import DevCycleLocalOptions
|
|
18
18
|
from devcycle_python_sdk.managers.sse_manager import SSEManager
|
|
19
|
+
from devcycle_python_sdk.models.config_metadata import ConfigMetadata
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
@@ -108,6 +109,9 @@ class EnvironmentConfigManager(threading.Thread):
|
|
|
108
109
|
)
|
|
109
110
|
self._polling_enabled = False
|
|
110
111
|
|
|
112
|
+
def get_config_metadata(self) -> Optional[ConfigMetadata]:
|
|
113
|
+
return self._local_bucketing.get_config_metadata()
|
|
114
|
+
|
|
111
115
|
def run(self):
|
|
112
116
|
while self._polling_enabled:
|
|
113
117
|
try:
|
|
@@ -3,6 +3,7 @@ from typing import List, Optional
|
|
|
3
3
|
from devcycle_python_sdk.models.eval_hook import EvalHook
|
|
4
4
|
from devcycle_python_sdk.models.eval_hook_context import HookContext
|
|
5
5
|
from devcycle_python_sdk.models.variable import Variable
|
|
6
|
+
from devcycle_python_sdk.models.variable_metadata import VariableMetadata
|
|
6
7
|
from devcycle_python_sdk.options import logger
|
|
7
8
|
|
|
8
9
|
|
|
@@ -48,19 +49,29 @@ class EvalHooksManager:
|
|
|
48
49
|
raise BeforeHookError(f"Before hook failed: {e}", e)
|
|
49
50
|
return modified_context
|
|
50
51
|
|
|
51
|
-
def run_after(
|
|
52
|
+
def run_after(
|
|
53
|
+
self,
|
|
54
|
+
context: HookContext,
|
|
55
|
+
variable: Variable,
|
|
56
|
+
variable_metadata: Optional[VariableMetadata],
|
|
57
|
+
) -> None:
|
|
52
58
|
"""Run after hooks with the evaluation result"""
|
|
53
59
|
for hook in self.hooks:
|
|
54
60
|
try:
|
|
55
|
-
hook.after(context, variable)
|
|
61
|
+
hook.after(context, variable, variable_metadata)
|
|
56
62
|
except Exception as e:
|
|
57
63
|
raise AfterHookError(f"After hook failed: {e}", e)
|
|
58
64
|
|
|
59
|
-
def run_finally(
|
|
65
|
+
def run_finally(
|
|
66
|
+
self,
|
|
67
|
+
context: HookContext,
|
|
68
|
+
variable: Optional[Variable],
|
|
69
|
+
variable_metadata: Optional[VariableMetadata],
|
|
70
|
+
) -> None:
|
|
60
71
|
"""Run finally hooks after evaluation completes"""
|
|
61
72
|
for hook in self.hooks:
|
|
62
73
|
try:
|
|
63
|
-
hook.on_finally(context, variable)
|
|
74
|
+
hook.on_finally(context, variable, variable_metadata)
|
|
64
75
|
except Exception as e:
|
|
65
76
|
logger.error(f"Error running finally hook: {e}")
|
|
66
77
|
|
|
@@ -43,7 +43,7 @@ class SSEManager:
|
|
|
43
43
|
elif isinstance(event, ld_eventsource.actions.Event):
|
|
44
44
|
handle_message(event)
|
|
45
45
|
except Exception as e:
|
|
46
|
-
logger.
|
|
46
|
+
logger.debug(f"DevCycle: failed to read SSE message: {e}")
|
|
47
47
|
|
|
48
48
|
def update(self, config: dict):
|
|
49
49
|
if self.use_new_config(config["sse"]):
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from devcycle_python_sdk.models.environment_metadata import EnvironmentMetadata
|
|
2
|
+
from devcycle_python_sdk.models.project_metadata import ProjectMetadata
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ConfigMetadata:
|
|
9
|
+
project: ProjectMetadata
|
|
10
|
+
environment: EnvironmentMetadata
|
|
11
|
+
|
|
12
|
+
def to_json(self):
|
|
13
|
+
result = {}
|
|
14
|
+
for field_name in self.__dataclass_fields__:
|
|
15
|
+
value = getattr(self, field_name)
|
|
16
|
+
if value is not None:
|
|
17
|
+
if field_name == "project" and isinstance(value, ProjectMetadata):
|
|
18
|
+
result[field_name] = value.to_json()
|
|
19
|
+
elif field_name == "environment" and isinstance(
|
|
20
|
+
value, EnvironmentMetadata
|
|
21
|
+
):
|
|
22
|
+
result[field_name] = value.to_json()
|
|
23
|
+
else:
|
|
24
|
+
result[field_name] = value
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def from_json(json_obj: Optional[Dict[str, Any]]) -> Optional["ConfigMetadata"]:
|
|
29
|
+
if json_obj is None:
|
|
30
|
+
return None
|
|
31
|
+
project = ProjectMetadata.from_json(json_obj.get("project"))
|
|
32
|
+
environment = EnvironmentMetadata.from_json(json_obj.get("environment"))
|
|
33
|
+
|
|
34
|
+
if project is None or environment is None:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
return ConfigMetadata(
|
|
38
|
+
project=project,
|
|
39
|
+
environment=environment,
|
|
40
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class EnvironmentMetadata:
|
|
7
|
+
id: str
|
|
8
|
+
key: str
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def from_json(
|
|
12
|
+
json_obj: Optional[Dict[str, Any]],
|
|
13
|
+
) -> Optional["EnvironmentMetadata"]:
|
|
14
|
+
if json_obj is None:
|
|
15
|
+
return None
|
|
16
|
+
return EnvironmentMetadata(
|
|
17
|
+
id=json_obj["id"],
|
|
18
|
+
key=json_obj["key"],
|
|
19
|
+
)
|
|
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
|
|
@@ -2,14 +2,17 @@ from typing import Callable, Optional
|
|
|
2
2
|
|
|
3
3
|
from devcycle_python_sdk.models.eval_hook_context import HookContext
|
|
4
4
|
from devcycle_python_sdk.models.variable import Variable
|
|
5
|
+
from devcycle_python_sdk.models.variable_metadata import VariableMetadata
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class EvalHook:
|
|
8
9
|
def __init__(
|
|
9
10
|
self,
|
|
10
11
|
before: Callable[[HookContext], Optional[HookContext]],
|
|
11
|
-
after: Callable[[HookContext, Variable], None],
|
|
12
|
-
on_finally: Callable[
|
|
12
|
+
after: Callable[[HookContext, Variable, Optional[VariableMetadata]], None],
|
|
13
|
+
on_finally: Callable[
|
|
14
|
+
[HookContext, Optional[Variable], Optional[VariableMetadata]], None
|
|
15
|
+
],
|
|
13
16
|
error: Callable[[HookContext, Exception], None],
|
|
14
17
|
):
|
|
15
18
|
self.before = before
|
|
@@ -1,10 +1,28 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
|
|
3
4
|
from devcycle_python_sdk.models.user import DevCycleUser
|
|
5
|
+
from devcycle_python_sdk.models.config_metadata import ConfigMetadata
|
|
4
6
|
|
|
5
7
|
|
|
8
|
+
@dataclass
|
|
6
9
|
class HookContext:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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(
|
|
21
|
+
value, ConfigMetadata
|
|
22
|
+
):
|
|
23
|
+
result[field_name] = value.to_json()
|
|
24
|
+
elif field_name == "user" and isinstance(value, DevCycleUser):
|
|
25
|
+
result[field_name] = value.to_json()
|
|
26
|
+
else:
|
|
27
|
+
result[field_name] = value
|
|
28
|
+
return result
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EvalReasons:
|
|
6
|
+
"""Evaluation reasons constants"""
|
|
7
|
+
|
|
8
|
+
DEFAULT = "DEFAULT"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DefaultReasonDetails:
|
|
12
|
+
"""Default reason details constants"""
|
|
13
|
+
|
|
14
|
+
MISSING_CONFIG = "Missing Config"
|
|
15
|
+
USER_NOT_TARGETED = "User Not Targeted"
|
|
16
|
+
TYPE_MISMATCH = "Variable Type Mismatch"
|
|
17
|
+
ERROR = "Error"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(order=False)
|
|
21
|
+
class EvalReason:
|
|
22
|
+
reason: str
|
|
23
|
+
details: Optional[str] = None
|
|
24
|
+
target_id: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
def to_json(self):
|
|
27
|
+
return {
|
|
28
|
+
key: getattr(self, key)
|
|
29
|
+
for key in self.__dataclass_fields__
|
|
30
|
+
if getattr(self, key) is not None
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_json(cls, data: dict) -> "EvalReason":
|
|
35
|
+
return cls(
|
|
36
|
+
reason=data["reason"],
|
|
37
|
+
details=data.get("details"),
|
|
38
|
+
target_id=data.get("target_id"),
|
|
39
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class ProjectMetadata:
|
|
7
|
+
id: str
|
|
8
|
+
key: str
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def from_json(json_obj: Optional[Dict[str, Any]]) -> Optional["ProjectMetadata"]:
|
|
12
|
+
if json_obj is None:
|
|
13
|
+
return None
|
|
14
|
+
return ProjectMetadata(
|
|
15
|
+
id=json_obj["id"],
|
|
16
|
+
key=json_obj["key"],
|
|
17
|
+
)
|
|
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
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from typing import Optional, Any
|
|
4
4
|
|
|
5
|
+
from .eval_reason import EvalReason, EvalReasons
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
class TypeEnum:
|
|
7
9
|
BOOLEAN = "Boolean"
|
|
@@ -32,16 +34,26 @@ class Variable:
|
|
|
32
34
|
isDefaulted: Optional[bool] = False
|
|
33
35
|
defaultValue: Any = None
|
|
34
36
|
evalReason: Optional[str] = None
|
|
37
|
+
eval: Optional[EvalReason] = None
|
|
35
38
|
|
|
36
39
|
def to_json(self):
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if
|
|
41
|
-
|
|
40
|
+
result = {}
|
|
41
|
+
for key in self.__dataclass_fields__:
|
|
42
|
+
value = getattr(self, key)
|
|
43
|
+
if value is not None:
|
|
44
|
+
if key == "eval" and isinstance(value, EvalReason):
|
|
45
|
+
result[key] = value.to_json()
|
|
46
|
+
else:
|
|
47
|
+
result[key] = value
|
|
48
|
+
return result
|
|
42
49
|
|
|
43
50
|
@classmethod
|
|
44
51
|
def from_json(cls, data: dict) -> "Variable":
|
|
52
|
+
eval_data = data.get("eval")
|
|
53
|
+
eval_reason = None
|
|
54
|
+
if eval_data:
|
|
55
|
+
eval_reason = EvalReason.from_json(eval_data)
|
|
56
|
+
|
|
45
57
|
return cls(
|
|
46
58
|
_id=data["_id"],
|
|
47
59
|
key=data["key"],
|
|
@@ -50,11 +62,20 @@ class Variable:
|
|
|
50
62
|
isDefaulted=data.get("isDefaulted", None),
|
|
51
63
|
defaultValue=data.get("defaultValue"),
|
|
52
64
|
evalReason=data.get("evalReason"),
|
|
65
|
+
eval=eval_reason,
|
|
53
66
|
)
|
|
54
67
|
|
|
55
68
|
@staticmethod
|
|
56
|
-
def create_default_variable(
|
|
69
|
+
def create_default_variable(
|
|
70
|
+
key: str, default_value: Any, default_reason_detail: Optional[str] = None
|
|
71
|
+
) -> "Variable":
|
|
57
72
|
var_type = determine_variable_type(default_value)
|
|
73
|
+
if default_reason_detail is not None:
|
|
74
|
+
eval_reason = EvalReason(
|
|
75
|
+
reason=EvalReasons.DEFAULT, details=default_reason_detail
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
eval_reason = None
|
|
58
79
|
return Variable(
|
|
59
80
|
_id=None,
|
|
60
81
|
key=key,
|
|
@@ -62,4 +83,21 @@ class Variable:
|
|
|
62
83
|
value=default_value,
|
|
63
84
|
defaultValue=default_value,
|
|
64
85
|
isDefaulted=True,
|
|
86
|
+
eval=eval_reason,
|
|
65
87
|
)
|
|
88
|
+
|
|
89
|
+
def get_flag_meta_data(self) -> dict:
|
|
90
|
+
"""
|
|
91
|
+
Returns metadata dictionary for OpenFeature flag resolution.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
dict: Dictionary containing evalReasonDetails and evalReasonTargetId
|
|
95
|
+
if they exist, empty dict otherwise.
|
|
96
|
+
"""
|
|
97
|
+
meta_data = {}
|
|
98
|
+
if self.eval:
|
|
99
|
+
if self.eval.details:
|
|
100
|
+
meta_data["evalReasonDetails"] = self.eval.details
|
|
101
|
+
if self.eval.target_id:
|
|
102
|
+
meta_data["evalReasonTargetId"] = self.eval.target_id
|
|
103
|
+
return meta_data
|
|
@@ -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
|