devcycle-python-server-sdk 3.12.0__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.
Files changed (29) hide show
  1. devcycle_python_sdk/VERSION.txt +1 -1
  2. devcycle_python_sdk/api/local_bucketing.py +19 -4
  3. devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
  4. devcycle_python_sdk/cloud_client.py +3 -3
  5. devcycle_python_sdk/local_client.py +13 -5
  6. devcycle_python_sdk/managers/config_manager.py +4 -0
  7. devcycle_python_sdk/managers/eval_hooks_manager.py +15 -4
  8. devcycle_python_sdk/managers/event_queue_manager.py +0 -1
  9. devcycle_python_sdk/models/config_metadata.py +40 -0
  10. devcycle_python_sdk/models/environment_metadata.py +26 -0
  11. devcycle_python_sdk/models/eval_hook.py +5 -2
  12. devcycle_python_sdk/models/eval_hook_context.py +23 -5
  13. devcycle_python_sdk/models/eval_reason.py +0 -1
  14. devcycle_python_sdk/models/project_metadata.py +24 -0
  15. devcycle_python_sdk/models/variable_metadata.py +22 -0
  16. devcycle_python_sdk/options.py +1 -1
  17. devcycle_python_sdk/protobuf/utils.py +9 -3
  18. devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +34 -42
  19. devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi +11 -11
  20. {devcycle_python_server_sdk-3.12.0.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/METADATA +2 -2
  21. {devcycle_python_server_sdk-3.12.0.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/RECORD +29 -25
  22. test/api/test_local_bucketing.py +6 -3
  23. test/models/test_user.py +0 -1
  24. test/openfeature_test/test_provider.py +0 -1
  25. test/test_cloud_client.py +41 -6
  26. test/test_local_client.py +53 -4
  27. {devcycle_python_server_sdk-3.12.0.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/WHEEL +0 -0
  28. {devcycle_python_server_sdk-3.12.0.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/licenses/LICENSE +0 -0
  29. {devcycle_python_server_sdk-3.12.0.dist-info → devcycle_python_server_sdk-3.13.0.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- 3.12.0
1
+ 3.13.0
@@ -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 (
@@ -28,6 +28,7 @@ from devcycle_python_sdk.models.bucketed_config import BucketedConfig
28
28
  from devcycle_python_sdk.models.user import DevCycleUser
29
29
  from devcycle_python_sdk.models.variable import Variable, determine_variable_type
30
30
  from devcycle_python_sdk.models.event import FlushPayload
31
+ from devcycle_python_sdk.models.config_metadata import ConfigMetadata
31
32
 
32
33
  logger = logging.getLogger(__name__)
33
34
 
@@ -140,6 +141,7 @@ class LocalBucketing:
140
141
  "generateBucketedConfigForUserUTF8"
141
142
  )
142
143
  self.VariableForUserProtobuf = self._get_export("variableForUser_PB")
144
+ self.getConfigMetadata = self._get_export("getConfigMetadata")
143
145
 
144
146
  # Extract variable type enum values from WASM
145
147
  self.variable_type_map = {
@@ -298,7 +300,7 @@ class LocalBucketing:
298
300
 
299
301
  def get_variable_for_user_protobuf(
300
302
  self, user: DevCycleUser, key: str, default_value: Any
301
- ) -> Optional[Variable]:
303
+ ) -> Tuple[Optional[Variable], Optional[str]]:
302
304
  var_type = determine_variable_type(default_value)
303
305
  pb_variable_type = pb_utils.convert_type_enum_to_variable_type(var_type)
304
306
 
@@ -317,13 +319,18 @@ class LocalBucketing:
317
319
  variable_addr = self.VariableForUserProtobuf(self.wasm_store, params_addr)
318
320
 
319
321
  if variable_addr == 0:
320
- return None
322
+ return None, None
321
323
  else:
322
324
  var_bytes = self._read_assembly_script_byte_array(variable_addr)
323
325
  sdk_variable = pb2.SDKVariable_PB()
324
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
+ )
325
332
 
326
- return pb_utils.create_variable(sdk_variable, default_value)
333
+ return pb_utils.create_variable(sdk_variable, default_value), feature_id
327
334
 
328
335
  def generate_bucketed_config(self, user: DevCycleUser) -> BucketedConfig:
329
336
  user_json = json.dumps(user.to_json())
@@ -357,6 +364,14 @@ class LocalBucketing:
357
364
  config_addr = self._new_assembly_script_byte_array(data)
358
365
  self.setConfigDataUTF8(self.wasm_store, self.sdk_key_addr, config_addr)
359
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
+
360
375
  def set_platform_data(self, platform_json: str) -> None:
361
376
  with self.wasm_lock:
362
377
  data = platform_json.encode("utf-8")
@@ -115,7 +115,7 @@ class DevCycleCloudClient(AbstractDevCycleClient):
115
115
  before_hook_error = e
116
116
  variable = self.bucketing_api.variable(key, context.user)
117
117
  if before_hook_error is None:
118
- self.eval_hooks_manager.run_after(context, variable)
118
+ self.eval_hooks_manager.run_after(context, variable, None)
119
119
  else:
120
120
  raise before_hook_error
121
121
  except CloudClientUnauthorizedError as e:
@@ -126,7 +126,7 @@ class DevCycleCloudClient(AbstractDevCycleClient):
126
126
  return Variable.create_default_variable(
127
127
  key=key,
128
128
  default_value=default_value,
129
- default_reason_detail=DefaultReasonDetails.MISSING_VARIABLE,
129
+ default_reason_detail=DefaultReasonDetails.ERROR,
130
130
  )
131
131
  except BeforeHookError as e:
132
132
  self.eval_hooks_manager.run_error(context, e)
@@ -140,7 +140,7 @@ class DevCycleCloudClient(AbstractDevCycleClient):
140
140
  default_reason_detail=DefaultReasonDetails.ERROR,
141
141
  )
142
142
  finally:
143
- self.eval_hooks_manager.run_finally(context, variable)
143
+ self.eval_hooks_manager.run_finally(context, variable, None)
144
144
 
145
145
  variable.defaultValue = default_value
146
146
 
@@ -26,6 +26,7 @@ from devcycle_python_sdk.models.feature import Feature
26
26
  from devcycle_python_sdk.models.platform_data import default_platform_data
27
27
  from devcycle_python_sdk.models.user import DevCycleUser
28
28
  from devcycle_python_sdk.models.variable import Variable
29
+ from devcycle_python_sdk.models.variable_metadata import VariableMetadata
29
30
  from devcycle_python_sdk.open_feature_provider.provider import DevCycleProvider
30
31
  from openfeature.provider import AbstractProvider
31
32
 
@@ -150,7 +151,10 @@ class DevCycleLocalClient(AbstractDevCycleClient):
150
151
  key, default_value, DefaultReasonDetails.MISSING_CONFIG
151
152
  )
152
153
 
153
- 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)
154
158
  variable = Variable.create_default_variable(
155
159
  key=key, default_value=default_value
156
160
  )
@@ -163,9 +167,13 @@ class DevCycleLocalClient(AbstractDevCycleClient):
163
167
  context = changed_context
164
168
  except BeforeHookError as e:
165
169
  before_hook_error = e
166
- bucketed_variable = self.local_bucketing.get_variable_for_user_protobuf(
167
- user, key, default_value
170
+ bucketed_variable, feature_id = (
171
+ self.local_bucketing.get_variable_for_user_protobuf(
172
+ user, key, default_value
173
+ )
168
174
  )
175
+ if feature_id is not None:
176
+ variable_metadata = VariableMetadata(feature_id=feature_id)
169
177
  if bucketed_variable is not None:
170
178
  variable = bucketed_variable
171
179
  else:
@@ -175,7 +183,7 @@ class DevCycleLocalClient(AbstractDevCycleClient):
175
183
  )
176
184
 
177
185
  if before_hook_error is None:
178
- self.eval_hooks_manager.run_after(context, variable)
186
+ self.eval_hooks_manager.run_after(context, variable, variable_metadata)
179
187
  else:
180
188
  raise before_hook_error
181
189
  except Exception as e:
@@ -192,7 +200,7 @@ class DevCycleLocalClient(AbstractDevCycleClient):
192
200
 
193
201
  return variable
194
202
  finally:
195
- self.eval_hooks_manager.run_finally(context, variable)
203
+ self.eval_hooks_manager.run_finally(context, variable, variable_metadata)
196
204
  return variable
197
205
 
198
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
 
@@ -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
@@ -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
- def __init__(self, key: str, user: DevCycleUser, default_value: Any):
8
- self.key = key
9
- self.default_value = default_value
10
- self.user = user
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
@@ -14,7 +14,6 @@ class DefaultReasonDetails:
14
14
  MISSING_CONFIG = "Missing Config"
15
15
  USER_NOT_TARGETED = "User Not Targeted"
16
16
  TYPE_MISMATCH = "Variable Type Mismatch"
17
- MISSING_VARIABLE = "Missing Variable"
18
17
  ERROR = "Error"
19
18
 
20
19
 
@@ -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,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
@@ -90,7 +90,7 @@ class DevCycleLocalOptions:
90
90
 
91
91
  if self.event_request_chunk_size > self.max_event_queue_size:
92
92
  logger.warning(
93
- f"DevCycle: event_request_chunk_size: {self.event_request_chunk_size} must be smaller than max_event_queue_size: { self.max_event_queue_size}"
93
+ f"DevCycle: event_request_chunk_size: {self.event_request_chunk_size} must be smaller than max_event_queue_size: {self.max_event_queue_size}"
94
94
  )
95
95
  self.event_request_chunk_size = 100
96
96
 
@@ -34,11 +34,17 @@ def create_nullable_custom_data(val: Optional[dict]) -> pb2.NullableCustomData:
34
34
  if value is None:
35
35
  values[key] = pb2.CustomDataValue(type=pb2.CustomDataType.Null) # type: ignore
36
36
  elif isinstance(value, bool):
37
- values[key] = pb2.CustomDataValue(type=pb2.CustomDataType.Bool, boolValue=value) # type: ignore
37
+ values[key] = pb2.CustomDataValue(
38
+ type=pb2.CustomDataType.Bool, boolValue=value
39
+ ) # type: ignore
38
40
  elif isinstance(value, str):
39
- values[key] = pb2.CustomDataValue(type=pb2.CustomDataType.Str, stringValue=value) # type: ignore
41
+ values[key] = pb2.CustomDataValue(
42
+ type=pb2.CustomDataType.Str, stringValue=value
43
+ ) # type: ignore
40
44
  elif isinstance(value, (int, float)):
41
- values[key] = pb2.CustomDataValue(type=pb2.CustomDataType.Num, doubleValue=value) # type: ignore
45
+ values[key] = pb2.CustomDataValue(
46
+ type=pb2.CustomDataType.Num, doubleValue=value
47
+ ) # type: ignore
42
48
  else:
43
49
  logger.warning(
44
50
  f"Custom Data contains data type that can't be written, will be ignored. Key: {key}, Type: {str(type(value))}"
@@ -1,59 +1,51 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
- # NO CHECKED-IN PROTOBUF GENCODE
4
3
  # source: variableForUserParams.proto
5
- # Protobuf Python Version: 5.29.3
6
4
  """Generated protocol buffer code."""
5
+
7
6
  from google.protobuf import descriptor as _descriptor
8
7
  from google.protobuf import descriptor_pool as _descriptor_pool
9
- from google.protobuf import runtime_version as _runtime_version
10
8
  from google.protobuf import symbol_database as _symbol_database
11
9
  from google.protobuf.internal import builder as _builder
12
- _runtime_version.ValidateProtobufRuntimeVersion(
13
- _runtime_version.Domain.PUBLIC,
14
- 5,
15
- 29,
16
- 3,
17
- '',
18
- 'variableForUserParams.proto'
19
- )
20
10
  # @@protoc_insertion_point(imports)
21
11
 
22
12
  _sym_db = _symbol_database.Default()
23
13
 
24
14
 
25
-
26
-
27
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bvariableForUserParams.proto\"/\n\x0eNullableString\x12\r\n\x05value\x18\x01 \x01(\t\x12\x0e\n\x06isNull\x18\x02 \x01(\x08\"/\n\x0eNullableDouble\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x0e\n\x06isNull\x18\x02 \x01(\x08\"m\n\x0f\x43ustomDataValue\x12\x1d\n\x04type\x18\x01 \x01(\x0e\x32\x0f.CustomDataType\x12\x11\n\tboolValue\x18\x02 \x01(\x08\x12\x13\n\x0b\x64oubleValue\x18\x03 \x01(\x01\x12\x13\n\x0bstringValue\x18\x04 \x01(\t\"\x93\x01\n\x12NullableCustomData\x12-\n\x05value\x18\x01 \x03(\x0b\x32\x1e.NullableCustomData.ValueEntry\x12\x0e\n\x06isNull\x18\x02 \x01(\x08\x1a>\n\nValueEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1f\n\x05value\x18\x02 \x01(\x0b\x32\x10.CustomDataValue:\x02\x38\x01\"\x9c\x01\n\x18VariableForUserParams_PB\x12\x0e\n\x06sdkKey\x18\x01 \x01(\t\x12\x13\n\x0bvariableKey\x18\x02 \x01(\t\x12&\n\x0cvariableType\x18\x03 \x01(\x0e\x32\x10.VariableType_PB\x12\x19\n\x04user\x18\x04 \x01(\x0b\x32\x0b.DVCUser_PB\x12\x18\n\x10shouldTrackEvent\x18\x05 \x01(\x08\"\xe8\x02\n\nDVCUser_PB\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x1e\n\x05\x65mail\x18\x02 \x01(\x0b\x32\x0f.NullableString\x12\x1d\n\x04name\x18\x03 \x01(\x0b\x32\x0f.NullableString\x12!\n\x08language\x18\x04 \x01(\x0b\x32\x0f.NullableString\x12 \n\x07\x63ountry\x18\x05 \x01(\x0b\x32\x0f.NullableString\x12!\n\x08\x61ppBuild\x18\x06 \x01(\x0b\x32\x0f.NullableDouble\x12#\n\nappVersion\x18\x07 \x01(\x0b\x32\x0f.NullableString\x12$\n\x0b\x64\x65viceModel\x18\x08 \x01(\x0b\x32\x0f.NullableString\x12\'\n\ncustomData\x18\t \x01(\x0b\x32\x13.NullableCustomData\x12.\n\x11privateCustomData\x18\n \x01(\x0b\x32\x13.NullableCustomData\"\xed\x01\n\x0eSDKVariable_PB\x12\x0b\n\x03_id\x18\x01 \x01(\t\x12\x1e\n\x04type\x18\x02 \x01(\x0e\x32\x10.VariableType_PB\x12\x0b\n\x03key\x18\x03 \x01(\t\x12\x11\n\tboolValue\x18\x04 \x01(\x08\x12\x13\n\x0b\x64oubleValue\x18\x05 \x01(\x01\x12\x13\n\x0bstringValue\x18\x06 \x01(\t\x12#\n\nevalReason\x18\x07 \x01(\x0b\x32\x0f.NullableString\x12!\n\x08_feature\x18\x08 \x01(\x0b\x32\x0f.NullableString\x12\x1c\n\x04\x65val\x18\t \x01(\x0b\x32\x0e.EvalReason_PB\"C\n\rEvalReason_PB\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x0f\n\x07\x64\x65tails\x18\x02 \x01(\t\x12\x11\n\ttarget_id\x18\x03 \x01(\t*@\n\x0fVariableType_PB\x12\x0b\n\x07\x42oolean\x10\x00\x12\n\n\x06Number\x10\x01\x12\n\n\x06String\x10\x02\x12\x08\n\x04JSON\x10\x03*6\n\x0e\x43ustomDataType\x12\x08\n\x04\x42ool\x10\x00\x12\x07\n\x03Num\x10\x01\x12\x07\n\x03Str\x10\x02\x12\x08\n\x04Null\x10\x03\x42X\n&com.devcycle.sdk.server.local.protobufP\x01Z\x07./proto\xaa\x02\"DevCycle.SDK.Server.Local.Protobufb\x06proto3')
15
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
16
+ b'\n\x1bvariableForUserParams.proto"/\n\x0eNullableString\x12\r\n\x05value\x18\x01 \x01(\t\x12\x0e\n\x06isNull\x18\x02 \x01(\x08"/\n\x0eNullableDouble\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x0e\n\x06isNull\x18\x02 \x01(\x08"m\n\x0f\x43ustomDataValue\x12\x1d\n\x04type\x18\x01 \x01(\x0e\x32\x0f.CustomDataType\x12\x11\n\tboolValue\x18\x02 \x01(\x08\x12\x13\n\x0b\x64oubleValue\x18\x03 \x01(\x01\x12\x13\n\x0bstringValue\x18\x04 \x01(\t"\x93\x01\n\x12NullableCustomData\x12-\n\x05value\x18\x01 \x03(\x0b\x32\x1e.NullableCustomData.ValueEntry\x12\x0e\n\x06isNull\x18\x02 \x01(\x08\x1a>\n\nValueEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1f\n\x05value\x18\x02 \x01(\x0b\x32\x10.CustomDataValue:\x02\x38\x01"\x9c\x01\n\x18VariableForUserParams_PB\x12\x0e\n\x06sdkKey\x18\x01 \x01(\t\x12\x13\n\x0bvariableKey\x18\x02 \x01(\t\x12&\n\x0cvariableType\x18\x03 \x01(\x0e\x32\x10.VariableType_PB\x12\x19\n\x04user\x18\x04 \x01(\x0b\x32\x0b.DVCUser_PB\x12\x18\n\x10shouldTrackEvent\x18\x05 \x01(\x08"\xe8\x02\n\nDVCUser_PB\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x1e\n\x05\x65mail\x18\x02 \x01(\x0b\x32\x0f.NullableString\x12\x1d\n\x04name\x18\x03 \x01(\x0b\x32\x0f.NullableString\x12!\n\x08language\x18\x04 \x01(\x0b\x32\x0f.NullableString\x12 \n\x07\x63ountry\x18\x05 \x01(\x0b\x32\x0f.NullableString\x12!\n\x08\x61ppBuild\x18\x06 \x01(\x0b\x32\x0f.NullableDouble\x12#\n\nappVersion\x18\x07 \x01(\x0b\x32\x0f.NullableString\x12$\n\x0b\x64\x65viceModel\x18\x08 \x01(\x0b\x32\x0f.NullableString\x12\'\n\ncustomData\x18\t \x01(\x0b\x32\x13.NullableCustomData\x12.\n\x11privateCustomData\x18\n \x01(\x0b\x32\x13.NullableCustomData"\xed\x01\n\x0eSDKVariable_PB\x12\x0b\n\x03_id\x18\x01 \x01(\t\x12\x1e\n\x04type\x18\x02 \x01(\x0e\x32\x10.VariableType_PB\x12\x0b\n\x03key\x18\x03 \x01(\t\x12\x11\n\tboolValue\x18\x04 \x01(\x08\x12\x13\n\x0b\x64oubleValue\x18\x05 \x01(\x01\x12\x13\n\x0bstringValue\x18\x06 \x01(\t\x12#\n\nevalReason\x18\x07 \x01(\x0b\x32\x0f.NullableString\x12!\n\x08_feature\x18\x08 \x01(\x0b\x32\x0f.NullableString\x12\x1c\n\x04\x65val\x18\t \x01(\x0b\x32\x0e.EvalReason_PB"C\n\rEvalReason_PB\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x0f\n\x07\x64\x65tails\x18\x02 \x01(\t\x12\x11\n\ttarget_id\x18\x03 \x01(\t*@\n\x0fVariableType_PB\x12\x0b\n\x07\x42oolean\x10\x00\x12\n\n\x06Number\x10\x01\x12\n\n\x06String\x10\x02\x12\x08\n\x04JSON\x10\x03*6\n\x0e\x43ustomDataType\x12\x08\n\x04\x42ool\x10\x00\x12\x07\n\x03Num\x10\x01\x12\x07\n\x03Str\x10\x02\x12\x08\n\x04Null\x10\x03\x42X\n&com.devcycle.sdk.server.local.protobufP\x01Z\x07./proto\xaa\x02"DevCycle.SDK.Server.Local.Protobufb\x06proto3'
17
+ )
28
18
 
29
19
  _globals = globals()
30
20
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
- _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'variableForUserParams_pb2', _globals)
32
- if not _descriptor._USE_C_DESCRIPTORS:
33
- _globals['DESCRIPTOR']._loaded_options = None
34
- _globals['DESCRIPTOR']._serialized_options = b'\n&com.devcycle.sdk.server.local.protobufP\001Z\007./proto\252\002\"DevCycle.SDK.Server.Local.Protobuf'
35
- _globals['_NULLABLECUSTOMDATA_VALUEENTRY']._loaded_options = None
36
- _globals['_NULLABLECUSTOMDATA_VALUEENTRY']._serialized_options = b'8\001'
37
- _globals['_VARIABLETYPE_PB']._serialized_start=1221
38
- _globals['_VARIABLETYPE_PB']._serialized_end=1285
39
- _globals['_CUSTOMDATATYPE']._serialized_start=1287
40
- _globals['_CUSTOMDATATYPE']._serialized_end=1341
41
- _globals['_NULLABLESTRING']._serialized_start=31
42
- _globals['_NULLABLESTRING']._serialized_end=78
43
- _globals['_NULLABLEDOUBLE']._serialized_start=80
44
- _globals['_NULLABLEDOUBLE']._serialized_end=127
45
- _globals['_CUSTOMDATAVALUE']._serialized_start=129
46
- _globals['_CUSTOMDATAVALUE']._serialized_end=238
47
- _globals['_NULLABLECUSTOMDATA']._serialized_start=241
48
- _globals['_NULLABLECUSTOMDATA']._serialized_end=388
49
- _globals['_NULLABLECUSTOMDATA_VALUEENTRY']._serialized_start=326
50
- _globals['_NULLABLECUSTOMDATA_VALUEENTRY']._serialized_end=388
51
- _globals['_VARIABLEFORUSERPARAMS_PB']._serialized_start=391
52
- _globals['_VARIABLEFORUSERPARAMS_PB']._serialized_end=547
53
- _globals['_DVCUSER_PB']._serialized_start=550
54
- _globals['_DVCUSER_PB']._serialized_end=910
55
- _globals['_SDKVARIABLE_PB']._serialized_start=913
56
- _globals['_SDKVARIABLE_PB']._serialized_end=1150
57
- _globals['_EVALREASON_PB']._serialized_start=1152
58
- _globals['_EVALREASON_PB']._serialized_end=1219
21
+ _builder.BuildTopDescriptorsAndMessages(
22
+ DESCRIPTOR, "variableForUserParams_pb2", _globals
23
+ )
24
+ if _descriptor._USE_C_DESCRIPTORS == False:
25
+ DESCRIPTOR._options = None
26
+ DESCRIPTOR._serialized_options = b'\n&com.devcycle.sdk.server.local.protobufP\001Z\007./proto\252\002"DevCycle.SDK.Server.Local.Protobuf'
27
+ _NULLABLECUSTOMDATA_VALUEENTRY._options = None
28
+ _NULLABLECUSTOMDATA_VALUEENTRY._serialized_options = b"8\001"
29
+ _globals["_VARIABLETYPE_PB"]._serialized_start = 1221
30
+ _globals["_VARIABLETYPE_PB"]._serialized_end = 1285
31
+ _globals["_CUSTOMDATATYPE"]._serialized_start = 1287
32
+ _globals["_CUSTOMDATATYPE"]._serialized_end = 1341
33
+ _globals["_NULLABLESTRING"]._serialized_start = 31
34
+ _globals["_NULLABLESTRING"]._serialized_end = 78
35
+ _globals["_NULLABLEDOUBLE"]._serialized_start = 80
36
+ _globals["_NULLABLEDOUBLE"]._serialized_end = 127
37
+ _globals["_CUSTOMDATAVALUE"]._serialized_start = 129
38
+ _globals["_CUSTOMDATAVALUE"]._serialized_end = 238
39
+ _globals["_NULLABLECUSTOMDATA"]._serialized_start = 241
40
+ _globals["_NULLABLECUSTOMDATA"]._serialized_end = 388
41
+ _globals["_NULLABLECUSTOMDATA_VALUEENTRY"]._serialized_start = 326
42
+ _globals["_NULLABLECUSTOMDATA_VALUEENTRY"]._serialized_end = 388
43
+ _globals["_VARIABLEFORUSERPARAMS_PB"]._serialized_start = 391
44
+ _globals["_VARIABLEFORUSERPARAMS_PB"]._serialized_end = 547
45
+ _globals["_DVCUSER_PB"]._serialized_start = 550
46
+ _globals["_DVCUSER_PB"]._serialized_end = 910
47
+ _globals["_SDKVARIABLE_PB"]._serialized_start = 913
48
+ _globals["_SDKVARIABLE_PB"]._serialized_end = 1150
49
+ _globals["_EVALREASON_PB"]._serialized_start = 1152
50
+ _globals["_EVALREASON_PB"]._serialized_end = 1219
59
51
  # @@protoc_insertion_point(module_scope)
@@ -7,14 +7,14 @@ from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Opti
7
7
  DESCRIPTOR: _descriptor.FileDescriptor
8
8
 
9
9
  class VariableType_PB(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
10
- __slots__ = ()
10
+ __slots__ = []
11
11
  Boolean: _ClassVar[VariableType_PB]
12
12
  Number: _ClassVar[VariableType_PB]
13
13
  String: _ClassVar[VariableType_PB]
14
14
  JSON: _ClassVar[VariableType_PB]
15
15
 
16
16
  class CustomDataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
17
- __slots__ = ()
17
+ __slots__ = []
18
18
  Bool: _ClassVar[CustomDataType]
19
19
  Num: _ClassVar[CustomDataType]
20
20
  Str: _ClassVar[CustomDataType]
@@ -29,7 +29,7 @@ Str: CustomDataType
29
29
  Null: CustomDataType
30
30
 
31
31
  class NullableString(_message.Message):
32
- __slots__ = ("value", "isNull")
32
+ __slots__ = ["value", "isNull"]
33
33
  VALUE_FIELD_NUMBER: _ClassVar[int]
34
34
  ISNULL_FIELD_NUMBER: _ClassVar[int]
35
35
  value: str
@@ -37,7 +37,7 @@ class NullableString(_message.Message):
37
37
  def __init__(self, value: _Optional[str] = ..., isNull: bool = ...) -> None: ...
38
38
 
39
39
  class NullableDouble(_message.Message):
40
- __slots__ = ("value", "isNull")
40
+ __slots__ = ["value", "isNull"]
41
41
  VALUE_FIELD_NUMBER: _ClassVar[int]
42
42
  ISNULL_FIELD_NUMBER: _ClassVar[int]
43
43
  value: float
@@ -45,7 +45,7 @@ class NullableDouble(_message.Message):
45
45
  def __init__(self, value: _Optional[float] = ..., isNull: bool = ...) -> None: ...
46
46
 
47
47
  class CustomDataValue(_message.Message):
48
- __slots__ = ("type", "boolValue", "doubleValue", "stringValue")
48
+ __slots__ = ["type", "boolValue", "doubleValue", "stringValue"]
49
49
  TYPE_FIELD_NUMBER: _ClassVar[int]
50
50
  BOOLVALUE_FIELD_NUMBER: _ClassVar[int]
51
51
  DOUBLEVALUE_FIELD_NUMBER: _ClassVar[int]
@@ -57,9 +57,9 @@ class CustomDataValue(_message.Message):
57
57
  def __init__(self, type: _Optional[_Union[CustomDataType, str]] = ..., boolValue: bool = ..., doubleValue: _Optional[float] = ..., stringValue: _Optional[str] = ...) -> None: ...
58
58
 
59
59
  class NullableCustomData(_message.Message):
60
- __slots__ = ("value", "isNull")
60
+ __slots__ = ["value", "isNull"]
61
61
  class ValueEntry(_message.Message):
62
- __slots__ = ("key", "value")
62
+ __slots__ = ["key", "value"]
63
63
  KEY_FIELD_NUMBER: _ClassVar[int]
64
64
  VALUE_FIELD_NUMBER: _ClassVar[int]
65
65
  key: str
@@ -72,7 +72,7 @@ class NullableCustomData(_message.Message):
72
72
  def __init__(self, value: _Optional[_Mapping[str, CustomDataValue]] = ..., isNull: bool = ...) -> None: ...
73
73
 
74
74
  class VariableForUserParams_PB(_message.Message):
75
- __slots__ = ("sdkKey", "variableKey", "variableType", "user", "shouldTrackEvent")
75
+ __slots__ = ["sdkKey", "variableKey", "variableType", "user", "shouldTrackEvent"]
76
76
  SDKKEY_FIELD_NUMBER: _ClassVar[int]
77
77
  VARIABLEKEY_FIELD_NUMBER: _ClassVar[int]
78
78
  VARIABLETYPE_FIELD_NUMBER: _ClassVar[int]
@@ -86,7 +86,7 @@ class VariableForUserParams_PB(_message.Message):
86
86
  def __init__(self, sdkKey: _Optional[str] = ..., variableKey: _Optional[str] = ..., variableType: _Optional[_Union[VariableType_PB, str]] = ..., user: _Optional[_Union[DVCUser_PB, _Mapping]] = ..., shouldTrackEvent: bool = ...) -> None: ...
87
87
 
88
88
  class DVCUser_PB(_message.Message):
89
- __slots__ = ("user_id", "email", "name", "language", "country", "appBuild", "appVersion", "deviceModel", "customData", "privateCustomData")
89
+ __slots__ = ["user_id", "email", "name", "language", "country", "appBuild", "appVersion", "deviceModel", "customData", "privateCustomData"]
90
90
  USER_ID_FIELD_NUMBER: _ClassVar[int]
91
91
  EMAIL_FIELD_NUMBER: _ClassVar[int]
92
92
  NAME_FIELD_NUMBER: _ClassVar[int]
@@ -110,7 +110,7 @@ class DVCUser_PB(_message.Message):
110
110
  def __init__(self, user_id: _Optional[str] = ..., email: _Optional[_Union[NullableString, _Mapping]] = ..., name: _Optional[_Union[NullableString, _Mapping]] = ..., language: _Optional[_Union[NullableString, _Mapping]] = ..., country: _Optional[_Union[NullableString, _Mapping]] = ..., appBuild: _Optional[_Union[NullableDouble, _Mapping]] = ..., appVersion: _Optional[_Union[NullableString, _Mapping]] = ..., deviceModel: _Optional[_Union[NullableString, _Mapping]] = ..., customData: _Optional[_Union[NullableCustomData, _Mapping]] = ..., privateCustomData: _Optional[_Union[NullableCustomData, _Mapping]] = ...) -> None: ...
111
111
 
112
112
  class SDKVariable_PB(_message.Message):
113
- __slots__ = ("_id", "type", "key", "boolValue", "doubleValue", "stringValue", "evalReason", "_feature", "eval")
113
+ __slots__ = ["_id", "type", "key", "boolValue", "doubleValue", "stringValue", "evalReason", "_feature", "eval"]
114
114
  _ID_FIELD_NUMBER: _ClassVar[int]
115
115
  TYPE_FIELD_NUMBER: _ClassVar[int]
116
116
  KEY_FIELD_NUMBER: _ClassVar[int]
@@ -132,7 +132,7 @@ class SDKVariable_PB(_message.Message):
132
132
  def __init__(self, _id: _Optional[str] = ..., type: _Optional[_Union[VariableType_PB, str]] = ..., key: _Optional[str] = ..., boolValue: bool = ..., doubleValue: _Optional[float] = ..., stringValue: _Optional[str] = ..., evalReason: _Optional[_Union[NullableString, _Mapping]] = ..., _feature: _Optional[_Union[NullableString, _Mapping]] = ..., eval: _Optional[_Union[EvalReason_PB, _Mapping]] = ...) -> None: ...
133
133
 
134
134
  class EvalReason_PB(_message.Message):
135
- __slots__ = ("reason", "details", "target_id")
135
+ __slots__ = ["reason", "details", "target_id"]
136
136
  REASON_FIELD_NUMBER: _ClassVar[int]
137
137
  DETAILS_FIELD_NUMBER: _ClassVar[int]
138
138
  TARGET_ID_FIELD_NUMBER: _ClassVar[int]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devcycle-python-server-sdk
3
- Version: 3.12.0
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
@@ -1,56 +1,60 @@
1
- devcycle_python_sdk/VERSION.txt,sha256=TuPL9BzJKZqNU-jwsDtquUrxVYO4LjhqEL1HqlHKDZU,7
1
+ devcycle_python_sdk/VERSION.txt,sha256=NxOs_9lT1XG8y-FjlRru-YinX5RcBJt_ulPwgDESZ_o,7
2
2
  devcycle_python_sdk/__init__.py,sha256=30TcYb9PbvVi97gwk16omCGG1UoUI9lmmU7xpvxNMvc,336
3
- devcycle_python_sdk/bucketing-lib.release.wasm,sha256=wpbXlOj_TVIO2OcOD781sW5ICmGsYe9pvxG6DwYw6FY,223167
4
- devcycle_python_sdk/cloud_client.py,sha256=LMKB6gEuQNn5YePUPO0t1QIAUXot4OyshSUlUbuu07U,9413
3
+ devcycle_python_sdk/bucketing-lib.release.wasm,sha256=6iooyHRR-GBfrBH0_mleWyaNHr9uM_nVPS-IXGROGCY,224670
4
+ devcycle_python_sdk/cloud_client.py,sha256=dR2ye29Up699hO1cdrAWeoXIOBPVoSvQ4aKe2gSBRno,9414
5
5
  devcycle_python_sdk/devcycle_client.py,sha256=OqNza7OXOSRmMQq2kmyivljmH1GyN4dTiz0EhvDvt8w,1069
6
6
  devcycle_python_sdk/exceptions.py,sha256=Et7fS8pEFbHkFY86T2pJoEUstwjh4_rzhJxVH8fzAns,1053
7
- devcycle_python_sdk/local_client.py,sha256=obZlHBJ9MS79sntdpPb4JTwC29e58Q5PzzEtei3m9F0,11684
8
- devcycle_python_sdk/options.py,sha256=W3vNZfV6QNnQyaOuRf6Z5cK6ut6fWtlGqlUfhzWX0_c,5253
7
+ devcycle_python_sdk/local_client.py,sha256=HbHIGLeIw9fHOSwp7RJ8ozIO_IqQ23bzSoGoGFLp0Kk,12083
8
+ devcycle_python_sdk/options.py,sha256=DRvr1IQRW3BDHl81TvORfCtt0qVW42c8PoYjMMPkzwQ,5252
9
9
  devcycle_python_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  devcycle_python_sdk/api/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
11
  devcycle_python_sdk/api/backoff.py,sha256=F6WYCloaZD9CjNu6mKBMq7K20S8-i2WVU6X05ZRoawE,303
12
12
  devcycle_python_sdk/api/bucketing_client.py,sha256=BRQWtTnIyLdWoFT7sPHPGg8kstDtW-jx1N4og3_SEgI,5565
13
13
  devcycle_python_sdk/api/config_client.py,sha256=73aQAq130rTHpQrVDX6n_DwFKz0WuKxRoLDiE3vSoAM,5077
14
14
  devcycle_python_sdk/api/event_client.py,sha256=WxkuV6geEC9MgG4FN36HeEQQa22ilq2mkOVMZkpiEk4,3356
15
- devcycle_python_sdk/api/local_bucketing.py,sha256=IXOP-680OmaO1XoX31YwY8FychAD5aqeyFFGe9SE_u4,15949
15
+ devcycle_python_sdk/api/local_bucketing.py,sha256=kXggXhv2PDQxDoLseuJwWbge8EgeqL0FWTmEIn8TUQc,16704
16
16
  devcycle_python_sdk/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- devcycle_python_sdk/managers/config_manager.py,sha256=AIGY9jXEhUzhEeaKIA1P5F2q6GaclrezAoGHgYU4dLQ,5634
18
- devcycle_python_sdk/managers/eval_hooks_manager.py,sha256=skOGK_hyZNpgkdeNXTTa-d1GVRITjoi8T6dO5I_Cqfk,2679
19
- devcycle_python_sdk/managers/event_queue_manager.py,sha256=uPy1vO6OAisZjUNAhWELE_g17alTiOcPv42z-WwkGck,8110
17
+ devcycle_python_sdk/managers/config_manager.py,sha256=QsJPMnf0wgNND3Yqhv125ckta_YFOMUb8I8pFq64tOE,5827
18
+ devcycle_python_sdk/managers/eval_hooks_manager.py,sha256=YOLj0IvNFqDbWvPA2aOqT15yKkwsdnZT9MQ7MHTv6M4,2963
19
+ devcycle_python_sdk/managers/event_queue_manager.py,sha256=8tqPZDD0-AWmz_NcWJBb80OIMzNBpDgbafpFefiOOmk,8109
20
20
  devcycle_python_sdk/managers/sse_manager.py,sha256=hSGpnZ6gNbQXGotqNxjogoB82znAyMMFhqdlqeCc5bI,2586
21
21
  devcycle_python_sdk/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  devcycle_python_sdk/models/bucketed_config.py,sha256=qTfhIViX7m2WS2oDBBCmKKzPsJG6DEav3zCEXwP27nA,3980
23
+ devcycle_python_sdk/models/config_metadata.py,sha256=HHhw2ncHOUAC3SH-IokVP4GrNLbflcGE9p7a2ZSuksA,1413
24
+ devcycle_python_sdk/models/environment_metadata.py,sha256=Zal9Ly15GaMcUBcD7CujNvtl5w_qfL8TQtLSRBpTHMo,629
23
25
  devcycle_python_sdk/models/error_response.py,sha256=r_dRUDAbral9UGaMYcnBsZOr9Hmkyi1SQbhGmZilgpA,442
24
- devcycle_python_sdk/models/eval_hook.py,sha256=_mDoEa8gkzMq-XpFRZrcXu_ldtpUzv5tlh-PghLpMMI,590
25
- devcycle_python_sdk/models/eval_hook_context.py,sha256=uBa8t0XxbcPBYAewNPFC2902qCjGhVoqUzZOZbGZ3n0,267
26
- devcycle_python_sdk/models/eval_reason.py,sha256=nMXcbR1DM8eriCQ1NNnIRQKkhJSYAbY8_9GgBfj7P8k,943
26
+ devcycle_python_sdk/models/eval_hook.py,sha256=k9w-AafvR-X7i8uf5nVuNEbD_PRS8QQmxW_90KVgGHA,742
27
+ devcycle_python_sdk/models/eval_hook_context.py,sha256=FYfMfXgtlW1O70khIcWF87yie2SWW4iWH2ehPex9paM,928
28
+ devcycle_python_sdk/models/eval_reason.py,sha256=obBjIWt8ZUh6knpVGsmCfWOCj4c7DXdH7K-FY-pchsU,901
27
29
  devcycle_python_sdk/models/event.py,sha256=Sj3eEopuPp8QwQl_Oz8E6Y9Ph6nLG0HxU7eR7xSbA7g,3722
28
30
  devcycle_python_sdk/models/feature.py,sha256=m3wxxIjC5jxhKpPLI4RcsBaNLuy-zsMqoAgc8ySPe0I,852
29
31
  devcycle_python_sdk/models/platform_data.py,sha256=nyQsgI6pcTd1FL3Y2ACmpvTYb08R_UtWul25OWZstps,849
32
+ devcycle_python_sdk/models/project_metadata.py,sha256=zRoE_oy_N3isqfrA5hAZ7S-7i5QBTSu9OGigFAvTqi8,602
30
33
  devcycle_python_sdk/models/user.py,sha256=mhEk-mnKJRg_tydpBlAVhGDj9rDCacJ-jNwiGeSpZiE,7027
31
34
  devcycle_python_sdk/models/variable.py,sha256=NXubhYWXYRAUEh4kDe2Tu9T5QnZmQ3roYVPbP-u2H3s,3030
35
+ devcycle_python_sdk/models/variable_metadata.py,sha256=oeH5fxDFLgAyc_B1RuOYRp4w68lTaBxTPvNgnXz7alE,583
32
36
  devcycle_python_sdk/open_feature_provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
37
  devcycle_python_sdk/open_feature_provider/provider.py,sha256=l-psKU5m6jSBE53VwX3fBBas-TztekFDlfYTyTJQm6c,5558
34
38
  devcycle_python_sdk/protobuf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
- devcycle_python_sdk/protobuf/utils.py,sha256=6cBL8GZ5nCZw6JoO7lAdgCj9wM1u0lp6QFQMnGE1fiM,5414
36
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.py,sha256=yawFsQ7m-6OB7DxrjXo2FLLkHhiTv6lDnjubhjAgw_8,5198
37
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi,sha256=tfN0zBapUPA9WGqUG5lU5XXgH_BsHTt5mgXKGsyS4-I,6623
39
+ devcycle_python_sdk/protobuf/utils.py,sha256=SwsXH5QGAfbSn1FvgwO3x4W8uTYLsL1r3E-P5nDD4Nw,5528
40
+ devcycle_python_sdk/protobuf/variableForUserParams_pb2.py,sha256=SIFCo4AbjtlWQh2DYxkrWCR85PQajA4PIofXoRe4Gds,4956
41
+ devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi,sha256=OU4ORhjnbXy0AqdkxyARxg4JXGTPDuDzBW2W9RGdcMs,6623
38
42
  devcycle_python_sdk/util/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
39
43
  devcycle_python_sdk/util/strings.py,sha256=gH0Xm3gIXNqn6hczKPk10Jn5_9knuD7OB5MrB7bqztw,252
40
44
  devcycle_python_sdk/util/version.py,sha256=Cz7Us_EuXP-T8GI47g2WHYdBJj_zKo-5y545IDHJAjI,253
41
- devcycle_python_server_sdk-3.12.0.dist-info/licenses/LICENSE,sha256=tHQMqu-Jic2UpMGqb-vNFbGygysl8IG4l1clnnh0OFo,1071
45
+ devcycle_python_server_sdk-3.13.0.dist-info/licenses/LICENSE,sha256=tHQMqu-Jic2UpMGqb-vNFbGygysl8IG4l1clnnh0OFo,1071
42
46
  example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
47
  example/cloud_client_example.py,sha256=YD8ZsTv3dPYfbFUKraRpRT0VsJGrPwjm8ZCuA5H54yI,2463
44
48
  example/local_bucketing_client_example.py,sha256=TaIfy3VnW9fXii9NPYs5UOuP4fLKruXMjnZApveqzl4,2796
45
49
  example/openfeature_example.py,sha256=kO6F-Xk20l6CPiMfIxSRreshEGmHMFHRg3zbeLXa3AI,2410
46
50
  test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
- test/test_cloud_client.py,sha256=oqOWh4aa07ObJOJeEjXMon0O2jPW_lYZMVInbFR-uzI,14586
48
- test/test_local_client.py,sha256=d0D0h2vqSiZItiqGXrlFXRRmuZG-TNg8MT84dY6w0Ks,16250
51
+ test/test_cloud_client.py,sha256=CoK4xfHPMGyJ4hXrdI98e5f4zEL51vx31u_xmC3FEtc,15957
52
+ test/test_local_client.py,sha256=yg19ddGmg9t1TfjT-4EgHp0cjfxHjfJZOGIFM8mSNX8,18078
49
53
  test/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
54
  test/api/test_bucketing_client.py,sha256=M2mjACowLvbsIjXdP-w8-fwY02xqv1X1r_h9v-pXa7A,7739
51
55
  test/api/test_config_client.py,sha256=3BwMm4y-4NQcdW2DDGppoJLIF-ffgjlUJti2Qjw1D1M,5129
52
56
  test/api/test_event_client.py,sha256=v4Xoum-PdywfczrfMV1llM5vrIJoqHybzxPLVokh09g,3310
53
- test/api/test_local_bucketing.py,sha256=0cbD8kiRYCVadvU-ldMES-sr7rV9Ld4H1q62HCx4AHU,15970
57
+ test/api/test_local_bucketing.py,sha256=ECouaWpgWrlfEBtTK5OavmYX87XiXKg-9epmzTOCMdE,16126
54
58
  test/fixture/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
59
  test/fixture/data.py,sha256=6ReLxWPMhCDbJ5Ex24_iR0PJNjKzPFV1I9j_v0FVbBU,1462
56
60
  test/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -58,15 +62,15 @@ test/managers/test_config_manager.py,sha256=2TUnt8501KM7UlJNH7srIhvC-izehD7nOfqZ
58
62
  test/managers/test_event_queue_manager.py,sha256=z850atk8zAlD7sYaA1OJwgCFcfC0ps4NEk4XUnNCq1U,11629
59
63
  test/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
64
  test/models/test_bucketed_config.py,sha256=8wCEyxaHxY21nl7ZbiFzp-IzZQBZIPIVPPzeREjumQk,858
61
- test/models/test_user.py,sha256=ZUbFUndYrnefyGl8rxZgG3HdHPZs4BosQWimkT7etJc,10540
65
+ test/models/test_user.py,sha256=nuGJvR2KHilRlx5DDEzh1F-lpZ7qT7H88kqPvfcFfM8,10539
62
66
  test/openfeature_test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
- test/openfeature_test/test_provider.py,sha256=hIzdqqBTulGRODeTcRJUP93v76WEolUC2QRe3DccF3U,10932
67
+ test/openfeature_test/test_provider.py,sha256=vj8eKprkF-psRdy54AtitYoEqBJKhDpfEzEDdhM-3FE,10931
64
68
  test/openfeature_test/test_provider_local_sdk.py,sha256=ExkA1LYLuvW-dD4Bw2BKggzDQ8GpnIp3WyNK4rUZgJQ,5696
65
69
  test/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
70
  test/util/test_strings.py,sha256=T5H5U0af7_ld1yJC8IdDmay_Mx-iJQ5Fi5fbvKcAc7A,862
67
71
  test/util/test_utils.py,sha256=tVBJm_qnqg-4PssLpXIxqlk0JehuiAIPs4zpi2oNVnY,6671
68
72
  test/util/test_version.py,sha256=3Xg0_84m0bdHXz6pxd2Z85Oz9DoUC1T0Ol_WqEH_jac,335
69
- devcycle_python_server_sdk-3.12.0.dist-info/METADATA,sha256=-bA66ayqIdC5UFrsLx0jZ9m4eNSHzcdTuFvA-sY-IQE,723
70
- devcycle_python_server_sdk-3.12.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
71
- devcycle_python_server_sdk-3.12.0.dist-info/top_level.txt,sha256=34f8zDzihAHoH7siAFlmscyO_QLNWWq9HApFlwtYL4k,33
72
- devcycle_python_server_sdk-3.12.0.dist-info/RECORD,,
73
+ devcycle_python_server_sdk-3.13.0.dist-info/METADATA,sha256=4RisdQtReQqBoSXMquKU0Ui47Z5bbnmAVmrdOLt4hsI,723
74
+ devcycle_python_server_sdk-3.13.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
75
+ devcycle_python_server_sdk-3.13.0.dist-info/top_level.txt,sha256=34f8zDzihAHoH7siAFlmscyO_QLNWWq9HApFlwtYL4k,33
76
+ devcycle_python_server_sdk-3.13.0.dist-info/RECORD,,
@@ -98,7 +98,7 @@ class LocalBucketingTest(unittest.TestCase):
98
98
  self.local_bucketing.set_platform_data(platform_json)
99
99
  self.local_bucketing.init_event_queue(self.client_uuid, "{}")
100
100
  user = DevCycleUser(user_id="test_user_id")
101
- result = self.local_bucketing.get_variable_for_user_protobuf(
101
+ result, feature_id = self.local_bucketing.get_variable_for_user_protobuf(
102
102
  user=user, key="string-var", default_value="default value"
103
103
  )
104
104
  self.assertIsNotNone(result)
@@ -106,6 +106,7 @@ class LocalBucketingTest(unittest.TestCase):
106
106
  self.assertEqual(result.type, TypeEnum.STRING)
107
107
  self.assertEqual(result.value, "variationOn")
108
108
  self.assertFalse(result.isDefaulted)
109
+ self.assertIsNotNone(feature_id)
109
110
 
110
111
  def test_get_variable_for_user_protobuf_special_characters(self):
111
112
  self.local_bucketing.store_config(special_character_config())
@@ -113,7 +114,7 @@ class LocalBucketingTest(unittest.TestCase):
113
114
  self.local_bucketing.set_platform_data(platform_json)
114
115
  self.local_bucketing.init_event_queue(self.client_uuid, "{}")
115
116
  user = DevCycleUser(user_id="test_user_id")
116
- result = self.local_bucketing.get_variable_for_user_protobuf(
117
+ result, feature_id = self.local_bucketing.get_variable_for_user_protobuf(
117
118
  user=user, key="string-var", default_value="default value"
118
119
  )
119
120
  self.assertIsNotNone(result)
@@ -121,6 +122,7 @@ class LocalBucketingTest(unittest.TestCase):
121
122
  self.assertEqual(result.type, TypeEnum.STRING)
122
123
  self.assertEqual(result.value, "öé 🐍 ¥ variationOn")
123
124
  self.assertFalse(result.isDefaulted)
125
+ self.assertIsNotNone(feature_id)
124
126
 
125
127
  def test_get_variable_for_user_protobuf_type_mismatch(self):
126
128
  self.local_bucketing.store_config(small_config())
@@ -131,10 +133,11 @@ class LocalBucketingTest(unittest.TestCase):
131
133
 
132
134
  # type mismatch is handled inside the WASM and will return
133
135
  # no data if the type is not correct
134
- result = self.local_bucketing.get_variable_for_user_protobuf(
136
+ result, feature_id = self.local_bucketing.get_variable_for_user_protobuf(
135
137
  user=user, key="string-var", default_value=9999
136
138
  )
137
139
  self.assertIsNone(result)
140
+ self.assertIsNone(feature_id)
138
141
 
139
142
  def test_generate_bucketed_config(self):
140
143
  self.local_bucketing.store_config(small_config())
test/models/test_user.py CHANGED
@@ -6,7 +6,6 @@ from devcycle_python_sdk.models.user import DevCycleUser
6
6
  from openfeature.provider import EvaluationContext
7
7
  from openfeature.exception import TargetingKeyMissingError, InvalidContextError
8
8
 
9
-
10
9
  logger = logging.getLogger(__name__)
11
10
 
12
11
 
@@ -21,7 +21,6 @@ from devcycle_python_sdk.open_feature_provider.provider import (
21
21
  DevCycleProvider,
22
22
  )
23
23
 
24
-
25
24
  logger = logging.getLogger(__name__)
26
25
 
27
26
 
test/test_cloud_client.py CHANGED
@@ -5,7 +5,6 @@ import uuid
5
5
  from time import time
6
6
  from unittest.mock import patch
7
7
 
8
-
9
8
  from devcycle_python_sdk import DevCycleCloudClient, DevCycleCloudOptions
10
9
  from devcycle_python_sdk.models.eval_hook import EvalHook
11
10
  from devcycle_python_sdk.models.user import DevCycleUser
@@ -99,7 +98,7 @@ class DevCycleCloudClientTest(unittest.TestCase):
99
98
  self.assertEqual(result.value, "default_value")
100
99
  self.assertTrue(result.isDefaulted)
101
100
  self.assertEqual(result.eval.reason, "DEFAULT")
102
- self.assertEqual(result.eval.details, "Missing Variable")
101
+ self.assertEqual(result.eval.details, "Error")
103
102
 
104
103
  # other exception - return default
105
104
  mock_variable_call.reset_mock()
@@ -307,10 +306,10 @@ class DevCycleCloudClientTest(unittest.TestCase):
307
306
  hook_called["before"] = True
308
307
  return context
309
308
 
310
- def after_hook(context, variable):
309
+ def after_hook(context, variable, variable_metadata):
311
310
  hook_called["after"] = True
312
311
 
313
- def finally_hook(context, variable):
312
+ def finally_hook(context, variable, variable_metadata):
314
313
  hook_called["finally"] = True
315
314
 
316
315
  def error_hook(context, error):
@@ -347,10 +346,10 @@ class DevCycleCloudClientTest(unittest.TestCase):
347
346
  hook_called["before"] = True
348
347
  raise Exception("Before hook failed")
349
348
 
350
- def after_hook(context, variable):
349
+ def after_hook(context, variable, variable_metadata):
351
350
  hook_called["after"] = True
352
351
 
353
- def finally_hook(context, variable):
352
+ def finally_hook(context, variable, variable_metadata):
354
353
  hook_called["finally"] = True
355
354
 
356
355
  def error_hook(context, error):
@@ -370,6 +369,42 @@ class DevCycleCloudClientTest(unittest.TestCase):
370
369
  self.assertTrue(hook_called["finally"])
371
370
  self.assertTrue(hook_called["error"])
372
371
 
372
+ @patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable")
373
+ def test_context_has_null_config_metadata(self, mock_variable_call):
374
+ mock_variable_call.return_value = Variable(
375
+ _id="123", key="strKey", value=999, type=TypeEnum.NUMBER
376
+ )
377
+
378
+ context_received = None
379
+
380
+ def before_hook(context):
381
+ nonlocal context_received
382
+ context_received = context
383
+ return context
384
+
385
+ def after_hook(context, variable, variable_metadata):
386
+ pass
387
+
388
+ def finally_hook(context, variable, variable_metadata):
389
+ pass
390
+
391
+ def error_hook(context, error):
392
+ pass
393
+
394
+ self.test_client.add_hook(
395
+ EvalHook(before_hook, after_hook, finally_hook, error_hook)
396
+ )
397
+
398
+ # Test that context has config_metadata field but it's null for cloud client
399
+ variable = self.test_client.variable(self.test_user, "strKey", 42)
400
+
401
+ # Verify the variable evaluation worked
402
+ self.assertIsNotNone(variable)
403
+ self.assertIsNotNone(context_received)
404
+ self.assertTrue(hasattr(context_received, "config_metadata"))
405
+ # Cloud client should have null config_metadata since it's not implemented
406
+ self.assertIsNone(context_received.config_metadata)
407
+
373
408
 
374
409
  if __name__ == "__main__":
375
410
  unittest.main()
test/test_local_client.py CHANGED
@@ -392,10 +392,10 @@ class DevCycleLocalClientTest(unittest.TestCase):
392
392
  hook_called["before"] = True
393
393
  return context
394
394
 
395
- def after_hook(context, variable):
395
+ def after_hook(context, variable, variable_metadata):
396
396
  hook_called["after"] = True
397
397
 
398
- def finally_hook(context, variable):
398
+ def finally_hook(context, variable, variable_metadata):
399
399
  hook_called["finally"] = True
400
400
 
401
401
  def error_hook(context, error):
@@ -432,10 +432,10 @@ class DevCycleLocalClientTest(unittest.TestCase):
432
432
  hook_called["before"] = True
433
433
  raise Exception("Before hook failed")
434
434
 
435
- def after_hook(context, variable):
435
+ def after_hook(context, variable, variable_metadata):
436
436
  hook_called["after"] = True
437
437
 
438
- def finally_hook(context, variable):
438
+ def finally_hook(context, variable, variable_metadata):
439
439
  hook_called["finally"] = True
440
440
 
441
441
  def error_hook(context, error):
@@ -457,6 +457,55 @@ class DevCycleLocalClientTest(unittest.TestCase):
457
457
  self.assertTrue(hook_called["finally"])
458
458
  self.assertTrue(hook_called["error"])
459
459
 
460
+ @responses.activate
461
+ def test_context_has_config_metadata(self):
462
+ self.setup_client()
463
+
464
+ context_received = None
465
+
466
+ def before_hook(context):
467
+ nonlocal context_received
468
+ context_received = context
469
+ return context
470
+
471
+ def after_hook(context, variable, variable_metadata):
472
+ pass
473
+
474
+ def finally_hook(context, variable, variable_metadata):
475
+ pass
476
+
477
+ def error_hook(context, error):
478
+ pass
479
+
480
+ self.client.add_hook(
481
+ EvalHook(before_hook, after_hook, finally_hook, error_hook)
482
+ )
483
+
484
+ user = DevCycleUser(user_id="1234")
485
+
486
+ # Test that context has config_metadata field
487
+ variable = self.client.variable(user, "num-var", 42)
488
+
489
+ # Verify the variable evaluation worked
490
+ self.assertIsNotNone(variable)
491
+ self.assertIsNotNone(context_received)
492
+ self.assertTrue(hasattr(context_received, "config_metadata"))
493
+ # For local client, config_metadata should be populated
494
+ self.assertIsNotNone(context_received.config_metadata)
495
+ self.assertTrue(hasattr(context_received.config_metadata, "project"))
496
+ self.assertTrue(hasattr(context_received.config_metadata, "environment"))
497
+ # Verify the project and environment data
498
+ self.assertEqual(
499
+ context_received.config_metadata.project.id, "61f97628ff4afcb6d057dbf0"
500
+ )
501
+ self.assertEqual(context_received.config_metadata.project.key, "emma-project")
502
+ self.assertEqual(
503
+ context_received.config_metadata.environment.id, "61f97628ff4afcb6d057dbf2"
504
+ )
505
+ self.assertEqual(
506
+ context_received.config_metadata.environment.key, "development"
507
+ )
508
+
460
509
 
461
510
  def _benchmark_variable_call(client: DevCycleLocalClient, user: DevCycleUser, key: str):
462
511
  return client.variable(user, key, "default_value")