diff --git a/devcycle_python_sdk/api/local_bucketing.py b/devcycle_python_sdk/api/local_bucketing.py index 5786bf2..a4c1c59 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) -> 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) + 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 80994ca..bb18556 100644 Binary files a/devcycle_python_sdk/bucketing-lib.release.wasm and b/devcycle_python_sdk/bucketing-lib.release.wasm differ 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..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,6 +109,9 @@ def _get_config(self, last_modified: Optional[float] = None): ) self._polling_enabled = False + def get_config_metadata(self) -> Optional[ConfigMetadata]: + 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..5d77152 --- /dev/null +++ b/devcycle_python_sdk/models/config_metadata.py @@ -0,0 +1,32 @@ +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 + + +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_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=project, + environment=environment, + ) diff --git a/devcycle_python_sdk/models/environment_metadata.py b/devcycle_python_sdk/models/environment_metadata.py new file mode 100644 index 0000000..25817d5 --- /dev/null +++ b/devcycle_python_sdk/models/environment_metadata.py @@ -0,0 +1,22 @@ +from typing import Dict, Any, Optional + + +class EnvironmentMetadata: + def __init__( + self, + id: str, + key: str, + ): + self.id = id + self.key = key + + @staticmethod + def from_json( + json_obj: Optional[Dict[str, Any]], + ) -> Optional["EnvironmentMetadata"]: + if json_obj is None: + return None + return EnvironmentMetadata( + 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 5c3ee96..a040017 100644 --- a/devcycle_python_sdk/models/eval_hook_context.py +++ b/devcycle_python_sdk/models/eval_hook_context.py @@ -1,10 +1,18 @@ -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): + 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 + 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..fe2b1e7 --- /dev/null +++ b/devcycle_python_sdk/models/project_metadata.py @@ -0,0 +1,20 @@ +from typing import Dict, Any, Optional + + +class ProjectMetadata: + def __init__( + self, + id: str, + key: str, + ): + self.id = id + self.key = key + + @staticmethod + def from_json(json_obj: Optional[Dict[str, Any]]) -> Optional["ProjectMetadata"]: + if json_obj is None: + return None + return ProjectMetadata( + 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 289f5b9..c313a65 100644 --- a/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +++ b/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py @@ -1,59 +1,51 @@ # -*- 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() - - -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) -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' - _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 +_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 # @@protoc_insertion_point(module_scope) 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/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 7f74327..6a3eb69 100644 --- a/test/test_cloud_client.py +++ b/test/test_cloud_client.py @@ -370,6 +370,42 @@ 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) + + # Verify the variable evaluation worked + self.assertIsNotNone(variable) + 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..5c0be60 100644 --- a/test/test_local_client.py +++ b/test/test_local_client.py @@ -457,6 +457,55 @@ 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) + + # Verify the variable evaluation worked + self.assertIsNotNone(variable) + 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"