devcycle-python-server-sdk 3.11.4__tar.gz → 3.13.0__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.
Files changed (87) hide show
  1. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/PKG-INFO +2 -2
  2. devcycle_python_server_sdk-3.13.0/devcycle_python_sdk/VERSION.txt +1 -0
  3. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/api/bucketing_client.py +12 -0
  4. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/api/local_bucketing.py +19 -12
  5. devcycle_python_server_sdk-3.13.0/devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
  6. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/cloud_client.py +14 -5
  7. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/local_client.py +41 -18
  8. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/managers/config_manager.py +4 -0
  9. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/managers/eval_hooks_manager.py +15 -4
  10. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/managers/event_queue_manager.py +0 -1
  11. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/managers/sse_manager.py +1 -1
  12. devcycle_python_server_sdk-3.13.0/devcycle_python_sdk/models/config_metadata.py +40 -0
  13. devcycle_python_server_sdk-3.13.0/devcycle_python_sdk/models/environment_metadata.py +26 -0
  14. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/models/eval_hook.py +5 -2
  15. devcycle_python_server_sdk-3.13.0/devcycle_python_sdk/models/eval_hook_context.py +28 -0
  16. devcycle_python_server_sdk-3.13.0/devcycle_python_sdk/models/eval_reason.py +39 -0
  17. devcycle_python_server_sdk-3.13.0/devcycle_python_sdk/models/project_metadata.py +24 -0
  18. devcycle_python_server_sdk-3.13.0/devcycle_python_sdk/models/variable.py +103 -0
  19. devcycle_python_server_sdk-3.13.0/devcycle_python_sdk/models/variable_metadata.py +22 -0
  20. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/open_feature_provider/provider.py +10 -5
  21. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/options.py +1 -1
  22. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/protobuf/utils.py +27 -3
  23. devcycle_python_server_sdk-3.13.0/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +51 -0
  24. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi +99 -77
  25. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_server_sdk.egg-info/PKG-INFO +2 -2
  26. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_server_sdk.egg-info/SOURCES.txt +5 -0
  27. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_server_sdk.egg-info/requires.txt +1 -1
  28. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/api/test_local_bucketing.py +17 -3
  29. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/models/test_user.py +0 -1
  30. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/openfeature_test/test_provider.py +79 -3
  31. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/openfeature_test/test_provider_local_sdk.py +25 -0
  32. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/test_cloud_client.py +46 -4
  33. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/test_local_client.py +69 -5
  34. devcycle_python_server_sdk-3.11.4/devcycle_python_sdk/VERSION.txt +0 -1
  35. devcycle_python_server_sdk-3.11.4/devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
  36. devcycle_python_server_sdk-3.11.4/devcycle_python_sdk/models/eval_hook_context.py +0 -10
  37. devcycle_python_server_sdk-3.11.4/devcycle_python_sdk/models/variable.py +0 -65
  38. devcycle_python_server_sdk-3.11.4/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +0 -46
  39. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/LICENSE +0 -0
  40. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/README.md +0 -0
  41. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/__init__.py +0 -0
  42. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/api/__init__.py +0 -0
  43. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/api/backoff.py +0 -0
  44. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/api/config_client.py +0 -0
  45. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/api/event_client.py +0 -0
  46. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/devcycle_client.py +0 -0
  47. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/exceptions.py +0 -0
  48. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/managers/__init__.py +0 -0
  49. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/models/__init__.py +0 -0
  50. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/models/bucketed_config.py +0 -0
  51. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/models/error_response.py +0 -0
  52. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/models/event.py +0 -0
  53. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/models/feature.py +0 -0
  54. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/models/platform_data.py +0 -0
  55. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/models/user.py +0 -0
  56. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/open_feature_provider/__init__.py +0 -0
  57. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/protobuf/__init__.py +0 -0
  58. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/py.typed +0 -0
  59. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/util/__init__.py +0 -0
  60. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/util/strings.py +0 -0
  61. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_sdk/util/version.py +0 -0
  62. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_server_sdk.egg-info/dependency_links.txt +0 -0
  63. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/devcycle_python_server_sdk.egg-info/top_level.txt +0 -0
  64. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/example/__init__.py +0 -0
  65. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/example/cloud_client_example.py +0 -0
  66. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/example/local_bucketing_client_example.py +0 -0
  67. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/example/openfeature_example.py +0 -0
  68. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/pyproject.toml +0 -0
  69. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/setup.cfg +0 -0
  70. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/setup.py +0 -0
  71. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/__init__.py +0 -0
  72. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/api/__init__.py +0 -0
  73. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/api/test_bucketing_client.py +0 -0
  74. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/api/test_config_client.py +0 -0
  75. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/api/test_event_client.py +0 -0
  76. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/fixture/__init__.py +0 -0
  77. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/fixture/data.py +0 -0
  78. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/managers/__init__.py +0 -0
  79. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/managers/test_config_manager.py +0 -0
  80. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/managers/test_event_queue_manager.py +0 -0
  81. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/models/__init__.py +0 -0
  82. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/models/test_bucketed_config.py +0 -0
  83. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/openfeature_test/__init__.py +0 -0
  84. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/util/__init__.py +0 -0
  85. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/util/test_strings.py +0 -0
  86. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/util/test_utils.py +0 -0
  87. {devcycle_python_server_sdk-3.11.4 → devcycle_python_server_sdk-3.13.0}/test/util/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devcycle-python-server-sdk
3
- Version: 3.11.4
3
+ Version: 3.13.0
4
4
  Summary: DevCycle Python SDK
5
5
  Home-page: https://github.com/devcycleHQ/python-server-sdk
6
6
  Author-email:
@@ -12,7 +12,7 @@ Requires-Dist: urllib3>=1.15.1
12
12
  Requires-Dist: requests>=2.32
13
13
  Requires-Dist: wasmtime~=30.0.0
14
14
  Requires-Dist: protobuf>=4.23.3
15
- Requires-Dist: openfeature-sdk>=0.8.0
15
+ Requires-Dist: openfeature-sdk==0.8.0
16
16
  Requires-Dist: launchdarkly-eventsource>=1.2.1
17
17
  Requires-Dist: responses>=0.23.1
18
18
  Dynamic: description
@@ -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
- if sdk_variable.type != pb_variable_type:
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")
@@ -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, default_value=default_value
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, default_value=default_value
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, default_value=default_value
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, target=key, value=1
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(key, default_value)
150
+ return Variable.create_default_variable(
151
+ key, default_value, DefaultReasonDetails.MISSING_CONFIG
152
+ )
143
153
 
144
- context = HookContext(key, user, default_value)
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 = self.local_bucketing.get_variable_for_user_protobuf(
158
- user, key, default_value
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
- logger.warning(f"DevCycle: Error retrieving variable for user: {e}")
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(self, context: HookContext, variable: Variable) -> None:
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(self, context: HookContext, variable: Optional[Variable]) -> None:
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
 
@@ -19,7 +19,6 @@ from devcycle_python_sdk.models.event import (
19
19
  from devcycle_python_sdk.models.user import DevCycleUser
20
20
  from devcycle_python_sdk.models.bucketed_config import BucketedConfig
21
21
 
22
-
23
22
  logger = logging.getLogger(__name__)
24
23
 
25
24
 
@@ -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.exception(f"DevCycle: failed to read SSE message: {e}")
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[[HookContext, Optional[Variable]], None],
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
@@ -0,0 +1,28 @@
1
+ from typing import Any, Optional
2
+ from dataclasses import dataclass
3
+
4
+ from devcycle_python_sdk.models.user import DevCycleUser
5
+ from devcycle_python_sdk.models.config_metadata import ConfigMetadata
6
+
7
+
8
+ @dataclass
9
+ class HookContext:
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
@@ -0,0 +1,103 @@
1
+ # ruff: noqa: N815
2
+ from dataclasses import dataclass
3
+ from typing import Optional, Any
4
+
5
+ from .eval_reason import EvalReason, EvalReasons
6
+
7
+
8
+ class TypeEnum:
9
+ BOOLEAN = "Boolean"
10
+ STRING = "String"
11
+ NUMBER = "Number"
12
+ JSON = "JSON"
13
+
14
+
15
+ def determine_variable_type(value: Any) -> str:
16
+ if isinstance(value, bool):
17
+ return TypeEnum.BOOLEAN
18
+ elif isinstance(value, str):
19
+ return TypeEnum.STRING
20
+ elif isinstance(value, (int, float)):
21
+ return TypeEnum.NUMBER
22
+ elif isinstance(value, dict):
23
+ return TypeEnum.JSON
24
+ else:
25
+ raise TypeError(f"Unsupported type: {type(value)}")
26
+
27
+
28
+ @dataclass(order=False)
29
+ class Variable:
30
+ _id: Optional[str]
31
+ key: str
32
+ type: str
33
+ value: Any = None
34
+ isDefaulted: Optional[bool] = False
35
+ defaultValue: Any = None
36
+ evalReason: Optional[str] = None
37
+ eval: Optional[EvalReason] = None
38
+
39
+ def to_json(self):
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
49
+
50
+ @classmethod
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
+
57
+ return cls(
58
+ _id=data["_id"],
59
+ key=data["key"],
60
+ type=data["type"],
61
+ value=data["value"],
62
+ isDefaulted=data.get("isDefaulted", None),
63
+ defaultValue=data.get("defaultValue"),
64
+ evalReason=data.get("evalReason"),
65
+ eval=eval_reason,
66
+ )
67
+
68
+ @staticmethod
69
+ def create_default_variable(
70
+ key: str, default_value: Any, default_reason_detail: Optional[str] = None
71
+ ) -> "Variable":
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
79
+ return Variable(
80
+ _id=None,
81
+ key=key,
82
+ type=var_type,
83
+ value=default_value,
84
+ defaultValue=default_value,
85
+ isDefaulted=True,
86
+ eval=eval_reason,
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