From f4420a1cc4c44fbe922006d3772c05996b328fd0 Mon Sep 17 00:00:00 2001 From: Jason Salaber Date: Tue, 5 Aug 2025 17:03:53 -0400 Subject: [PATCH 1/2] feat: add config metadata to eval hooks for local bucketing --- devcycle_python_sdk/api/local_bucketing.py | 10 +++++ .../bucketing-lib.release.wasm | Bin 223167 -> 224670 bytes devcycle_python_sdk/local_client.py | 4 +- .../managers/config_manager.py | 3 ++ devcycle_python_sdk/models/config_metadata.py | 25 +++++++++++ .../models/environment_metadata.py | 17 ++++++++ .../models/eval_hook_context.py | 5 ++- .../models/project_metadata.py | 17 ++++++++ .../protobuf/variableForUserParams_pb2.py | 22 +++------- .../protobuf/variableForUserParams_pb2.pyi | 22 +++++----- test/test_cloud_client.py | 34 +++++++++++++++ test/test_local_client.py | 41 ++++++++++++++++++ update_wasm_lib.sh | 2 +- 13 files changed, 171 insertions(+), 31 deletions(-) create mode 100644 devcycle_python_sdk/models/config_metadata.py create mode 100644 devcycle_python_sdk/models/environment_metadata.py create mode 100644 devcycle_python_sdk/models/project_metadata.py diff --git a/devcycle_python_sdk/api/local_bucketing.py b/devcycle_python_sdk/api/local_bucketing.py index 5786bf2..3691694 100644 --- a/devcycle_python_sdk/api/local_bucketing.py +++ b/devcycle_python_sdk/api/local_bucketing.py @@ -28,6 +28,7 @@ from devcycle_python_sdk.models.user import DevCycleUser from devcycle_python_sdk.models.variable import Variable, determine_variable_type from devcycle_python_sdk.models.event import FlushPayload +from devcycle_python_sdk.models.config_metadata import ConfigMetadata logger = logging.getLogger(__name__) @@ -140,6 +141,7 @@ def __console_log_func(message_ptr) -> None: "generateBucketedConfigForUserUTF8" ) self.VariableForUserProtobuf = self._get_export("variableForUser_PB") + self.getConfigMetadata = self._get_export("getConfigMetadata") # Extract variable type enum values from WASM self.variable_type_map = { @@ -357,6 +359,14 @@ def store_config(self, config_json: str) -> None: config_addr = self._new_assembly_script_byte_array(data) self.setConfigDataUTF8(self.wasm_store, self.sdk_key_addr, config_addr) + def get_config_metadata(self) -> dict: + with self.wasm_lock: + config_addr = self.getConfigMetadata(self.wasm_store, self.sdk_key_addr) + config_bytes = self._read_assembly_script_string(config_addr) + config_data = json.loads(config_bytes.encode("utf-8")) + + return ConfigMetadata.from_json(config_data) + def set_platform_data(self, platform_json: str) -> None: with self.wasm_lock: data = platform_json.encode("utf-8") diff --git a/devcycle_python_sdk/bucketing-lib.release.wasm b/devcycle_python_sdk/bucketing-lib.release.wasm index 80994caaf48cfdb6c52d2eee9d4df25d479899fd..bb1855666c54b5432e1c9a46027100a9d4860ef7 100644 GIT binary patch delta 42908 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 z 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 diff --git a/devcycle_python_sdk/local_client.py b/devcycle_python_sdk/local_client.py index 6f687ff..e5bd267 100644 --- a/devcycle_python_sdk/local_client.py +++ b/devcycle_python_sdk/local_client.py @@ -150,7 +150,9 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable key, default_value, DefaultReasonDetails.MISSING_CONFIG ) - context = HookContext(key, user, default_value) + config_metadata = self.local_bucketing.get_config_metadata() + + context = HookContext(key, user, default_value, config_metadata) variable = Variable.create_default_variable( key=key, default_value=default_value ) diff --git a/devcycle_python_sdk/managers/config_manager.py b/devcycle_python_sdk/managers/config_manager.py index 932050f..d0801e2 100644 --- a/devcycle_python_sdk/managers/config_manager.py +++ b/devcycle_python_sdk/managers/config_manager.py @@ -108,6 +108,9 @@ def _get_config(self, last_modified: Optional[float] = None): ) self._polling_enabled = False + def get_config_metadata(self) -> dict: + return self._local_bucketing.get_config_metadata() + def run(self): while self._polling_enabled: try: diff --git a/devcycle_python_sdk/models/config_metadata.py b/devcycle_python_sdk/models/config_metadata.py new file mode 100644 index 0000000..9123d75 --- /dev/null +++ b/devcycle_python_sdk/models/config_metadata.py @@ -0,0 +1,25 @@ +from devcycle_python_sdk.models.environment_metadata import EnvironmentMetadata +from devcycle_python_sdk.models.project_metadata import ProjectMetadata +import json + + +class ConfigMetadata: + def __init__( + self, + project: ProjectMetadata, + environment: EnvironmentMetadata, + ): + self.project = project + self.environment = environment + + def to_json(self) -> str: + return json.dumps(self, default=lambda o: o.__dict__) + + @staticmethod + def from_json(json_str: str) -> "ConfigMetadata": + if json_str is None: + return None + return ConfigMetadata( + project=ProjectMetadata.from_json(json_str["project"]), + environment=EnvironmentMetadata.from_json(json_str["environment"]), + ) \ No newline at end of file diff --git a/devcycle_python_sdk/models/environment_metadata.py b/devcycle_python_sdk/models/environment_metadata.py new file mode 100644 index 0000000..c88290a --- /dev/null +++ b/devcycle_python_sdk/models/environment_metadata.py @@ -0,0 +1,17 @@ +class EnvironmentMetadata: + def __init__( + self, + id: str, + key: str, + ): + self.id = id + self.key = key + + @staticmethod + def from_json(json_str: str) -> "EnvironmentMetadata": + if json_str is None: + return None + return EnvironmentMetadata( + id=json_str["id"], + key=json_str["key"], + ) \ No newline at end of file diff --git a/devcycle_python_sdk/models/eval_hook_context.py b/devcycle_python_sdk/models/eval_hook_context.py index 5c3ee96..76b7fe7 100644 --- a/devcycle_python_sdk/models/eval_hook_context.py +++ b/devcycle_python_sdk/models/eval_hook_context.py @@ -1,10 +1,11 @@ from typing import Any from devcycle_python_sdk.models.user import DevCycleUser - +from devcycle_python_sdk.models.config_metadata import ConfigMetadata class HookContext: - def __init__(self, key: str, user: DevCycleUser, default_value: Any): + def __init__(self, key: str, user: DevCycleUser, default_value: Any, config_metadata: ConfigMetadata = None): self.key = key self.default_value = default_value self.user = user + self.config_metadata = config_metadata diff --git a/devcycle_python_sdk/models/project_metadata.py b/devcycle_python_sdk/models/project_metadata.py new file mode 100644 index 0000000..22476ce --- /dev/null +++ b/devcycle_python_sdk/models/project_metadata.py @@ -0,0 +1,17 @@ +class ProjectMetadata: + def __init__( + self, + id: str, + key: str, + ): + self.id = id + self.key = key + + @staticmethod + def from_json(json_str: str) -> "ProjectMetadata": + if json_str is None: + return None + return ProjectMetadata( + id=json_str["id"], + key=json_str["key"], + ) \ No newline at end of file diff --git a/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py b/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py index 289f5b9..324e9e9 100644 --- a/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +++ b/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py @@ -1,22 +1,11 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE # source: variableForUserParams.proto -# Protobuf Python Version: 5.29.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 29, - 3, - '', - 'variableForUserParams.proto' -) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -29,11 +18,12 @@ _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'variableForUserParams_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n&com.devcycle.sdk.server.local.protobufP\001Z\007./proto\252\002\"DevCycle.SDK.Server.Local.Protobuf' - _globals['_NULLABLECUSTOMDATA_VALUEENTRY']._loaded_options = None - _globals['_NULLABLECUSTOMDATA_VALUEENTRY']._serialized_options = b'8\001' +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n&com.devcycle.sdk.server.local.protobufP\001Z\007./proto\252\002\"DevCycle.SDK.Server.Local.Protobuf' + _NULLABLECUSTOMDATA_VALUEENTRY._options = None + _NULLABLECUSTOMDATA_VALUEENTRY._serialized_options = b'8\001' _globals['_VARIABLETYPE_PB']._serialized_start=1221 _globals['_VARIABLETYPE_PB']._serialized_end=1285 _globals['_CUSTOMDATATYPE']._serialized_start=1287 diff --git a/devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi b/devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi index 175eee2..022c336 100644 --- a/devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi +++ b/devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi @@ -7,14 +7,14 @@ from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Opti DESCRIPTOR: _descriptor.FileDescriptor class VariableType_PB(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () + __slots__ = [] Boolean: _ClassVar[VariableType_PB] Number: _ClassVar[VariableType_PB] String: _ClassVar[VariableType_PB] JSON: _ClassVar[VariableType_PB] class CustomDataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () + __slots__ = [] Bool: _ClassVar[CustomDataType] Num: _ClassVar[CustomDataType] Str: _ClassVar[CustomDataType] @@ -29,7 +29,7 @@ Str: CustomDataType Null: CustomDataType class NullableString(_message.Message): - __slots__ = ("value", "isNull") + __slots__ = ["value", "isNull"] VALUE_FIELD_NUMBER: _ClassVar[int] ISNULL_FIELD_NUMBER: _ClassVar[int] value: str @@ -37,7 +37,7 @@ class NullableString(_message.Message): def __init__(self, value: _Optional[str] = ..., isNull: bool = ...) -> None: ... class NullableDouble(_message.Message): - __slots__ = ("value", "isNull") + __slots__ = ["value", "isNull"] VALUE_FIELD_NUMBER: _ClassVar[int] ISNULL_FIELD_NUMBER: _ClassVar[int] value: float @@ -45,7 +45,7 @@ class NullableDouble(_message.Message): def __init__(self, value: _Optional[float] = ..., isNull: bool = ...) -> None: ... class CustomDataValue(_message.Message): - __slots__ = ("type", "boolValue", "doubleValue", "stringValue") + __slots__ = ["type", "boolValue", "doubleValue", "stringValue"] TYPE_FIELD_NUMBER: _ClassVar[int] BOOLVALUE_FIELD_NUMBER: _ClassVar[int] DOUBLEVALUE_FIELD_NUMBER: _ClassVar[int] @@ -57,9 +57,9 @@ class CustomDataValue(_message.Message): def __init__(self, type: _Optional[_Union[CustomDataType, str]] = ..., boolValue: bool = ..., doubleValue: _Optional[float] = ..., stringValue: _Optional[str] = ...) -> None: ... class NullableCustomData(_message.Message): - __slots__ = ("value", "isNull") + __slots__ = ["value", "isNull"] class ValueEntry(_message.Message): - __slots__ = ("key", "value") + __slots__ = ["key", "value"] KEY_FIELD_NUMBER: _ClassVar[int] VALUE_FIELD_NUMBER: _ClassVar[int] key: str @@ -72,7 +72,7 @@ class NullableCustomData(_message.Message): def __init__(self, value: _Optional[_Mapping[str, CustomDataValue]] = ..., isNull: bool = ...) -> None: ... class VariableForUserParams_PB(_message.Message): - __slots__ = ("sdkKey", "variableKey", "variableType", "user", "shouldTrackEvent") + __slots__ = ["sdkKey", "variableKey", "variableType", "user", "shouldTrackEvent"] SDKKEY_FIELD_NUMBER: _ClassVar[int] VARIABLEKEY_FIELD_NUMBER: _ClassVar[int] VARIABLETYPE_FIELD_NUMBER: _ClassVar[int] @@ -86,7 +86,7 @@ class VariableForUserParams_PB(_message.Message): def __init__(self, sdkKey: _Optional[str] = ..., variableKey: _Optional[str] = ..., variableType: _Optional[_Union[VariableType_PB, str]] = ..., user: _Optional[_Union[DVCUser_PB, _Mapping]] = ..., shouldTrackEvent: bool = ...) -> None: ... class DVCUser_PB(_message.Message): - __slots__ = ("user_id", "email", "name", "language", "country", "appBuild", "appVersion", "deviceModel", "customData", "privateCustomData") + __slots__ = ["user_id", "email", "name", "language", "country", "appBuild", "appVersion", "deviceModel", "customData", "privateCustomData"] USER_ID_FIELD_NUMBER: _ClassVar[int] EMAIL_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] @@ -110,7 +110,7 @@ class DVCUser_PB(_message.Message): 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: ... class SDKVariable_PB(_message.Message): - __slots__ = ("_id", "type", "key", "boolValue", "doubleValue", "stringValue", "evalReason", "_feature", "eval") + __slots__ = ["_id", "type", "key", "boolValue", "doubleValue", "stringValue", "evalReason", "_feature", "eval"] _ID_FIELD_NUMBER: _ClassVar[int] TYPE_FIELD_NUMBER: _ClassVar[int] KEY_FIELD_NUMBER: _ClassVar[int] @@ -132,7 +132,7 @@ class SDKVariable_PB(_message.Message): 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: ... class EvalReason_PB(_message.Message): - __slots__ = ("reason", "details", "target_id") + __slots__ = ["reason", "details", "target_id"] REASON_FIELD_NUMBER: _ClassVar[int] DETAILS_FIELD_NUMBER: _ClassVar[int] TARGET_ID_FIELD_NUMBER: _ClassVar[int] diff --git a/test/test_cloud_client.py b/test/test_cloud_client.py index 7f74327..a7c1176 100644 --- a/test/test_cloud_client.py +++ b/test/test_cloud_client.py @@ -370,6 +370,40 @@ def error_hook(context, error): self.assertTrue(hook_called["finally"]) self.assertTrue(hook_called["error"]) + @patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable") + def test_context_has_null_config_metadata(self, mock_variable_call): + mock_variable_call.return_value = Variable( + _id="123", key="strKey", value=999, type=TypeEnum.NUMBER + ) + + context_received = None + + def before_hook(context): + nonlocal context_received + context_received = context + return context + + def after_hook(context, variable): + pass + + def finally_hook(context, variable): + pass + + def error_hook(context, error): + pass + + self.test_client.add_hook( + EvalHook(before_hook, after_hook, finally_hook, error_hook) + ) + + # Test that context has config_metadata field but it's null for cloud client + variable = self.test_client.variable(self.test_user, "strKey", 42) + + self.assertIsNotNone(context_received) + self.assertTrue(hasattr(context_received, 'config_metadata')) + # Cloud client should have null config_metadata since it's not implemented + self.assertIsNone(context_received.config_metadata) + if __name__ == "__main__": unittest.main() diff --git a/test/test_local_client.py b/test/test_local_client.py index 7764bd7..4538034 100644 --- a/test/test_local_client.py +++ b/test/test_local_client.py @@ -457,6 +457,47 @@ def error_hook(context, error): self.assertTrue(hook_called["finally"]) self.assertTrue(hook_called["error"]) + @responses.activate + def test_context_has_config_metadata(self): + self.setup_client() + + context_received = None + + def before_hook(context): + nonlocal context_received + context_received = context + return context + + def after_hook(context, variable): + pass + + def finally_hook(context, variable): + pass + + def error_hook(context, error): + pass + + self.client.add_hook( + EvalHook(before_hook, after_hook, finally_hook, error_hook) + ) + + user = DevCycleUser(user_id="1234") + + # Test that context has config_metadata field + variable = self.client.variable(user, "num-var", 42) + + self.assertIsNotNone(context_received) + self.assertTrue(hasattr(context_received, 'config_metadata')) + # For local client, config_metadata should be populated + self.assertIsNotNone(context_received.config_metadata) + self.assertTrue(hasattr(context_received.config_metadata, 'project')) + self.assertTrue(hasattr(context_received.config_metadata, 'environment')) + # Verify the project and environment data + self.assertEqual(context_received.config_metadata.project.id, "61f97628ff4afcb6d057dbf0") + self.assertEqual(context_received.config_metadata.project.key, "emma-project") + self.assertEqual(context_received.config_metadata.environment.id, "61f97628ff4afcb6d057dbf2") + self.assertEqual(context_received.config_metadata.environment.key, "development") + def _benchmark_variable_call(client: DevCycleLocalClient, user: DevCycleUser, key: str): return client.variable(user, key, "default_value") diff --git a/update_wasm_lib.sh b/update_wasm_lib.sh index 1200e7a..3508f6d 100755 --- a/update_wasm_lib.sh +++ b/update_wasm_lib.sh @@ -1,6 +1,6 @@ #!/bin/bash -BUCKETING_LIB_VERSION="1.41.0" +BUCKETING_LIB_VERSION="1.42.1" if [[ -n "$1" ]]; then BUCKETING_LIB_VERSION="$1" From da1dec88427a9bb4a47eca18184ff29fd0875ee7 Mon Sep 17 00:00:00 2001 From: Jason Salaber Date: Thu, 7 Aug 2025 11:08:47 -0400 Subject: [PATCH 2/2] chore: linter fixes --- devcycle_python_sdk/api/local_bucketing.py | 2 +- .../managers/config_manager.py | 3 +- devcycle_python_sdk/models/config_metadata.py | 17 +++-- .../models/environment_metadata.py | 15 +++-- .../models/eval_hook_context.py | 11 +++- .../models/project_metadata.py | 13 ++-- devcycle_python_sdk/options.py | 2 +- devcycle_python_sdk/protobuf/utils.py | 12 +++- .../protobuf/variableForUserParams_pb2.py | 64 ++++++++++--------- example/django-app/manage.py | 1 + requirements.txt | 2 +- test/test_cloud_client.py | 10 +-- test/test_local_client.py | 26 +++++--- 13 files changed, 110 insertions(+), 68 deletions(-) diff --git a/devcycle_python_sdk/api/local_bucketing.py b/devcycle_python_sdk/api/local_bucketing.py index 3691694..a4c1c59 100644 --- a/devcycle_python_sdk/api/local_bucketing.py +++ b/devcycle_python_sdk/api/local_bucketing.py @@ -359,7 +359,7 @@ def store_config(self, config_json: str) -> None: config_addr = self._new_assembly_script_byte_array(data) self.setConfigDataUTF8(self.wasm_store, self.sdk_key_addr, config_addr) - def get_config_metadata(self) -> dict: + def get_config_metadata(self) -> Optional[ConfigMetadata]: with self.wasm_lock: config_addr = self.getConfigMetadata(self.wasm_store, self.sdk_key_addr) config_bytes = self._read_assembly_script_string(config_addr) diff --git a/devcycle_python_sdk/managers/config_manager.py b/devcycle_python_sdk/managers/config_manager.py index d0801e2..ae8c971 100644 --- a/devcycle_python_sdk/managers/config_manager.py +++ b/devcycle_python_sdk/managers/config_manager.py @@ -16,6 +16,7 @@ from wsgiref.handlers import format_date_time from devcycle_python_sdk.options import DevCycleLocalOptions from devcycle_python_sdk.managers.sse_manager import SSEManager +from devcycle_python_sdk.models.config_metadata import ConfigMetadata logger = logging.getLogger(__name__) @@ -108,7 +109,7 @@ def _get_config(self, last_modified: Optional[float] = None): ) self._polling_enabled = False - def get_config_metadata(self) -> dict: + def get_config_metadata(self) -> Optional[ConfigMetadata]: return self._local_bucketing.get_config_metadata() def run(self): diff --git a/devcycle_python_sdk/models/config_metadata.py b/devcycle_python_sdk/models/config_metadata.py index 9123d75..5d77152 100644 --- a/devcycle_python_sdk/models/config_metadata.py +++ b/devcycle_python_sdk/models/config_metadata.py @@ -1,5 +1,6 @@ from devcycle_python_sdk.models.environment_metadata import EnvironmentMetadata from devcycle_python_sdk.models.project_metadata import ProjectMetadata +from typing import Dict, Any, Optional import json @@ -16,10 +17,16 @@ def to_json(self) -> str: return json.dumps(self, default=lambda o: o.__dict__) @staticmethod - def from_json(json_str: str) -> "ConfigMetadata": - if json_str is None: + def from_json(json_obj: Optional[Dict[str, Any]]) -> Optional["ConfigMetadata"]: + if json_obj is None: return None + project = ProjectMetadata.from_json(json_obj.get("project")) + environment = EnvironmentMetadata.from_json(json_obj.get("environment")) + + if project is None or environment is None: + return None + return ConfigMetadata( - project=ProjectMetadata.from_json(json_str["project"]), - environment=EnvironmentMetadata.from_json(json_str["environment"]), - ) \ No newline at end of file + project=project, + environment=environment, + ) diff --git a/devcycle_python_sdk/models/environment_metadata.py b/devcycle_python_sdk/models/environment_metadata.py index c88290a..25817d5 100644 --- a/devcycle_python_sdk/models/environment_metadata.py +++ b/devcycle_python_sdk/models/environment_metadata.py @@ -1,3 +1,6 @@ +from typing import Dict, Any, Optional + + class EnvironmentMetadata: def __init__( self, @@ -8,10 +11,12 @@ def __init__( self.key = key @staticmethod - def from_json(json_str: str) -> "EnvironmentMetadata": - if json_str is None: + def from_json( + json_obj: Optional[Dict[str, Any]], + ) -> Optional["EnvironmentMetadata"]: + if json_obj is None: return None return EnvironmentMetadata( - id=json_str["id"], - key=json_str["key"], - ) \ No newline at end of file + id=json_obj["id"], + key=json_obj["key"], + ) diff --git a/devcycle_python_sdk/models/eval_hook_context.py b/devcycle_python_sdk/models/eval_hook_context.py index 76b7fe7..a040017 100644 --- a/devcycle_python_sdk/models/eval_hook_context.py +++ b/devcycle_python_sdk/models/eval_hook_context.py @@ -1,10 +1,17 @@ -from typing import Any +from typing import Any, Optional from devcycle_python_sdk.models.user import DevCycleUser from devcycle_python_sdk.models.config_metadata import ConfigMetadata + class HookContext: - def __init__(self, key: str, user: DevCycleUser, default_value: Any, config_metadata: ConfigMetadata = None): + def __init__( + self, + key: str, + user: DevCycleUser, + default_value: Any, + config_metadata: Optional[ConfigMetadata] = None, + ): self.key = key self.default_value = default_value self.user = user diff --git a/devcycle_python_sdk/models/project_metadata.py b/devcycle_python_sdk/models/project_metadata.py index 22476ce..fe2b1e7 100644 --- a/devcycle_python_sdk/models/project_metadata.py +++ b/devcycle_python_sdk/models/project_metadata.py @@ -1,3 +1,6 @@ +from typing import Dict, Any, Optional + + class ProjectMetadata: def __init__( self, @@ -8,10 +11,10 @@ def __init__( self.key = key @staticmethod - def from_json(json_str: str) -> "ProjectMetadata": - if json_str is None: + def from_json(json_obj: Optional[Dict[str, Any]]) -> Optional["ProjectMetadata"]: + if json_obj is None: return None return ProjectMetadata( - id=json_str["id"], - key=json_str["key"], - ) \ No newline at end of file + id=json_obj["id"], + key=json_obj["key"], + ) diff --git a/devcycle_python_sdk/options.py b/devcycle_python_sdk/options.py index 55a265e..00e2126 100644 --- a/devcycle_python_sdk/options.py +++ b/devcycle_python_sdk/options.py @@ -90,7 +90,7 @@ def __init__( if self.event_request_chunk_size > self.max_event_queue_size: logger.warning( - f"DevCycle: event_request_chunk_size: {self.event_request_chunk_size} must be smaller than max_event_queue_size: { self.max_event_queue_size}" + f"DevCycle: event_request_chunk_size: {self.event_request_chunk_size} must be smaller than max_event_queue_size: {self.max_event_queue_size}" ) self.event_request_chunk_size = 100 diff --git a/devcycle_python_sdk/protobuf/utils.py b/devcycle_python_sdk/protobuf/utils.py index c433bf6..eff7cac 100644 --- a/devcycle_python_sdk/protobuf/utils.py +++ b/devcycle_python_sdk/protobuf/utils.py @@ -34,11 +34,17 @@ def create_nullable_custom_data(val: Optional[dict]) -> pb2.NullableCustomData: if value is None: values[key] = pb2.CustomDataValue(type=pb2.CustomDataType.Null) # type: ignore elif isinstance(value, bool): - values[key] = pb2.CustomDataValue(type=pb2.CustomDataType.Bool, boolValue=value) # type: ignore + values[key] = pb2.CustomDataValue( + type=pb2.CustomDataType.Bool, boolValue=value + ) # type: ignore elif isinstance(value, str): - values[key] = pb2.CustomDataValue(type=pb2.CustomDataType.Str, stringValue=value) # type: ignore + values[key] = pb2.CustomDataValue( + type=pb2.CustomDataType.Str, stringValue=value + ) # type: ignore elif isinstance(value, (int, float)): - values[key] = pb2.CustomDataValue(type=pb2.CustomDataType.Num, doubleValue=value) # type: ignore + values[key] = pb2.CustomDataValue( + type=pb2.CustomDataType.Num, doubleValue=value + ) # type: ignore else: logger.warning( f"Custom Data contains data type that can't be written, will be ignored. Key: {key}, Type: {str(type(value))}" diff --git a/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py b/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py index 324e9e9..c313a65 100644 --- a/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +++ b/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py @@ -2,6 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # source: variableForUserParams.proto """Generated protocol buffer code.""" + from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database @@ -11,39 +12,40 @@ _sym_db = _symbol_database.Default() - - -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') +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' +) _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'variableForUserParams_pb2', _globals) +_builder.BuildTopDescriptorsAndMessages( + DESCRIPTOR, "variableForUserParams_pb2", _globals +) if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'\n&com.devcycle.sdk.server.local.protobufP\001Z\007./proto\252\002\"DevCycle.SDK.Server.Local.Protobuf' - _NULLABLECUSTOMDATA_VALUEENTRY._options = None - _NULLABLECUSTOMDATA_VALUEENTRY._serialized_options = b'8\001' - _globals['_VARIABLETYPE_PB']._serialized_start=1221 - _globals['_VARIABLETYPE_PB']._serialized_end=1285 - _globals['_CUSTOMDATATYPE']._serialized_start=1287 - _globals['_CUSTOMDATATYPE']._serialized_end=1341 - _globals['_NULLABLESTRING']._serialized_start=31 - _globals['_NULLABLESTRING']._serialized_end=78 - _globals['_NULLABLEDOUBLE']._serialized_start=80 - _globals['_NULLABLEDOUBLE']._serialized_end=127 - _globals['_CUSTOMDATAVALUE']._serialized_start=129 - _globals['_CUSTOMDATAVALUE']._serialized_end=238 - _globals['_NULLABLECUSTOMDATA']._serialized_start=241 - _globals['_NULLABLECUSTOMDATA']._serialized_end=388 - _globals['_NULLABLECUSTOMDATA_VALUEENTRY']._serialized_start=326 - _globals['_NULLABLECUSTOMDATA_VALUEENTRY']._serialized_end=388 - _globals['_VARIABLEFORUSERPARAMS_PB']._serialized_start=391 - _globals['_VARIABLEFORUSERPARAMS_PB']._serialized_end=547 - _globals['_DVCUSER_PB']._serialized_start=550 - _globals['_DVCUSER_PB']._serialized_end=910 - _globals['_SDKVARIABLE_PB']._serialized_start=913 - _globals['_SDKVARIABLE_PB']._serialized_end=1150 - _globals['_EVALREASON_PB']._serialized_start=1152 - _globals['_EVALREASON_PB']._serialized_end=1219 + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n&com.devcycle.sdk.server.local.protobufP\001Z\007./proto\252\002"DevCycle.SDK.Server.Local.Protobuf' + _NULLABLECUSTOMDATA_VALUEENTRY._options = None + _NULLABLECUSTOMDATA_VALUEENTRY._serialized_options = b"8\001" + _globals["_VARIABLETYPE_PB"]._serialized_start = 1221 + _globals["_VARIABLETYPE_PB"]._serialized_end = 1285 + _globals["_CUSTOMDATATYPE"]._serialized_start = 1287 + _globals["_CUSTOMDATATYPE"]._serialized_end = 1341 + _globals["_NULLABLESTRING"]._serialized_start = 31 + _globals["_NULLABLESTRING"]._serialized_end = 78 + _globals["_NULLABLEDOUBLE"]._serialized_start = 80 + _globals["_NULLABLEDOUBLE"]._serialized_end = 127 + _globals["_CUSTOMDATAVALUE"]._serialized_start = 129 + _globals["_CUSTOMDATAVALUE"]._serialized_end = 238 + _globals["_NULLABLECUSTOMDATA"]._serialized_start = 241 + _globals["_NULLABLECUSTOMDATA"]._serialized_end = 388 + _globals["_NULLABLECUSTOMDATA_VALUEENTRY"]._serialized_start = 326 + _globals["_NULLABLECUSTOMDATA_VALUEENTRY"]._serialized_end = 388 + _globals["_VARIABLEFORUSERPARAMS_PB"]._serialized_start = 391 + _globals["_VARIABLEFORUSERPARAMS_PB"]._serialized_end = 547 + _globals["_DVCUSER_PB"]._serialized_start = 550 + _globals["_DVCUSER_PB"]._serialized_end = 910 + _globals["_SDKVARIABLE_PB"]._serialized_start = 913 + _globals["_SDKVARIABLE_PB"]._serialized_end = 1150 + _globals["_EVALREASON_PB"]._serialized_start = 1152 + _globals["_EVALREASON_PB"]._serialized_end = 1219 # @@protoc_insertion_point(module_scope) diff --git a/example/django-app/manage.py b/example/django-app/manage.py index d28672e..aabb818 100755 --- a/example/django-app/manage.py +++ b/example/django-app/manage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys diff --git a/requirements.txt b/requirements.txt index ab3c71e..3645631 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ urllib3 >= 1.15.1 requests >= 2.32 wasmtime ~= 30.0.0 protobuf >= 4.23.3 -openfeature-sdk >= 0.8.0 +openfeature-sdk == 0.8.0 launchdarkly-eventsource >= 1.2.1 responses >= 0.23.1 \ No newline at end of file diff --git a/test/test_cloud_client.py b/test/test_cloud_client.py index a7c1176..6a3eb69 100644 --- a/test/test_cloud_client.py +++ b/test/test_cloud_client.py @@ -375,9 +375,9 @@ def test_context_has_null_config_metadata(self, mock_variable_call): mock_variable_call.return_value = Variable( _id="123", key="strKey", value=999, type=TypeEnum.NUMBER ) - + context_received = None - + def before_hook(context): nonlocal context_received context_received = context @@ -398,9 +398,11 @@ def error_hook(context, error): # Test that context has config_metadata field but it's null for cloud client variable = self.test_client.variable(self.test_user, "strKey", 42) - + + # Verify the variable evaluation worked + self.assertIsNotNone(variable) self.assertIsNotNone(context_received) - self.assertTrue(hasattr(context_received, 'config_metadata')) + self.assertTrue(hasattr(context_received, "config_metadata")) # Cloud client should have null config_metadata since it's not implemented self.assertIsNone(context_received.config_metadata) diff --git a/test/test_local_client.py b/test/test_local_client.py index 4538034..5c0be60 100644 --- a/test/test_local_client.py +++ b/test/test_local_client.py @@ -460,9 +460,9 @@ def error_hook(context, error): @responses.activate def test_context_has_config_metadata(self): self.setup_client() - + context_received = None - + def before_hook(context): nonlocal context_received context_received = context @@ -485,18 +485,26 @@ def error_hook(context, error): # Test that context has config_metadata field variable = self.client.variable(user, "num-var", 42) - + + # Verify the variable evaluation worked + self.assertIsNotNone(variable) self.assertIsNotNone(context_received) - self.assertTrue(hasattr(context_received, 'config_metadata')) + self.assertTrue(hasattr(context_received, "config_metadata")) # For local client, config_metadata should be populated self.assertIsNotNone(context_received.config_metadata) - self.assertTrue(hasattr(context_received.config_metadata, 'project')) - self.assertTrue(hasattr(context_received.config_metadata, 'environment')) + self.assertTrue(hasattr(context_received.config_metadata, "project")) + self.assertTrue(hasattr(context_received.config_metadata, "environment")) # Verify the project and environment data - self.assertEqual(context_received.config_metadata.project.id, "61f97628ff4afcb6d057dbf0") + self.assertEqual( + context_received.config_metadata.project.id, "61f97628ff4afcb6d057dbf0" + ) self.assertEqual(context_received.config_metadata.project.key, "emma-project") - self.assertEqual(context_received.config_metadata.environment.id, "61f97628ff4afcb6d057dbf2") - self.assertEqual(context_received.config_metadata.environment.key, "development") + self.assertEqual( + context_received.config_metadata.environment.id, "61f97628ff4afcb6d057dbf2" + ) + self.assertEqual( + context_received.config_metadata.environment.key, "development" + ) def _benchmark_variable_call(client: DevCycleLocalClient, user: DevCycleUser, key: str):